52

I'd like to be able to inject an initial command into the launching of an interactive process, so that I can do something like this:

echo "initial command" | INSERT_MAGIC_HERE some_tool

tool> initial command 

[result of initial command] 

tool> [now I type an interactive command]

What doesn't work:

  • Just piping the initial command in doesn't work, as this results in stdin not being connected to the terminal

  • Writing to /dev/pts/[number] sends the output to the terminal, not input to the process as if it were from the terminal

What would but with disadvantages:

  • Make a command which forks a child, writes to its stdin and then forwards everything from its own stdin. Downside - terminal control things (like line vs character mode) won't work. Maybe I could do something with proxying of pseudo terminals?

  • Make a modified version of xterm (I'm launching one for this task anyway) with a command line option to inject additional commands after encountering a desired prompt string. Ugly.

  • Make a modified version of the tool I'm trying to run so that it accepts an initial command on the command line. Breaks the standard installation.

(The tool of current interest, incidentally, is android's adb shell - I want to open an interactive shell on the phone, run a command automatically, and then have an interactive session)

Chris Stratton
  • 39,853
  • 6
  • 84
  • 117
  • Does `adb` has options similar to `-i`, `-c` like in the following command: `python -i -c'print "initial command"'`? Here an initial command is `print "initial command"` and `-i` option forces interactive mode afterwards. – jfs May 01 '11 at 02:28
  • I've written the program to create a pipe, fork, dup2 that to the child, put its own stdin in raw mode, push an initial string through the fifo, and then proxy stdin through. Seems to work in this case, but still wondering if there was a standard solution. – Chris Stratton May 01 '11 at 04:13

7 Answers7

61

You don't need to write a new tool to forward stdin - one has already been written (cat):

(echo "initial command" && cat) | some_tool

This does have the downside of connecting a pipe to some_tool, not a terminal.

caf
  • 233,326
  • 40
  • 323
  • 462
  • 2
    Good idea. I had been wondering about something like that. This does work, but there are sometimes downsides to not having a terminal - such as not being able to send ctrl-C through the chain (for example to kill something running under the shell of the embedded device obtained from some_tool) or to do character-by-character access (I was surprised to discover I can painfully run vi through my experimental program). Still, this answer has real utility since it's portable, requires nothing custom, and would be enough a lot of the time. – Chris Stratton May 02 '11 at 02:56
  • 3
    This slight modification of your answer seems to do what I want: stty raw -echo ; ( echo "initial command" && cat ) | some_tool ; stty sane – Chris Stratton May 02 '11 at 03:06
  • Very useful answer. Can this be extended to take arguments for multiple prompts in the interactive programme? For example, a programme with three questions, which will always be answered "n", "y", and "7"? – susjoh Dec 01 '13 at 20:16
  • 2
    @susjoh: `(printf "n\ny\n7\n" && cat) | some_tool` – caf Dec 01 '13 at 23:15
  • @ChrisStratton I'm trying your solution, but I can't do anything with the following example: `stty raw -echo; (echo "test" && cat) | less; stty sane` When I enter it in my terminal, I can't kill/stop it in any way. It just hangs, I have to kill it externally. – VasiliNovikov Nov 29 '17 at 10:44
  • @VasyaNovikov the point of `stty raw` is to make the host fully transparent and proxy everything to the target including job control. If you don't want that, use a different `stty` setting or leave it out entirely as in caf's answer itself. But it's not very hard to open another terminal and find and kill the `cat` process. – Chris Stratton Nov 29 '17 at 16:43
  • Good fu! But it suppresses the color sugars provided by my `some_tool`. Now every character is while. – Student Apr 29 '20 at 14:36
7

The accepted answer is simple and mostly good.

But it has a disadvantage: the programs gets a pipe as its input, not a terminal. This means that autocompletion will not work. In a lot of cases, this also disables pretty output, and I've heard some programs just refuse to work if stdin is not a terminal.

The following program solves the problem. It creates a pseudoterminal, spawns a program connected to this pseudoterminal. It first feeds extra input passed via commandline, and then feeds it input given by user via stdin.

For example, ptypipe "import this" python3 makes Python execute "import this" first, and then it drops you to interactive command prompt, with working completion and other stuff.

Likewise, ptypipe "date" bash runs Bash, which executes date and then gives a shell to you. Again, with working completion, colourized prompt and so on.

#!/usr/bin/env python3

import sys
import os
import pty
import tty
import select
import subprocess

STDIN_FILENO = 0
STDOUT_FILENO = 1
STDERR_FILENO = 2

def _writen(fd, data):
    while data:
        n = os.write(fd, data)
        data = data[n:]

def main_loop(master_fd, extra_input):
    fds = [master_fd, STDIN_FILENO]

    _writen(master_fd, extra_input)

    while True:
        rfds, _, _ = select.select(fds, [], [])
        if master_fd in rfds:
            data = os.read(master_fd, 1024)
            if not data:
                fds.remove(master_fd)
            else:
                os.write(STDOUT_FILENO, data)
        if STDIN_FILENO in rfds:
            data = os.read(STDIN_FILENO, 1024)
            if not data:
                fds.remove(STDIN_FILENO)
            else:
                _writen(master_fd, data)

def main():
    extra_input = sys.argv[1]
    interactive_command = sys.argv[2]

    if hasattr(os, "fsencode"):
        # convert them back to bytes
        # http://bugs.python.org/issue8776
        interactive_command = os.fsencode(interactive_command)
        extra_input = os.fsencode(extra_input)

    # add implicit newline
    if extra_input and extra_input[-1] != b'\n':
        extra_input += b'\n'

    # replace LF with CR (shells like CR for some reason)
    extra_input = extra_input.replace(b'\n', b'\r')

    pid, master_fd = pty.fork()

    if pid == 0:
        os.execlp("sh", "/bin/sh", "-c", interactive_command)

    try:
        mode = tty.tcgetattr(STDIN_FILENO)
        tty.setraw(STDIN_FILENO)
        restore = True
    except tty.error:    # This is the same as termios.error
        restore = False

    try:
        main_loop(master_fd, extra_input)
    except OSError:
        if restore:
            tty.tcsetattr(0, tty.TCSAFLUSH, mode)

    os.close(master_fd)
    return os.waitpid(pid, 0)[1]

if __name__ == "__main__":
    main()

(Note: I'm afraid this solution contains a possible deadlock. You may want to feed extra_input in small chunks to avoid it)

user202729
  • 3,358
  • 3
  • 25
  • 36
WGH
  • 3,222
  • 2
  • 29
  • 42
  • Remark: the reason for using CR is explained in [terminal - Understanding Return, Enter, and stty icrlf - Unix & Linux Stack Exchange](https://unix.stackexchange.com/questions/253271/understanding-return-enter-and-stty-icrlf) // also note that it's necessary to manually modify this answer if you want to pass arguments to the child function. // related, see [c - How do *nix pseudo-terminals work ? What's the master/slave channel? - Stack Overflow](https://stackoverflow.com/questions/476354/how-do-nix-pseudo-terminals-work-whats-the-master-slave-channel?noredirect=1&lq=1) – user202729 Feb 20 '23 at 23:25
4

This is easy to do with the program "expect" which is intended to let you write scripts to interact with programs.

I tested this by writing an expect script bc.exp to launch the calculator "bc" and send it the command "obase=16" to put it into hexadecimal output mode, and then turn over control to me.

The script (in a file named bc.exp) is

spawn bc
send "obase=16\n"
interact {
 \003 exit
}

One runs it with

expect bc.exp
1

The answer from @caf is a solid answer.

I thought I would expand on it here with a related option that works well for me.

My <initial command> is actually a list of commands, and found within a text file. Therefore, I want to pipe this into the <some command> and the connect to stdin.

cat handles this very well, it accepts - to read from stdin:

cat init_file - | some_command

This has the same limitation as discussed in cafs answer.

Daniel Brotherston
  • 1,954
  • 4
  • 18
  • 28
0

You can also (in some cases, when run from teminal) use tee to write directly to terminal output. The name tee refers to acting like a T, passing through stdin to stdout while also writing to a file (/dev/tty which is the terminal in this case).

echo "initial command" | tee /dev/tty | some_tool

https://unix.stackexchange.com/a/178754/89546

Real
  • 192
  • 1
  • 8
0

If you are running inside of tmux you can tell it to send the keys.

For example

$ stty -echo; tmux send-keys test; stty echo

will put test into your terminal input. (stty prevents the keys to be seen on the terminal)

Or if you are using vi mode:

stty -echo
tmux send-keys escape 0
stty echo
read -p "rename: " -e -i 'old name' new_name

This will put escape and 0 into the input buffer to place the following readline into command mode and move the cursor to 0.

laktak
  • 57,064
  • 17
  • 134
  • 164
-2

Maybe you could use a here document to pass your input to abd. E. g. like this (using bc to do a simple calculation as example).

[axe@gromp ~]$ bc <<END
> 3 + 4
> END
7

The bc session remains open afterwards, so what is provided between the start and end markers (between "<<END" and "END") will be passed to the command.

Axel Knauf
  • 1,683
  • 1
  • 13
  • 18
  • 1
    This does not seem to have any different result than a pipe, ie the command is not interactive thereafter. I suspect it's just a convenience way of feeding something into a pipe. – Chris Stratton Apr 30 '11 at 19:00
  • Sad to hear. Isn't `adb` itself some kind of script you could look into? And [issuing commands directly](http://developer.android.com/guide/developing/tools/adb.html#issuingcommands) does not keep the tool interactive afterwards, I assume? – Axel Knauf Apr 30 '11 at 19:04
  • adb is a C program for which I have source, but I'd like to find a general solution rather than recompile each tool where it would be interesting to be able to do this. Similarly, it's probably a chokepoint for out-of-band terminal control, so fork&exec'ing it from something that proxies file descriptors might not be too hard, but that's still not a general solution. – Chris Stratton Apr 30 '11 at 19:16