I would like to do equivalent of name referencing from a function run in a subshell, so that arbitrary variable (e.g. associative array) is then available in the parent.
Note on duplicates: I found similar asked 12 years ago (and others referenced within), but the question was fairly limited to a simple variable and fixed variable name - I would like arbitrary and multiple ones, possibly with complex values. Also, the answers are not that applicable focusing on solving the problem differently. I have the subshell function running remotely, so I cannot use those, e.g. I cannot use writing to local files, reverse order piping, etc. Also, I am fine if the solution is Bash-specific.
If it were to be done within the same shell environment, the example would be:
#!/bin/bash
func() {
declare -n var="$1"
var["a"]="word1"
var["b"]="word2"
}
declare -A x=()
func x
for e in "${!x[@]}"; do printf "[%s]=%s\n" "$e" "${x[$e]}" ; done
This works just fine:
[b]=word2
[a]=word1
Now, of course, once func x
is to be executed in a subshell, all is lost.
Instead, I am attempting to serialize the variable with declare -p
:
func2() {
declare -A X=()
X["p"]="word3"
declare -p X
}
This works with a subshell as I can eval
the output, although not sure how safe this approach is:
eval "$( func2 )"
The raw output of func2()
is:
declare -A X=([p]="word3" )
Now within the parent, I have associative array X
available.
However, how do I do this with:
printf "%q"
?Running a regex with respect to (1) feels rather hackish and I can't get (2) to work at all.
Personally I dislike in principle the idea that a function I call would be so tightly coupled to the code I call it from that that function is issuing commands for it's caller to execute. There could be efficiency or other constraints that make that necessary of course.
An option that doesn't use eval
and doesn't require func()
to use an associative array or even be written in shell would be:
$ cat tst.sh
#!/usr/bin/env bash
func() {
declare -A X
X["p"]="word3"
X["m"]='words
$RANDOM
*
4'
printf '%s\0' "${X[@]@K}" # needs bash 5.1 or later for @K
}
declare -A Y
while IFS= read -r -d '' line; do
declare -A Y+="( $line )"
done < <(func)
declare -p Y
$ ./tst.sh
declare -A Y=([p]="word3" [m]=$'words\n $RANDOM\n *\n4' )
Pass the contents of Y
to func()
and add declare -n X
as before if you have a need for func()
to access some initial values from Y[]
.
The benefit of that is it decouples func()
from the calling code so in future you could rewrite func()
as:
func() {
awk -v ORS='\0' 'BEGIN {
print "p", "word3"
print "m", "\047words\n $RANDOM\n *\n4\047"
}'
}
or call a C program or whatever you like without needing to change the code that calls func()
and without func()
needing to output declare
statements, it just has to output key-value pairs. Conversely you could change the calling code to not store func()
s output in an array without having to change func()
.
The loop in the calling code isn't strictly necessary with the way func()
is currently written, we could just have:
IFS= read -r -d '' line < <(func)
declare -A Y="( $line )"
because @K
will produce a single line of quoted/escaped fields, but we're using the loop anyway to accommodate other versions of func()
that might print multiple NUL-terminated lines to avoid unnecessary coupling of func()
to the calling code.
Regarding @K
in the first version of func()
above, see https://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion:
K Produces a possibly-quoted version of the value of parameter, except that it prints the values of indexed and associative arrays as a sequence of quoted key-value pairs (see Arrays).
If your shell doesn't support @K
you can always change printf '%s\0' "${X[@]@K}"
to:
for idx in "${!X[@]}"; do
printf '%q %q\0' "$idx" "${X[$idx]}"
done
See also How do I populate a bash associative array with command output? for related information.