exupero's blog
RSSApps

Get last command output from Tmux

Working in Tmux, I occasionally want the output of a command written to a file. To do that, I've prefixed my shell prompt's lines with the uncommon character § and written a script that uses Tmux's capture-pane command to get the last output of a command from a particular pane:

target=$1

tmux capture-pane -pJ -t "$target" -S - -E - \
  | tail -r \
  | awk 'start && /^§/ {exit} /^[^§]/ {start=1} start {print}' \
  | tail -r

The -p argument prints the pane contents to stdout, -J joins lines broken by wrapping in the terminal window, and -S - -E - gets the entire pane's content, not just the contents that are currently visible in the pane.

To get the last command's output, I reverse the order of the lines with tail -r. That command works on my system, though it's not POSIX-compliant, so you might need to consider other options. The ones mentioned in that StackOverflow question, however, all reportedly require holding the entirety of stdin in memory, so if you're trying to capture the contents of an extremely large pane, be careful.

The awk script prints the lines between my shell prompts, using § as its landmarks. The order of clauses in the script makes it a little confusing, but it drops lines beginning with § until it finds a line that doesn't start with §, prints the lines that don't begin with §, and when it finds another line beginning with § it exits. An equivalent Clojure expression would be:

(let [prompt-line? #(re-find #"^§" %)]
  (->> lines
       (drop-while prompt-line?)
       (take-while (complement prompt-line?))))

Finally, the lines are reversed again, to put them back in their original order.

In practice this method has worked well for me, but because tail -r (and other stdin-reversing programs, such as tac) require holding all lines in memory, you might prefer a more memory efficient, if more complex, approach:

target=$1

tmux capture-pane -pJ -t "$target" -S - -E - | awk '
  /^§/ {reset=1}
  reset && /^[^§]/ {delete a; i=0; reset=0}
  /^[^§]/ {a[i++]=$0}
  END {for (j=0; j<i;) print a[j++]}
'

The above awk script stores each line of command output in an array, but whenever it encounters a prompt's § it clears the array and starts over. At the end of input, the contents of the array are the lines we want.

This version may be more memory efficient, but a quick check with the time command on about 3,500 lines of pane history suggests it's about half the speed of the original version. That makes sense since tail -r and tac can be implemented as compiled, highly efficient binaries, while the above script is interpreted by awk.