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 uses bind
.
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