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
cd
ing 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(**)
- the redirection to the tempfile is stopped (kill the
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.