Process `zsh` function with util that only recognizes commands

zsh

(This question uses the Mac caffeinate tool as an example, but the concept applies to all tools (i.e. xargs) that accept a utility as argument.)

Mac's caffeinate tool accepts the name of a utility: caffeinate sleep 1, for instance, (where sleep is the utility). Is there any way for it to accept a zsh function as well, without modifying the tool itself? For example:

function mysleep {
  sleep 2
}

caffeinate mysleep    # mysleep: No such file or directory

Edit: This question is indeed a duplicate for Bash—thanks for pointing me to the answer. For zsh, however, export -f doesn't work. Is there a way to do this in zsh? (I'm removing the bash tag to reduce confusion.)

Best Answer

caffeinate expects to execute a command in a new process.

To interpret a zsh function, you need a zsh command.

And you'd need to pass the definition of that function (as well as other functions it may need) to it, for instance with:

mysleep() {
  sleep 2
}
caffeinate zsh -c "$(functions mysleep);mysleep"

functions mysleep dumps the definition of the mysleep function which we pass to that new zsh for interpretation before calling the function, so that zsh invoked by caffeinate ends up interpreting:

mysleep() {
  sleep 2
};mysleep

If we compare to bash's:

mysleep() {
  sleep 2
}
export -f mysleep
caffeinate bash -c "mysleep"

(which is 2 characters shorter to type), bash will do:

execve("/path/to/caffeinate",
  ["caffeinate", "bash", "-c", "mysleep"],
  ["BASH_FUNC_mysleep%%=() {  sleep 2\n}", rest-of-environment])

While with zsh, we get:

execve("/path/to/caffeinate",
  ["caffeinate", "zsh", "-c", "mysleep () {\n\tsleep 2\n};mysleep"],
  [rest-of-environment])

I see several advantages of that latter approach:

  • we have full control: we know how we pass the function definition, how it is being used. There's less scope for nasty things like the type of shellshock here.
  • as the name of the bash environment variable that carries the function definition contains % characters (and even if it didn't, think of sudo for instance), we're not guaranteed guaranteed that caffeinate will propagate it to the bash command it runs.
  • if it is propagate, because the function definition is stored in envp[] instead of argv[], that means it pollutes the environment of every other command executed in that environment (including sleep for instance in this example).
  • (minor) even though the bash shell code is shorter, that's more data passed to execve() so contributes more toward the E2BIG limit of that system call.

If you wanted to use the environment, you could still do:

FUNCS=$(functions mysleep) caffeinate zsh -c '
  eval "$FUNCS";mysleep'

In the case of caffeinate here, that is where we only need for caffeinate to run while the function is running, not necessarily for it to run the function, we can use other approaches like:

mysleep | caffeinate cat

cat will run as long as mysleep runs. mysleep would still run in a separate process and that affects the stdout of mysleep though.

mysleep 3> {fd}>(caffeinate cat)

would solve both problems.

As above, that creates a pipe between mysleep and cat. But the writing end of the pipe is now on a newly allocate file descriptor above 10 (stored in $fd) that mysleep will typically not write to. cat will therefore read nothing but wait until end-of-file on the pipe which will only happen when mysleep (and all the children processes that inherit that fd) terminates.