This is a reply to James' message. I changed the subject because the content could be useful to a wider audience.
3 points:
- Thanks
- Techniques to define variables in the caller's environment
- Zsh feature requests (Bart, wdyt?)
Thanks
Many thanks to Philippe Altherr for all the ERR_RETURN/ERR_EXIT fixes, and
also, thanks to all maintainers!
Thanks for your thanks 😊 I remember that the whole thing turned out to be much more involved than initially thought. For sure others contributed too, in particular Bart!
Iirc, the fix for "eval" and "source" was just a side outcome of the bigger change. Great to see that it actually helped someone. Also thanks for sharing your use case. It made me realise that I could improve one of my techniques to define variables in the caller's environment, which brings us to my next point.
Techniques to define variables in the caller's environment
Defining variables in the caller's environment is a recurring issue that I face in Zsh. It's not something that is explicitly supported but there are techniques to achieve it. I mainly rely on two.
My first technique makes use of the EXIT trap. When an exit TRAP is configured within a function it triggers when the function exits and runs the trap code in the function's caller environment. The following proof of concept implements the equivalent of local name=value.
function define-var() {
trap "local $1=${(q)2}" EXIT
}
function main () {
define-var var "Hello world!"
echo "var=$var"
}
% main
var=Hello world!
% echo "var=${var:-<no such global variable>}"
var=<no such global variable>
The technique has the advantage that the helper function can define its own local variables, for example to compute the initialization value of the new variable, without any risk to corrupt the caller's environment; only the variables defined in the trap code will be added to the caller's environment. The main disadvantage is that generating the trap code can quickly become much more complicated if more than one variable need to be defined and/or their initialization values aren't simply strings but arrays. Things are even worse if the set of variables to define depends on parameters of the helper function. The use of the EXIT trap also looks, and arguably is, very hackish.
My second technique makes use of the Zsh associative variable functions that gives access to the code of all defined functions.
function define-vars() {
local var1=Hello
local var2=Bye
}
function main () {
eval $functions[define-vars]
echo "var1=$var1 var2=$var2"
}
% main
var1=Hello var2=Bye
% echo "var1=${var1:-<no such global variable>} var2=${var2:-<no such global variable>}"
var1=<no such global variable> var2=<no such global variable>
The technique is particularly useful when many variables need to be defined. In this case all locally defined variables end up in the caller's environment. Therefore the technique isn't ideal if local variables are needed for local computations as they may accidentally corrupt the caller's environment.
If arguments need to be passed to the helper function to compute the names and/or the values of the variables, it's possible to do so but it's a little cumbersome.
function define-vars() {
local var1=$1
local var2=$2
}
function main () {
local args=("$@")
set - Hello Bye; eval $functions[define-vars]
set - "${args[@]}"
echo "var1=$var1 var2=$var2"
}
% main
var1=Hello var2=Bye
If there is no need to save and restore the positional parameters, it's not too bad but the "call" to the helper function looks weird.
The first thing I realized with James' message is that I can achieve the same by relying on a helper script instead of a helper function. Indeed, scripts called with the . and the source builtins run in the caller's environment.
{ echo 'local var1=$1'; echo 'local var2=$2' } > define-vars.zsh
function main () {
. ./define-vars.zsh Hello Bye
echo "var1=$var1 var2=$var2"
}
% main
var1=Hello var2=Bye
% echo "var1=${var1:-<no such global variable>} var2=${var2:-<no such global variable>}"
var1=<no such global variable> var2=<no such global variable>
This has the added advantage that passing arguments is much more natural. However, it has the disadvange that part of the code lives in a separate file.
The second thing I relaize with James' message is that I could combine his and my technique to benefit from the best of both worlds.
function define-vars() {
local var1=$1
local var2=$2
}
alias define-vars='. =(<<<$functions[define-vars])'
function main () {
define-vars Hello Bye
echo "var1=$var1 var2=$var2"
}
% main
var1=Hello var2=Bye
Thanks to the alias, it's almost perfect. I say almost because behind the scene, each call to define-vars triggers the printing of its code and its parsing by the . builtin.
What if instead the helper function could be called as follows?
. define-vars Hello Bye
This is actually possible in ksh and it runs the called function in the caller's environment! Which brings us to my last point.
Zsh feature requests
3 feature requests:
- Dot notation à la ksh to call functions
- Named references that set variables in upper scopes
- Typeset option to set variables in upper scopes
Dot notation à la ksh to call functions
In ksh, the dot notation allows to call functions such that they run in the caller's environment. As demonstrated above, this could also be useful in Zsh. Any reason why this could not be done?
If the invoked entity exists both as a function and as a script in the PATH, then ksh prefers the function. For backward compatibitly Zsh may have to do the opposite.
Named references that set variables in upper scopes
In my first technique, I rely on the EXIT trap to define the variable. With the upcoming next release of Zsh, I could instead almost rely on named references.
function define-var() {
typeset -n -u ref=$1
ref=$2
}
function main () {
define-var var "Hello world!"
echo "var=$var"
}
% main
var=Hello world!
% echo "var=${var:-<no such global variable>}"
var=Hello world!
I say almost because unfortunately assignments to named references to not yet defined variables always define the variable in the global scope. Adding an option to specify that the variable should instead be defined in the enclosing scope (for -u named references) shouldn't be technically difficult. The feature would however break a long standing invariant, namely that no variables can be defined in enclosing scopes. For that reason, it's not something that I would consider adding in the upcoming release but I think it would be worth considering it for the release after it.
Typeset option to set variables in upper scopes
If in a future release named references are enhanced to be able to define variables in upper scopes, then it would make sense to also enhance typeset to be able to do the same. Indeed, in my proof of concept, define-var defines a new variable in the enclosing scope no matter what. In that case, relying on a named reference is a little overkill. Using a plain typeset would be cleaner.
Philippe