Can bash write to its own input stream?
With zsh
, you can use print -z
to place some text into the line editor buffer for the next prompt:
print -z echo test
would prime the line editor with echo test
which you can edit at the next prompt.
I don't think bash
has a similar feature, however on many systems, you can prime the terminal device input buffer with the TIOCSTI
ioctl()
:
perl -e 'require "sys/ioctl.ph"; ioctl(STDIN, &TIOCSTI, $_)
for split "", join " ", @ARGV' echo test
Would insert echo test
into the terminal device input buffer, as if received from the terminal.
A more portable variation on @mike's Terminology
approach and that doesn't sacrifice security would be to send the terminal emulator a fairly standard query status report
escape sequence: <ESC>[5n
which terminals invariably reply (so as input) as <ESC>[0n
and bind that to the string you want to insert:
bind '"\e[0n": "echo test"'; printf '\e[5n'
If within GNU screen
, you can also do:
screen -X stuff 'echo test'
Now, except for the TIOCSTI ioctl approach, we're asking the terminal emulator to send us some string as if typed. If that string comes before readline
(bash
's line editor) has disabled terminal local echo, then that string will be displayed not at the shell prompt, messing up the display slightly.
To work around that, you could either delay the sending of the request to the terminal slightly to make sure the response arrives when the echo has been disabled by readline.
bind '"\e[0n": "echo test"'; ((sleep 0.05; printf '\e[5n') &)
(here assuming your sleep
supports sub-second resolution).
Ideally you'd want to do something like:
bind '"\e[0n": "echo test"'
stty -echo
printf '\e[5n'
wait-until-the-response-arrives
stty echo
However bash
(contrary to zsh
) doesn't have support for such a wait-until-the-response-arrives
that doesn't read the response.
However it has a has-the-response-arrived-yet
feature with read -t0
:
bind '"\e[0n": "echo test"'
saved_settings=$(stty -g)
stty -echo -icanon min 1 time 0
printf '\e[5n'
until read -t0; do
sleep 0.02
done
stty "$saved_settings"
Further reading
See @starfry's answer's that expands on the two solutions given by @mikeserv and myself with a few more detailed information.
This answer is provided as clarification of my own understanding and is inspired by @StéphaneChazelas and @mikeserv before me.
TL;DR
- it isn't possible to do this in
bash
without external help; - the correct way to do this is with a send terminal input
ioctl
but - the easiest workable
bash
solution usesbind
.
The easy solution
bind '"\e[0n": "ls -l"'; printf '\e[5n'
Bash has a shell builtin called bind
that allows a shell command to be executed when a key sequence is received. In essence, the output of the shell command is written to the shell's input buffer.
$ bind '"\e[0n": "ls -l"'
The key sequence \e[0n
(<ESC>[0n
) is an ANSI Terminal escape code that a terminal sends to indicate that it is functioning normally. It sends this in response to a device status report request which is sent as <ESC>[5n
.
By binding the response to an echo
that outputs the text to inject, we can inject that text whenever we want by requesting device status and that's done by sending a <ESC>[5n
escape sequence.
printf '\e[5n'
This works, and is probably sufficient to answer the original question because no other tools are involved. It's pure bash
but relies on a well-behaving terminal (practically all are).
It leaves the echoed text on the command line ready to be used as if it had been typed. It can be appended, edited, and pressing ENTER
causes it to be executed.
Add \n
to the bound command to have it executed automatically.
However, this solution only works in the current terminal (which is within the scope of the original question). It works from an interactive prompt or from a sourced script but it raises an error if used from a subshell:
bind: warning: line editing not enabled
The correct solution described next is more flexible but it relies on external commands.
The correct solution
The proper way to inject input uses tty_ioctl, a unix system call for I/O Control that has a TIOCSTI
command that can be used to inject input.
TIOC from "Terminal IOCtl" and STI from "Send Terminal Input".
There is no command built into bash
for this; doing so requires an external command. There isn't such a command in the typical GNU/Linux distribution but it isn't difficult to achieve with a little programming. Here's a shell function that uses perl
:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
Here, 0x5412
is the code for the TIOCSTI
command.
TIOCSTI
is a constant defined in the standard C header files with the value 0x5412
. Try grep -r TIOCSTI /usr/include
, or look in /usr/include/asm-generic/ioctls.h
; it's included in C programs indirectly by #include <sys/ioctl.h>
.
You can then do:
$ inject ls -l
ls -l$ ls -l <- cursor here
Implementations in some other languages are shown below (save in a file and then chmod +x
it):
Perl inject.pl
#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV
You can generate sys/ioctl.ph
which defines TIOCSTI
instead of using the numeric value. See here
Python inject.py
#!/usr/bin/python
import fcntl, sys, termios
del sys.argv[0]
for c in ' '.join(sys.argv):
fcntl.ioctl(sys.stdin, termios.TIOCSTI, c)
Ruby inject.rb
#!/usr/bin/ruby
ARGV.join(' ').split('').each { |c| $stdin.ioctl(0x5412,c) }
C inject.c
compile with gcc -o inject inject.c
#include <sys/ioctl.h>
int main(int argc, char *argv[])
{
int a,c;
for (a=1, c=0; a< argc; c=0 )
{
while (argv[a][c])
ioctl(0, TIOCSTI, &argv[a][c++]);
if (++a < argc) ioctl(0, TIOCSTI," ");
}
return 0;
}
**!**There are further examples here.
Using ioctl
to do this works in subshells. It can also inject into other terminals as explained next.
Taking it further (controlling other terminals)
It's beyond the scope of the original question but it is possible to inject characters into another terminal, subject to having the appropriate permissions. Normally this means being root
, but see below for other ways.
Extending the C program given above to accept a command-line argument specifying another terminal's tty allows injecting to that terminal:
#include <stdlib.h>
#include <argp.h>
#include <sys/ioctl.h>
#include <sys/fcntl.h>
const char *argp_program_version ="inject - see https://unix.stackexchange.com/q/213799";
static char doc[] = "inject - write to terminal input stream";
static struct argp_option options[] = {
{ "tty", 't', "TTY", 0, "target tty (defaults to current)"},
{ "nonl", 'n', 0, 0, "do not output the trailing newline"},
{ 0 }
};
struct arguments
{
int fd, nl, next;
};
static error_t parse_opt(int key, char *arg, struct argp_state *state) {
struct arguments *arguments = state->input;
switch (key)
{
case 't': arguments->fd = open(arg, O_WRONLY|O_NONBLOCK);
if (arguments->fd > 0)
break;
else
return EINVAL;
case 'n': arguments->nl = 0; break;
case ARGP_KEY_ARGS: arguments->next = state->next; return 0;
default: return ARGP_ERR_UNKNOWN;
}
return 0;
}
static struct argp argp = { options, parse_opt, 0, doc };
static struct arguments arguments;
static void inject(char c)
{
ioctl(arguments.fd, TIOCSTI, &c);
}
int main(int argc, char *argv[])
{
arguments.fd=0;
arguments.nl='\n';
if (argp_parse (&argp, argc, argv, 0, 0, &arguments))
{
perror("Error");
exit(errno);
}
int a,c;
for (a=arguments.next, c=0; a< argc; c=0 )
{
while (argv[a][c])
inject (argv[a][c++]);
if (++a < argc) inject(' ');
}
if (arguments.nl) inject(arguments.nl);
return 0;
}
It also sends a newline by default but, similar to echo
, it provides a -n
option to suppress it. The --t
or --tty
option requires an argument - the tty
of the terminal to be injected. The value for this can be obtained in that terminal:
$ tty
/dev/pts/20
Compile it with gcc -o inject inject.c
. Prefix the text to inject with --
if it contains any hyphens to prevent the argument parser misinterpreting command-line options. See ./inject --help
. Use it like this:
$ inject --tty /dev/pts/22 -- ls -lrt
or just
$ inject -- ls -lrt
to inject the current terminal.
Injecting into another terminal requires administrative rights that can be obtained by:
- issuing the command as
root
, - using
sudo
, - having the
CAP_SYS_ADMIN
capability or - setting the executable
setuid
To assign CAP_SYS_ADMIN
:
$ sudo setcap cap_sys_admin+ep inject
To assign setuid
:
$ sudo chown root:root inject
$ sudo chmod u+s inject
Clean output
Injected text appears ahead of the prompt as if it was typed before the prompt appeared (which, in effect, it was) but it then appears again after the prompt.
One way to hide the text that appears ahead of the prompt is to prepend the prompt with a carriage return (\r
not line-feed) and clear the current line (<ESC>[M
):
$ PS1="\r\e[M$PS1"
However, this will only clear the line on which the prompt appears. If the injected text includes newlines then this won't work as intended.
Another solution disables echoing of injected characters. A wrapper uses stty
to do this:
saved_settings=$(stty -g)
stty -echo -icanon min 1 time 0
inject echo line one
inject echo line two
until read -t0; do
sleep 0.02
done
stty "$saved_settings"
where inject
is one of the solutions described above, or replaced by printf '\e[5n'
.
Alternative approaches
If your environment meets certain prerequisites then you may have other methods available that you can use to inject input. If you're in a desktop environment then xdotool is an X.Org utility that simulates mouse and keyboard activity but your distro may not include it by default. You can try:
$ xdotool type ls
If you use tmux, the terminal multiplexer, then you can do this:
$ tmux send-key -t session:pane ls
where -t
selects which session and pane to inject. GNU Screen has a similar capability with its stuff
command:
$ screen -S session -p pane -X stuff ls
If your distro includes the console-tools package then you may have a writevt
command that uses ioctl
like our examples. Most distros have, however, deprecated this package in favour of kbd which lacks this feature.
An updated copy of writevt.c can be compiled using gcc -o writevt writevt.c
.
Other options that may fit some use-cases better include expect and empty which are designed to allow interactive tools to be scripted.
You could also use a shell that supports terminal injection such as zsh
which can do print -z ls
.
The "Wow, that's clever..." answer
The method described here is also discussed here and builds on the method discussed here.
A shell redirect from /dev/ptmx
gets a new pseudo-terminal:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
A little tool written in C that unlocks the pseudoterminal master (ptm) and outputs the name of the pseudoterminal slave (pts) to its standard output.
#include <stdio.h>
int main(int argc, char *argv[]) {
if(unlockpt(0)) return 2;
char *ptsname(int fd);
printf("%s\n",ptsname(0));
return argc - 1;
}
(save as pts.c
and compile with gcc -o pts pts.c
)
When the program is called with its standard input set to a ptm it unlocks the corresponding pts and outputs its name to standard output.
$ ./pts </dev/ptmx
/dev/pts/20
The unlockpt() function unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by the given file descriptor. The program passes this as zero which is the program's standard input.
The ptsname() function returns the name of the slave pseudoterminal device corresponding to the master referred to by the given file descriptor, again passing zero for the program's standard input.
A process can be connected to the pts. First get a ptm (here it's assigned to file descriptor 3, opened read-write by the <>
redirect).
exec 3<>/dev/ptmx
Then start the process:
$ (setsid -c bash -i 2>&1 | tee log) <>"$(./pts <&3)" 3>&- >&0 &
The processes spawned by this command-line is best illustrated with pstree
:
$ pstree -pg -H $(jobs -p %+) $$
bash(5203,5203)─┬─bash(6524,6524)─┬─bash(6527,6527)
│ └─tee(6528,6524)
└─pstree(6815,6815)
The output is relative to the current shell ($$
) and the PID (-p
) and PGID (-g
) of each process are shown in parentheses (PID,PGID)
.
At the head of the tree is bash(5203,5203)
, the interactive shell that we're typing commands into, and its file descriptors connect it to the terminal application we're using to interact with it (xterm
, or similar).
$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3
Looking at the command again, the first set of parentheses started a subshell, bash(6524,6524)
) with its file descriptor 0 (its standard input) being assigned to the pts (which is opened read-write, <>
) as returned by another subshell that executed ./pts <&3
to unlock the pts associated with file descriptor 3 (created in the preceding step, exec 3<>/dev/ptmx
).
The subshell's file descriptor 3 is closed (3>&-
) so that the ptm isn't accessible to it. Its standard input (fd 0), which is the pts that was opened read/write, is redirected (actually the fd is copied - >&0
) to its standard output (fd 1).
This creates a subshell with its standard input and output connected to the pts. It can be sent input by writing to the ptm and its output can be seen by reading from the ptm:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
The subshell executes this command:
setsid -c bash -i 2>&1 | tee log
It runs bash(6527,6527)
in interactive (-i
) mode in a new session (setsid -c
, note the PID and PGID are the same). Its standard error is redirected to its standard output (2>&1
) and piped via tee(6528,6524)
so it's written to a log
file as well as to the pts. This gives another way to see the subshell's output:
$ tail -f log
Because the subshell is running bash
interactively, it can be sent commands to execute, like this example which displays the subshell's file descriptors:
$ echo 'ls -l /dev/fd/' >&3
Reading subshell's output (tail -f log
or cat <&3
) reveals:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
Standard input (fd 0) is connected to the pts and both standard output (fd 1) and error (fd 2) are connected to the same pipe, the one that connects to tee
:
$ (find /proc -type l | xargs ls -l | fgrep 'pipe:[116261]') 2>/dev/null
l-wx------ /proc/6527/fd/1 -> pipe:[116261]
l-wx------ /proc/6527/fd/2 -> pipe:[116261]
lr-x------ /proc/6528/fd/0 -> pipe:[116261]
And a look at the file descriptors of tee
$ ls -l /proc/6528/fd/
lr-x------ 0 -> pipe:[116261]
lrwx------ 1 -> /dev/pts/17
lrwx------ 2 -> /dev/pts/3
l-wx------ 3 -> /home/myuser/work/log
Standard Output (fd 1) is the pts: anything that 'tee' writes to its standard output is sent back to the ptm. Standard Error (fd 2) is the pts belonging to the controlling terminal.
Wrapping it up
The following script uses the technique described above. It sets up an interactive bash
session that can be injected by writing to a file descriptor. It's available here and documented with explanations.
sh -cm 'cat <&9 &cat >&9|( ### copy to/from host/slave
trap " stty $(stty -g ### save/restore stty settings on exit
stty -echo raw) ### host: no echo and raw-mode
kill -1 0" EXIT ### send a -HUP to host pgrp on EXIT
<>"$($pts <&9)" >&0 2>&1\
setsid -wc -- bash) <&1 ### point bash <0,1,2> at slave and setsid bash
' -- 9<>/dev/ptmx 2>/dev/null ### open pty master on <>9
It depends on what you mean by bash
only. If you mean a single, interactive bash
session, then the answer is almost definitely no. And this is because even when you enter a command like ls -l
at the command-line on any canonical terminal then bash
is not yet even aware of it - and bash
isn't even involved at that point.
Rather, what has happened up to that point is that the kernel's tty line-discipline has buffered and stty echo
d the user's input only to the screen. It flushes that input to its reader - bash
, in your example case - line by line - and generally translates \r
eturns to \n
ewlines on Unix systems as well - and so bash
isn't - and so neither can your sourced script be - made aware there is any input at all until the user presses the ENTER
key.
Now, there are some work-arounds. The most robust is not a work-around at all, actually, and involves using multiple processes or specially-written programs to sequence input, hide the line-discipline's -echo
from the user, and only write to the screen what is judged appropriate while interpreting input specially when necessary. This can be difficult to do well because it means writing interpretation rules which can handle arbitrary input char by char as it arrives and to write it out simultaneously without mistake in order to simulate what the average user would expect in that scenario. It is for this reason, probably, that interactive terminal i/o is so rarely well understood - a prospect that difficult is not one which lends itself to further investigation for most.
Another work-around could involve the terminal emulator. You say that a problem for you is a dependency on X and on xdotool
. In that case such a work-around as I'm about to offer might have similar issues, but I'll go forward with it just the same.
printf '\33[22;1t\33]1;%b\33\\\33[20t\33[23;0t' \
'\025my command'
That will work in an xterm
w/ the allowwindowOps
resource set. It first saves the icon/window names on a stack, then sets the terminal's icon-string to ^Umy command
then requests that the terminal inject that name into the input queue, and last resets it to the saved values. It should work invisibly for interactive bash
shells run in an xterm
w/ the right config- but it's probably a bad idea. Please see Stéphane's comments below.
Here, though, is a picture I took of my Terminology terminal after running the printf
bit w/ a different escape sequence on my machine. For each newline in the printf
command I typed CTRL+V
then CTRL+J
and afterward pressed the ENTER
key. I typed nothing afterward, but, as you can see, the terminal injected my command
into the line-discipline's input queue for me:
The real way to do this is w/ a nested pty. It is how screen
and tmux
and similar work - both of which, by the way, can make this possible for you. xterm
actually comes with a little program called luit
which can also make this possible. It is not easy, though.
Here's one way you might:
sh -cm 'cat <&9 &cat >&9|( ### copy to/from host/slave
trap " stty $(stty -g ### save/restore stty settings on exit
stty -echo raw) ### host: no echo and raw-mode
kill -1 0" EXIT ### send a -HUP to host pgrp on EXIT
<>"$(pts <&9)" >&0 2>&1\
setsid -wc -- bash) <&1 ### point bash <0,1,2> at slave and setsid bash
' -- 9<>/dev/ptmx 2>/dev/null ### open pty master on <>9
That is by no means portable, but should work on most Linux systems given proper permissions for opening /dev/ptmx
. My user is in the tty
group which is enough on my system. You'll also need...
<<\C cc -xc - -o pts
#include <stdio.h>
int main(int argc, char *argv[]) {
if(unlockpt(0)) return 2;
char *ptsname(int fd);
printf("%s\n",ptsname(0));
return argc - 1;
}
C
...which, when run on a GNU system (or any other with a standard C compiler that can also read from stdin), will write out a small executable binary named pts
that will run the unlockpt()
function on its stdin and write to its stdout the name of the pty device it just unlocked. I wrote it when working on... How do I come by this pty and what can I do with it?.
Anyway, what the above bit of code does is runs a bash
shell in a pty a layer beneath the current tty. bash
is told to write all output to the slave pty, and the current tty is configured both not to -echo
its input nor to buffer it, but instead to pass it (mostly) raw
to cat
, which copies it over to bash
. And all the while another, backgrounded cat
copies all slave output to the current tty.
For the most part the above configuration would be entirely useless - just redundant, basically - except that we launch bash
with a copy of its own pty master fd on <>9
. This means that bash
can freely write to its own input stream with a simple redirection. All that bash
has to do is:
echo echo hey >&9
...to talk to itself.
Here's another picture: