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?
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With