52

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 ?

I want to be able to source a script that will generate a command-line and output it so that it appears when the prompt returns after the script ends so that the user can optionally edit it before pressing enter to execute it.

This can be achieved with xdotool but that only works when the terminal is in an X window and only if it's installed.

[me@mybox] 100 $ xdotool type "ls -l"
[me@mybox] 101 $ ls -l  <--- cursor appears here!

Can this be done using bash only?

starfry
  • 7,442
  • I'm thinking this should not be hard with Expect, if you can tolerate that, and have it drive a subshell; but I don' remember enough of it to post an actual answer. – tripleee May 17 '17 at 19:08

5 Answers5

50

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.

  • I think bind '"\e[0n": "echo test"'; printf '\e[5n' probably the bash-only answer I'm looking for. It works for me. However, I get ^[[0n printed before my prompt as well. I discovered this is caused when $PS1 contains a subshell. You can reproduce it by doing PS1='$(:)' before the bind command. Why would that happen and can anything be done about it? – starfry Jul 04 '15 at 08:43
  • Although everything in this answer is correct, the question was for bash, not zsh. Sometimes we do not have a choice of what shell to use. – Falsenames Jul 04 '15 at 22:49
  • @Falsenames only the first paragraph is for zsh. The rest is either shell agnostic or bash specific. The Q&A doesn't have to be useful only to bash users. – Stéphane Chazelas Jul 05 '15 at 06:43
  • 1
    @starfry seems like maybe you could just put a \return at the head of $PS1? That should work if $PS1 is long enough. If not then put ^[[M there. – mikeserv Jul 05 '15 at 23:51
  • @mikeserv - r does the trick. This doesn't of course prevent the output, it's just overwritten before the eye sees it. I guess ^[[M erases the line to clear the injected text in case it's longer than the prompt. Is that right (I could't find it in the ANSI escape list I have) ? – starfry Jul 07 '15 at 08:23
  • @starfry- Yes - the ^[[M escape should delete the line. Or ^[[K\r should be fine, too. – mikeserv Jul 07 '15 at 10:55
  • @starfry - that will have other consequences though - readline isn't very good about prompt positioning. So any time output to the terminal doesn't end w/ a newline, your prompt will overwrite it. Do printf "where'd this go?" to see what i mean. – mikeserv Jul 07 '15 at 11:07
  • @stephane-chazelas - your typeahead workaround wrapper is good; it works also when injecting multiple lines and works with TIOCSTI too. The \r\e[M suggested by @mikeserv is simpler but doesn't work across newlines. – starfry Jul 08 '15 at 10:41
  • @starfry - how do you get newlines out of ^[[0n? – mikeserv Jul 08 '15 at 18:09
  • @mikeserv - I don't. Sorry I was referring to using \r\e[M with the TIOCSTI method. If I send a command that way (such as echo test) and include a newline then the command appears before the prompt because the a new line has been fed and that (empty) line is the only one that r\e[M affects. I don't think you can get a newline out of ^[[0n. – starfry Jul 08 '15 at 18:34
  • Actually, @mikeserv, doing bind '"\e[0n":"ls^M"' where ^M is obtained by pressing CONTROL+V followed by CONTROL+M gives you a newline in the bound string. – starfry Jul 10 '15 at 08:23
  • @starfry - yeah, that makes sense. Linux terminals set the stty sane mode with icrnl - which means they translate input carriage returns to newlines. I t makes sense that readline would obey that in interpreted bound input. Cool trick, too. If you didn't yet notice, a few days ago I updated my own answer here to drop the while loop entirely and simplify the whole affair a good deal to the point that the bash launched gets a direct write descriptor on its own stdin. Actually, much of the simplification was due to my rereading Stéphane's suggestions while awake enough to understand some. – mikeserv Jul 10 '15 at 08:50
  • @StéphaneChazelas @mikeserv Can one somehow extend your solutions to harder task: write a command to the other terminal, say /dev/pts/3? For simplification, lets say both terminals use the same shell type. – jimmij Jul 20 '15 at 20:59
  • @jimmij - With super-user permissions on a linux system, or with reptyr or similar, or with at least the possibility of first entering a command or two at the other terminal (such as while sleep 1; do :; done), then any of them could be made to work. The terminal emulator solutions will just work anyway, though. printf \\33\[escape_sequence >/dev/pts/[num] will just work if the reading program interprets the escape. xterm can also be told to use a pre-existing pty with xterm -S. – mikeserv Jul 20 '15 at 22:17
  • @jimmij - Yes you can write to another terminal. Your question prompted me to post my personal notes resulting from this question and there is an example C program there that does exactly this. Hope it helps. – starfry Jul 21 '15 at 14:27
  • Now at the end of my .bashrc: [[ $TERM == screen* ]] && bind '"\e[0n": "tmux && exit"' && ((sleep 0.05; printf '\e[5n') &) – Mathieu CAROFF Jul 05 '18 at 20:59
37

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
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
starfry
  • 7,442
  • With the easiest bind '"\e[0n": "ls -l"'; printf '\e[5n' solution, after all the output of the ls -l also ^[[0n will be outputted on the Terminal once I hit the enter key thus run ls -l. Any ideas how to "hide" it please? Thank you. – Ali Nov 14 '19 at 00:44
  • 1
    I presented one solution that gives the effect you are after - in the clean output section of my answer I suggest adding a return to the prompt to hide surpflouous text. I tried PS1="\r\e[M$PS1" before doing bind '"\e[0n": "ls -l"'; printf '\e[5n' and that gave the effect you describe. – starfry Nov 14 '19 at 10:14
  • Thank you! I totally missed that point. – Ali Nov 15 '19 at 11:04
  • 1
    As of Linux 6.2, TIOCSTI is disabled by default: https://lore.kernel.org/linux-hardening/20221015041626.1467372-2-keescook@chromium.org/ – Jouni K. Seppänen Apr 02 '23 at 11:20
20

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

mikeserv
  • 58,310
  • 2
    What terminals did you manage to get that working in? That kind of thing was being abused in the olden days and should be disabled by default nowadays. With xterm, you can still query the icon title with \e[20t but only if configured with allowWindowOps: true. – Stéphane Chazelas Jul 03 '15 at 22:22
  • @StéphaneChazelas that works in Terminology, but i'm pretty sure it also works in gnome terminal, in the kde terminal (i forget its name, and i think there's a different escape), and as you say, w/ xterm w/ the proper config. W/ an xterm proper, though, you can read and write the copy/paste buffer and so it gets more simple besides, I think. Xterm also has escape sequences for changing/affecting the term description itself. – mikeserv Jul 03 '15 at 23:28
  • I can't get that to work in anything but terminology (which btw has several other similar vulnerabilities). That CVE being over 12 years old and relatively well known I'd be surprised if any of the main terminal emulator had the same vulnerability. Note that with xterm, that's \e[20t (not \e]1;?\a) – Stéphane Chazelas Jul 03 '15 at 23:32
  • @StéphaneChazelas - that's really good news - that's always worried me. i only pulled the information at all from theinivisbleisland.net list of escapes for xterm - it's definitely documented there for OSC. It also reports that an xterm will respond to ENQ w/ the string saved in its answerbackString resource - so that could work as another route. I should probably edit this and just tell the right way to do it - with a pty. – mikeserv Jul 03 '15 at 23:41
  • 1
  • @StéphaneChazelas - I found the 20t escape you mentioned. Thank you. And copy/paste buffer thing only works for xterms w/ allowwindowOp resources enabled as well. – mikeserv Jul 04 '15 at 00:03
  • @StéphaneChazelas - if you get this, will you look at my edit and maybe bestow some wisdom about the job control error? – mikeserv Jul 04 '15 at 07:16
  • You'll want to also set stdout and stderr to that slave (and relay to the host tty). and do a stty raw -echo on the host (no isig no opost...) – Stéphane Chazelas Jul 04 '15 at 11:38
  • @StéphaneChazelas - thanks, but maybe I'm a little slow cause I'm sleepy - would you mind expanding on that a little? If I set stdout+err >&8 then how do I get the output on screen? I suspected the isig had to do with the job control thing... Another thing which bugging me is the winch stuff doesn't work - I can query the host's rows/cols with stty -aF"$TTY" in the slave session, but no trap seems effective on either side - slave or host. There must be a way to capture that signal... maybe backgrounding cat and using wait... – mikeserv Jul 04 '15 at 11:47
  • You'll need another relaying cat <&9. For winch, either the shell has its own handler and your trap will be ignored or it hasn't and COLS/LINES won't be updated. – Stéphane Chazelas Jul 04 '15 at 12:05
  • @StéphaneChazelas - about COLS/LINES - i had tried giving up on those shell vars and did a thing like trap 'stty -aF"$TTY"|{ IFS=\; read n r c n; IFS=\ ;stty $r $c </dev/tty; }' WINCH in the slave session. It works - if I do kill -WINCH -0 myself, but it just doesn't catch it automatically. I did do trap 'kill -WINCH "$!"' WINCH in the host session, too. I'll try the cat thing now. I didn't do that because I thought it would eat the slave's input. But I guess the master get's its own copy, too, huh? – mikeserv Jul 04 '15 at 12:13
  • @StéphaneChazelas - well, I did do the >&8 2>&8, and I did stty raw, but I also redirected the slave's stdout+err back at the host with an echo'd exec and kept opost. The winch thing? Probably I'll figure it out later. I did the redirect because the prompts weren't coming through on <&9 for bash - readline is weird. Anyway, all of the slaves isig controls work - and rearranged execution order to make an exit in the slave break up the whole affair. It's pretty good. It was fun to do. Thanks for the help. – mikeserv Jul 04 '15 at 13:17
  • @mikeserv I was trying to digest your answer. Could you explain the escape sequences in your printf? I looked at this doc which is probably the right haystack but the needle eludes me :) – starfry Jul 13 '15 at 09:27
  • @starfry - yeah, it's hard at first. \33[ is the 7-bit equivalent for CSI (there is a single, 8-bit character for it too, but \33[ is what I use). That's the biggest hurdle, really. OSC is \33] - so all of the escapes here start w/ 1 or the other, and end w/ \33\ - the string terminator. \25 is ^U - which erases all of the terminal junk at the head of the line. So we save OSC 1;^Umy command ST to icon_name, then ? query it, then reset it. The xterm bit is similar, but xterm provides a stack. Anyway, you should be looking at the pty stuff. The script is pretty good. – mikeserv Jul 13 '15 at 09:36
  • @mikserv I have been pulling my hair out trying to understand the script but I have it now thanks to your linked answer and a bit of experimentation. I guess this could be used to wrap more complex things such as a chroot or other container. Why the -- - are they needed? it seems to work without them. Explaining this would make a good interview question (or perhaps that's just too mean :)) – starfry Jul 14 '15 at 13:29
  • @starfry - no, -- really aren't needed. But (one of) my own version[s] is a little more complicated, and i don't do setsid -wc bash but rather setsid -wc -- "$@" and use the sh -c '...' -- my list of arbitrary arguments that need executing to easily pass in arbitrary execution statements without fretting over quote worries. In that case if arg1 happens to begin with a - there's no need to worry, even if such a thing is incredibly unlikely anyway. About the interview thing - I sure could use one those, but no idea where to start... – mikeserv Jul 14 '15 at 15:19
8

Though the ioctl(,TIOCSTI,) answer by Stéphane Chazelas is, of course, the right answer, some people might be happy enough with this partial but trivial answer: simply push the command onto the history stack, then the user can move 1 line up the history to find the command.

$ history -s "ls -l"
$ echo "move up 1 line in history to get command to run"

This can become a simple script, which has its own 1 line history:

#!/bin/bash
history -s "ls -l"
read -e -p "move up 1 line: "
eval "$REPLY"

read -e enables readline editing of the input, -p is a prompt.

meuh
  • 51,383
  • That will only work in shell functions, or if the script was sourced (. foo.sh or `source foo.sh, instead of run in a subshell.) Interesting approach, though. A similar hack that requires modifying the context of the calling shell would be to set up a custom completion that expanded the empty line to something, and then restored the old completion handler. – Peter Cordes Jul 06 '15 at 01:01
  • @PeterCordes you are right. I was taking the question too literally. But I've added an example of a simple script that could work. – meuh Jul 06 '15 at 06:36
  • @mikeserv Hey, its just a simple solution that may be useful to some people. You can even remove the eval if you have simple commands to edit, without pipes and redirection etc. – meuh Jul 06 '15 at 07:40
3

Oh my word, we missed a simple solution built-in to bash: the read command has an option -i ..., which when used with -e, pushes text into the input buffer. From the man page:

-i text

If readline is being used to read the line, text is placed into the editing buffer before editing begins.

So create a small bash function or shell script which takes the command to present to the user, and runs or evaluates their reply:

domycmd(){ read -e -i "$*"; eval "$REPLY"; }

This no doubt uses the ioctl(,TIOCSTI,) which has been around for over 32 years, as it already existed in 2.9BSD ioctl.h.

meuh
  • 51,383
  • 1
    Interesting one with a similar effect, but it doesn't inject to the prompt though. – starfry Jul 24 '15 at 13:19
  • on 2nd thoughts you are right. bash doesnt need to TIOCSTI since it's doing all the i/o itself. – meuh Jul 24 '15 at 14:08