Zsh Plumber - 0

What is it?

all posts in this series

There is a very cool and useful tool out there called Ultimate Plumber. You call it with some-command | up, and then it opens a simple TUI where you write a pipeline at the top of the screen and see results of some-command piped through that pipeline beneath it.

It is such a wonderful tool for working with pipes, but there is one problem I have: it doesn’t use the Zsh line editor.

By rewriting up in Zsh, we let the Zsh users benefit from that shell. They can:

  • have the full pipeline pushed to history when finished editing
  • make use of the shell’s environment, even cding after starting the plumber
  • use completions for the programs they are piping to

Here is the design that I am planning on:

  • z-plumber is activated when the last word in $BUFFER is |.
  • When activated,
    • the command line before the pipe character is executed,(*)
    • piped into the file descriptor $_ZPLUMBER_FD,
    • and that file descriptor is initially redirected into a temp file
  • The ZLE buffer is cleared, and by one mechanism or another the status of the initial command is visible.
    • This will either be running, exited zero, or exited non-zero.
    • This will be updated by utilizing zselect in some fashion.
  • Pressing something which triggers accept-line while editing the new buffer will run the command line with the temp file on stdin without accepting the line.
  • Another widget is bound on (TBD) which does the same thing, and pipes into $=PAGER
  • Another widget is bound on (TBD) which does the following:
    • the redirection to the tempfile is stopped (kill the cat, probably)
    • the buffer is executed with cat $_ZPLUMBER_TMPFILE $_ZPLUMBER_FD on stdin
    • the full pipe involving both buffers is pushed into history(**)

Now there are some asterisks here. Suppose you start with the following buffer:

export BAR=1; cat file1; head file2 |

and then in Z-plumber you run

{ read -r foo; something --with=$foo } | sed 's/_/ /'; read -r x

Doing exactly what I have laid out above would not be equivalent to

export BAR=1
cat file1
head file2 | { read -r foo; something --with=$foo } | sed 's/_/ /'
read -r x

But would actually be equivalent to

< <(export BAR=1; cat file1; head file2) {
	{ read -r foo; something --with=$foo } | sed 's/_/ /'
	read -r x
}

The less significant change is that the while ... done happens in the current shell instead of a subshell like the straightforward pipeline would suggest. This means that $foo persists after the command exits, and $ZSH_SUBSHELL will be non-zero.

The more significant change is that export BAR=1; cat file1 is also passed into the pipeline, and in a subshell. BAR may be needed for something, and file1 is probably not meant to be parsed.

Both of these would be difficult to account for. There may be some hack to get environment out of a subshell, or maybe the plumbing environment is run in a whole new Zsh session. We could attempt to find previous command separators. to ensure not everything is eaten up in the same pipeline. This leads to difficulties regarding complex commands (if...then...fi/for...do...done/{...}/…).

I am inclined to simply exit the plumber with an error if multiple commands not separated by | are found before the trailing pipe. This will reduce problems with environment variables and unintended input.

Additionally, the same thing could be done with the second portion, but a while-read loop is a likely use case for this.

Finally, we could simply run it as above, since it just works for most use-cases. We could then make sure the program pushed to history is representative of what was initially run:

print -sr "< <($_ZPLUMB_HEAD) { $BUFFER }"

instead of the more obvious (but incorrect)

print -sr "{ $_ZPLUMB_HEAD } | { $BUFFER }"

or even more incorrect

print -sr "$_ZPLUMB_HEAD | $BUFFER"

Maybe I could add a setting to switch between these three?

Regardless, this is shaping up to be an interesting project.