6

(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.)

2

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.

  • caffeinate zsh -c "$(functions mysleep); mysleep" is an interesting solution. It works, which is a big plus. However, if the function definition is multilined, this could get messy real quick. Ideally I'd like to define the function outside the caffeinate call (and also not in a file as suggested in another answer). For bash, the answer to all of these requirements is export -f func, but I don't know of a zsh equivalent. – Daniel Li Jun 3 '17 at 9:50
  • @DanielLi, there's no problem with multilines here. The main difference between bash's "export -f" is that the function definition is passed in argv[] (in that argument to "-c") instead of envp[] (in a BASH_FUNC_mysleep%% variable now after shellshock). See edit that may clarify in case you misinterpreted the answer in the first place. – Stéphane Chazelas Jun 3 '17 at 20:42
0

The best way to do this is simply put your function in a script file and call the script as such:

caffeinate myfunction.sh

Contents of myfunction.sh:

#!/bin/bash
sleep 2

make sure the script file is executable or it will give you a permission error:

chmod +x myfunction.sh
  • Good suggestion, but for my use case, I'm looking for a non-file-based approach. – Daniel Li Jun 3 '17 at 9:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.