Redirecting stdout of a script as the stdin of another script/command running in a tmux session

I am adapting Marcus Müller’s answer
to a question I asked last week —
a script that redirects its stdout to a tmux session
in order to render ANSI escape sequences,
and then captures the pane render as the real output of the script. 
I know it is not useful at all, since you can print it directly to stdout and have the same outcome, but it’s just a demo to play with, to bring the code to a bigger project more complex to explain, where I need this feature:

#!/bin/zsh

tmpdir="$(mktemp -d)"
fifo="${tmpdir}/fifo"
mkfifo "$fifo"
tmux new-session -d -s aux "while true; do cat ${fifo}; done"

exec 3>&1 1>"$fifo"


echo foo
echo bar
tput home
echo -n b

exec 1>&3 3>&-

tmux capture-pane -t "aux" -p -S0 -E1
tmux kill-session -t aux
rm -rf $tmpdir

which outputs (and must):

boo
bar

I am interested in simplifying the code. Is it possible to use any trick that plays with stdin redirection instead of a fifo that needs to be maintained? Can I use a one-liner somehow that keeps printing everything and closes the session automatically when it’s done?

I tried and played with tmux pipe-pane, buffer, send-keys and run-shell and I could not manage to make it. Especially when the session takes stdin as if you wrote on the console a command, not like the stdin of the script that a command/script that is running

I feel like it must be simplifiable somehow.

Asked By: Whimusical

||

I’m not clear exactly what you’re asking. 
Are you accepting the concept of using tmux to process the escape sequences,
but you just want to eliminate the FIFO?

Try just producing the output you want in tmux:

tmux new-session -d -s aux "echo foo; echo bar; tput home; echo -n b"

tmux capture-pane -t aux -p -S0 -E1
tmux kill-session -t aux

and leave off the FIFO stuff.

P.S. You should quote shell variables, like $fifo and $tmpdir
You don’t really need to quote constant strings made up
of non-whitespace characters, like "aux".

I don’t think you can do anything like that as tmux will close all fds above 2 it received (and 0, 1, 2 as well when detached).

See close_range(3, 4294967295, 0) or equivalent in strace (or equivalent) output or the closefrom(STDERR_FILENO + 1) calls in the source.

So you’ll need to pass the data some other way, either via a named pipe like you do or environment variable (but can’t contain NUL characters) or embedded in the shell code run by tmux or a socket, temporary file, shared memory, none of which is really going to be simpler or more reliable.

There are a number of issues in your approach though:

  • you’re using a fixed session name which means the script cannot be used reliably unless you can guarantee no two invocations of it are run at the same time. You could let tmux pick the session name and retrieve it via the -P option of new-session.
  • you don’t do any synchronisation: when you run capture-pane, there’s no guarantee that cat will have finished or even started. You’re doing that loop+kill instead of telling the tmux session to exit when you’ve retrieved the pane’s contents.
  • you’re embedding the contents of $fifo inside the shell code run in the new session. Better to pass as an environment variable.
  • you’re not checking the exit status of mktemp or mkfifo.
  • tmux will read the user’s ~/.tmux.conf which may interfere with the processing
  • you’ll want to pass TERM=tmux to the environment of tput (or zsh’s builtin echoti equivalent) as that’s a tmux terminal emulator that will end-up interpreting rather than the host terminal you run the script in, so you need to send it the escape sequences that it rather than the host terminal will understand¹.
  • with -S0 -E1, you’re only capturing the first 2 visible rows of the pane
  • you could tell tmux to create a pane the same size as that of the host terminal window
  • you may want to get the contents of the scrollback buffer as well in case the output doesn’t fit in one screen.

So, maybe something like:

#! /bin/zsh -

tmpdir=$(mktemp -d) || exit
trap 'rm -rf -- $tmpdir' EXIT INT TERM HUP QUIT

in=$tmpdir/in out=$tmpdir/out
mkfifo -- $in $out || exit
session=$(
  IN=$in OUT=$out tmux -f /dev/null new-session -PEd -x ${COLUMNS:-80} -y ${LINES:-24} '
    cat -- "$IN"
    echo done > "$OUT"
    read may_I_exit < "$IN"
    '
) || exit

cat "$@" > $in || exit

read can_I_retrieve_the_output < $out || exit
tmux capture-pane -t $session -pS-
echo you may exit > $in

Then for instance:

$ (TERM=tmux; print -rln foo bar$terminfo[home]b) | ./capture | hexdump -C
00000000  62 6f 6f 0a 62 61 72 0a  0a 0a 0a 0a 0a 0a 0a 0a  |boo.bar.........|
00000010  0a 0a 0a 0a 0a 0a 0a 0a  0a 0a 0a 0a 0a 0a 0a 0a  |................|
*
00000040  0a 0a                                             |..|
00000042

(note all the 0x0a (LF) bytes as my terminal window is 60 rows high).

(disclaimer: I’m not a tmux user myself, so there might very well be smarter or more correct ways to do it there)


¹ Though in the case of the home capability, it’s unlikely to make a difference in practice as it’s unlikely that you’d be using a terminal for which the home escape sequence is different from that of tmux‘ own emulation (e[H).

Answered By: Stéphane Chazelas

Is it possible to use any trick that plays with stdin redirection instead of a fifo that needs to be maintained?

Ask tmux what tty is assigned to the only pane of the only window of the new session. Then print to it.

#!/bin/zsh

tmux new-session -d -s aux 'tail -f /dev/null' || exit 1
tty="$(tmux display-message -p -t aux -F '#{pane_tty}')"

{
  echo foo
  echo bar
  TERM=tmux tput home
  echo -n b
} > "$tty"

tmux capture-pane -t aux -p -S0 -E1
tmux kill-session -t aux

Getting information from tmux display-message -p is quite useful in general. In your case you can get information about the tty directly from tmux new-session; and you don’t need the tty variable. You can set up the redirection without preliminary steps, like this:

{...} >"$(tmux new-session -d -s aux -P -F '#{pane_tty}' 'tail -f /dev/null')" || exit

Note tail -f /dev/null is just a command to keep the tmux pane "alive". The script does not try to send anything to this command, it prints to the right tty instead. This answer does not solve the title ("Redirecting stdout of a script as the stdin of another script/command running in a tmux session"), but it gives you the functionality you wanted.

Answered By: Kamil Maciorowski
Categories: Answers Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.