1

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.

2
  • 2
    Have you looked into the ncurses library? That might be more applicable for the larger project you're attempting. Alternatively, you may want to look into non-shell-based solutions to your overall project. This question sounds strongly like the XY problem.
    – Wildcard
    Commented Mar 23 at 0:35
  • 1
    @Wildcard, I can't see how using ncurses would help (and btw zsh does have a ncurses module). ncurses can help you determine the escape sequences needed to control given terminal in any specific way (such as moving the cursor to the home position) but here the OP rather wants to obtain the textual result of what that given terminal does with those escape sequences which is almost the opposite. Commented Mar 23 at 14:44

3 Answers 3

2

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.

7
  • Can you believe this was my first and foremost approach, and I swear I did EXACTLY the same as you but I got the results as if the input was the terminal prompt in the shape of commands, not the screen itself (I assumed it was by design). Now I could not believe it so I am repeating exactly same experiment as you and it works flawlessly so obviously I did something wrong
    – Whimusical
    Commented Mar 24 at 20:36
  • OK, I reproduced my previous error with the exact lines I did my test. MY_PROMPT%> tmux new-session -d -s aux -PF "#{pane_tty}" /dev/ttys017 MY_PROMPT%> echo -n foo > /dev/ttys017 && tput cr > /dev/ttys017 && echo bar > /dev/ttys017 MY_PROMPT%> tmux capture-pane -t "aux" -p -S0 -E3 and the result was the incorrectly undeterministicly ordered sequence foo MY_PROMPT %> bar
    – Whimusical
    Commented Mar 24 at 20:41
  • I opened a new question to understand why your tail command fixes my approach unix.stackexchange.com/questions/773056/…
    – Whimusical
    Commented Mar 24 at 21:33
  • 1
    @Whimusical This I do not know. Commented Mar 25 at 12:13
  • 1
    @Whimusical, I also wondered about that and tested sending expensive escape sequences such as switching back and forth between normal and alternate screen and couldn't fault it. In any case, there would be the same question with my approach where it's cat within the tmux session that writes to the tty, but that's essentially the same. Commented Mar 25 at 16:03
1

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

2
  • The OP is using zsh so quoting is not strictly necessary there as zsh doesn't have the same kind of bugs as bash or other Bourne-like shells have when variables are left unquoted. The fact that the $fifo variable is expanded in the shell code or the missing --s are potential problems though (independent of which shell is being used) Commented Mar 23 at 10:46
  • Thanks! But one of my requirements is keeping the logic within the script, so I must redirect the script stdout to tmux, I cannot execute the commands in the other pane
    – Whimusical
    Commented Mar 23 at 13:36
1

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

10
  • 1.Good point. 3.You mean im passing the fifo content instead of the fifo path there?. 6.Sorry I dont understand. 8.I thought it was implicit, but havent played enough yet with all conercases. Ill check. I am aware of 1,4,7,9 and unaware of 5 but not relevant
    – Whimusical
    Commented Mar 23 at 13:46
  • 1
    3. No, I mean removing the code injection issue by passing the path via an environment variable (like Marcus did in his approach though here using -E instead to pass the env along. 6, I'll edit to clarify later. 8, tmux creates it as default-size which by default is 80x24. Commented Mar 23 at 13:50
  • 1
    About 6, -E is for tmux not to clear the environment so $IN, $OUT for instance are available to the shell that it starts to interpret that command line. tmux will set the $TERM env vars to the correct value in any case so applications running under it know they are talking to a tmux terminal emulator and not xterm/gnome-terminal/linux or whatever other terminal emulators which have different feature sets and understand different escape sequences. Commented Mar 23 at 22:07
  • 1
    I am really sorry because your answer is by large the most useful I had and the one that enlightned me the most, and the most dedicated, but being honest, @Kamil Maciorowski's one deserves the check if it ends up working because it really addresses like noone the original purpose of the question. I'd hope I could do a 2-answer check
    – Whimusical
    Commented Mar 24 at 21:20
  • 1
    @Whimusical, no worries, Kamil's is the better approach here, though you may want to address points 1, 5, 7, 8, 9 there as well. Commented Mar 25 at 7:16

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .