0

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:

  1. arbitrary variable name(s) that I can pass into the function;
  2. safely, possibly with printf "%q"?

Running a regex with respect to (1) feels rather hackish and I can't get (2) to work at all.

4
  • Does "I have the subshell function running remotely" mean that you're dclaring func() locally but executing it remotely on another machine using something like ssh user@host "bash -c '$(typeset -f func); func x'" or that you're using ssh to call your whole shell script remotely or something else? Commented Jun 3 at 12:32
  • @EdMorton I used to have the whole script over there and have it act as a "library", so the script would take arguments first of which was function name to execute and pass the rest to it. What you mention with -f is something I started doing recently and it's neat, but (I believe) the two make no difference when it comes to returning values - the problem is the same. Commented Jun 3 at 12:51
  • 1
    You may be right, it may not matter, in which case it doesn't matter which version you describe and I'm just trying to make sure I completely understand a specific problem you're trying to solve. If the whole script (say, the_script) is on a remote machine, are you trying to return the values from func() to the code that calls it within that same script on the remote machine or to the script that called ssh user@host the_script on the local machine? Commented Jun 3 at 12:59
  • 1
    @EdMorton The latter case - as for the former (everything on one side), I would just use the regular declare -n. Sometimes I use the function in both scenarios, but the latter requires me to address it as per the question. Typically, it's one and the same script local (caller) and remote (callee). The script either runs fully local or gets (only some) values from remote. So I end up having a function that collects data wherever it is executed, but should reliably return it to the caller. Commented Jun 3 at 13:10

4 Answers 4

4

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.

Sign up to request clarification or add additional context in comments.

1 Comment

I just added it to my answer, there is a difference I learned about yesteray in that the declare approach actually does eval. So it's not only when shell does not support @K, basically the @K output appears to be useless if one does want to avoid eval.
4

You could try:

func2() { declare -n X="$1" declare -A X=() X["p"]="word3" declare -p "$1" } func2 Y declare -A Y=([p]="word3" ) # <- not a command, output from "func2 Y" eval "$(func2 Y)" declare -p Y declare -A Y=([p]="word3" ) # <- not a command, output from "declare -p Y" 

Comments

2

Here is an example. Read inline comments.

file a.sh

#!/usr/bin/env bash #-- call second shell. redirect stdout to &4. catch &3 exec 4>&1 ret=$( ./b.sh 3>&1 1>&4 ); exec 4>- #-- execute returned string eval $ret #-- proof result declare -p x 

file b.sh

#!/usr/bin/env bash #-- your code func() { declare -n var="$1" var["a"]="word1" var["b"]="word2" } declare -A x=() func x #-- test output to stdout and stderr echo stdout echo stderr >&2 #-- return content of 'x' declare -p x >&3 

1 Comment

At first I was gazing at this, but now I got it - you are getting the "returned" payload as well as preserving stdout and stderr. That's a nice to have, but actually not a must for me as of now. Anyhow, I can strip that for my use case. Now the way you call ./b.sh - I would be typically passing it the function name and args, including the x in this case. So that would need me to use eval (when doing the declare) also on the other side I am afraid.
0

After some more tweaking, the generalized approach on @EdMorton take:

aa_deserialize() { declare -n aa_ref="$1" declare -r fn_with_args="${@:2}" declare payload key IFS= read -r -d '' payload < <($fn_with_args) declare -A aa_local="( $payload )" for key in "${!aa_local[@]}"; do aa_ref[$key]=${aa_local[$key]}; done } 

This can be then used as: aa_deserialize Y func

Existing associative array Y will be populated with deserialized returned payload of the function func (which can still take arguments of its own).

Note: The reason the values are first put into local aa_local and only then copied to aa_ref lies in the fact that without explicit declare -A, the variable will be populated as regular array based on the format produced with @K. However, this construct would redeclare the original reference as local variable.

Obviously, a matching aa_serialize could be made and used at the end of each function that is to produce this particular kind of output.

aa_serialize() { declare -n aa_ref="$1" printf '%s\0' "${aa_ref[@]@K}" } 

Alternative approach without @K and that does NOT require populating local associative array variable first AND also does not suffer from eval effect of declare:

aa_serialize() { declare -n aa_ref="$1" for key in "${!aa_ref[@]}"; do printf "%q\0%q\0" "$key" "${aa_ref[$key]}" done } aa_deserialize() { declare -n aa_ref="$1" declare -r fn_with_args="${@:2}" declare key value while IFS= read -r -d '' key; do IFS= read -r -d '' value aa_ref+=([$key]=$value) done < <($fn_with_args) } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.