Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

Re: Capturing STDOUT without subshells or file I/O



On Sun, Sep 02, 2018 at 04:57:49PM -0400, Ben Klein wrote:
> Currently, I have this:
> 
>   p10k_render_prompt_from_spec p10k_left p10k_opts > $tmpd/prompt
>   read -d $'\0' _P10K_RENDERED_OUTPUT_PROMPT < $tmpd/prompt
>   p10k_render_prompt_from_spec p10k_right p10k_opts right > $tmpd/prompt
>   read -d $'\0' _P10K_RENDERED_OUTPUT_RPROMPT < $tmpd/prompt
> 
> And that involves creating a temporary file (/tmp) which might be
> mounted on a spinning drive, so performance might take a hit.
> 
> The other route using a pipe:
> 
> p10k_render_prompt_from_spec p10k_left p10k_opts | read -d $'\0'
> _P10K_RENDERED_OUTPUT_PROMPT
> 
> Causes environment variables and changes made during the `render_prompt`
> call to be ignored. (Ends up in a subshell...?)
> 
> Is there some way I can use a virtual FD, or perhaps Zsh provides some
> kind of buffer I could use instead?
> 
> In the end, I'm looking for a way to connect STDOUT to STDIN between ZSH
> functions/builtins without any subshells or file I/O. (It's fine if it
> gets buffered until close-of-stream.)

You can sort of use the fact that if you delete the filesystem
name of a temporary file (which would make the hard link count 0),
as long as there still exists open file descriptors referring to
it, you are still able to read/write to it.

Let's make an example function that changes $FOO from 0 to 42, then
prints some output, sort of like your function:

> % FOO=0
> % fun() { FOO=42; print -r - "output of $funcstack[1]()"; }
> % print -r - "FOO=$FOO"
> FOO=0

Now we will use mktemp(1) to create a file in a tmpfs (usually /tmp)
directory, which a swap-backed ramdisk, then redirects an arbitrary
read/write fd to it and deletes the file:

> % tmp=$(mktemp)
> % exec {FD}<>$tmp
> % rm -fv -- $tmp
> removed '/tmp/tmp.sS1G9F6OjR'

The redirected file descriptor is still open however (if you have
colors in your ls you will see it in red because the file no longer
exists), and you can write to it by redirecting stdout:

> % ls -li /proc/$$/fd
> 29987340 lrwx------ 1 jp jp 64 Sep  2 14:36 0 -> /dev/pts/2
> 29986634 lrwx------ 1 jp jp 64 Sep  2 14:36 1 -> /dev/pts/2
> 29986638 lrwx------ 1 jp jp 64 Sep  2 14:36 12 -> /tmp/tmp.dneCPiOqS5
> 29986635 lrwx------ 1 jp jp 64 Sep  2 14:36 2 -> /dev/pts/2
> % fun >&$FD

Reading back your output, however, is a little weirder. The r/w
file descriptor file position is set past your output; reading
from it normally would give us back our output. You also can't
reopen it (file is gone), nor is there a mechanism in zsh to
call something like lseek(2) to rewind file descriptors.

However, what we *can* do it (ab)use the fact that subshells
will rewind the file descriptors since they are duplicated
during fork(). So we can make a subshell, redirect our fd to
stdin and read back what we have written! Like so:

> hobbes% output=$((</dev/stdin)<&$FD)
> hobbes% print -lr - "FOO=$FOO" "output=\"$output\""
> FOO=42
> output="output of fun()"

And of course, it's always good practice to close your
file descriptors in the main shell instance (subshell
file descriptors are implicitly closed when the subshell
exits) when you are finished with them:

> hobbes% exec {FD}>&-

Of course, realize this is kind of a hacky solution, and the better
way is to just have your function either not play with the current
environment (does something like a prompt generation script really
need to change the environment?) or have it set `PROMPT` and `RPROMPT`
itself and avoid worrying stdout at all.

But hey, we all need to have a little fun sometimes :)

-- 
Cheers,
Joey Pabalinas

Attachment: signature.asc
Description: PGP signature



Messages sorted by: Reverse Date, Date, Thread, Author