Injecting Terminal Input

It all started with a simple question:

Is it possible in an interactive bash shell to enter a command that outputs some text so that it appears at the next command prompt, as if the user had typed in that text at that prompt ?

What happened was an interesting digression. Asking the question on Stack Exchange produced some interesting answers but the gist of it is that

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

There other ways to inject input such as those built in to tmux and screen, and xdotool works in an environment running X.Org.

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 ouput 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.

Think about all those other escape sequences and what fun can be had with them (colours, anyone?). Yes - you've used them before but perhaps never given them much thought...

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

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 http://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.

An interactive injectable shell

The method described here is also discussed here and builds on the method discussed here.

The $0 parameter is the name of the executable. At the shell prompt, it's the name of the shell (i.e. bash). If the shell is a login shell then it's prefixed with a hyphen (i.e. -bash) as described on the bash man page (see the Invocation section therein). This breaks the referenced examples when used in a login shell. If this is a problem then replace $0 with ${0#-} which omits the leading hyphen, if any.

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 preceeding 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/john/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