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

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 echod 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 \returns to \newlines 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:

term_inject

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:

enter image description here

Tags:

Bash