Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to define a function dynamically in ZSH?

I would like to define a series of functions dynamically in ZSH.

For example:

#!/bin/zsh
for action in status start stop restart; do
     $action() {
         systemctl $action $*
     }
done

However, this results in four identical functions which all call the final argument:

$ status libvirtd
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ====
Authentication is required to restart 'libvirtd.service'.
...

Is there any way to define these functions dynamically like this?

like image 529
D. Scott Boggs Avatar asked Dec 17 '22 16:12

D. Scott Boggs


2 Answers

Yes, it's actually very easy:

for action in status start stop restart
do
    $action() {
        systemctl $0 "$@"
    }
done

The key point here is the use of $0. The problem with your original solution was that the "$action" inside the function's definition was not expanded during the definition, so in all four functions it just referred to the last value of this variable. So instead of trying to get it to work with ugly trickery using eval (as suggested in another solution), the nicest solution is just to use $0... In shell script, $0 expands to the name of the current script, and in shell functions, it expends to the name of the current function. Which happens to be exactly what you wanted here!

Note also how I used "$@" (the quotes are important) instead of $*. This works correctly with quoted arguments with whitespace, which $* ruins.

Finally, for this use case you could have used "alias" instead of function, and everything would have been much simpler:

for action in status start stop restart
do
    alias $action="systemctl $action"
done
like image 77
Nadav Har'El Avatar answered Jan 05 '23 17:01

Nadav Har'El


It's possible, but ugly:

for action in status start stop restart; do
    eval "$action() { systemctl $action \"\$@\"; }"
done

As with anything involving eval, this is tricky to get right. The thing eval does is parse the command twice, and execute it on the second parse. "Huh?" I hear you say? Well, the thing is that normally $variable references in a function definition don't get expanded immediately, but when the function is executed. So when your loop runs this (with action set to "status"):

$action() {
    systemctl $action $*
done

It expands the first reference to $action but not the second, giving this:

status() {
    systemctl $action $*
done

Instead, you want both references to $action expanded immediately. But you don't want the reference to $* expanded immediately, because then it'd use the arguments to your script, not the arguments given to the function at runtime. And actually, you don't want $* at all, because it mangles the arguments under some circumstances; use "$@" instead.

So you need a way to get some variable/parameter references expanded immediately, and defer some until later. eval gives you that. The big tricky thing is that you can need two levels of quoting/escaping (one for the first parse pass, one for the second), and you need to use those levels to control which variable/parameter references expand immediately, and which later.

When this runs (with action set to "status"):

eval "$action() { systemctl $action \"\$@\"; }"

...it does a parsing pass, expanding the unescaped variable references and removing a level of quoting & escaping, giving this:

status() { systemctl status "$@"; }

...which is what you wanted.

like image 43
Gordon Davisson Avatar answered Jan 05 '23 15:01

Gordon Davisson