176

I'm working with this:

GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

I have a script like below:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

test1 
echo "$e"

Which returns:

hello
4

But if I assign the result of the function to a variable, the global variable e is not modified:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

ret=$(test1)

echo "$ret"
echo "$e"

Returns:

hello
2

I've heard of the use of eval in this case, so I did this in test1:

eval 'e=4'

But the same result.

Could you explain me why it is not modified? How could I save the echo of the test1 function in ret and modify the global variable too?

1
  • Do you need to return hello ? You could just echo $e for it to return. Or echo everything you want and then parse the result ? Commented May 9, 2014 at 13:06

14 Answers 14

164

When you use a command substitution (i.e., the $(...) construct), you are creating a subshell. Subshells inherit variables from their parent shells, but this only works one way: A subshell cannot modify the environment of its parent shell.

Your variable e is set within a subshell, but not the parent shell. There are two ways to pass values from a subshell to its parent. First, you can output something to stdout, then capture it with a command substitution:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"

The above outputs:

Hello

For a numerical value in the range of 0 through 255, you can use return to pass the number as the exit status:

mysecondfunc() {
    echo "Hello"
    return 4
}

var="$(mysecondfunc)"
num_var=$?

echo "$var - num is $num_var"

This outputs:

Hello - num is 4
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for the point, but I have to return an string array, and within the function I have to add elements to two global string arrays.
You realise that if you just run the function without assigning it to a variable all the global variables within it will update. Instead of returning a string array, why not just update the string array in the function then assign it to another variable after the function has finished ?
@JohnDoe: You can't return a "string array" from a function. All you can do is print a string. However, you can do something like this: setarray() { declare -ag "$1=(a b c)"; }
71

This needs bash 4.1 if you use {fd} or local -n.

The rest should work in bash 3.x I hope. I am not completely sure due to printf %q - this might be a bash 4 feature.

Summary

Your example can be modified as follows to archive the desired effect:

# Add following 4 lines:
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }

e=2

# Add following line, called "Annotation"
function test1_() { passback e; }
function test1() {
  e=4
  echo "hello"
}

# Change following line to:
capture ret test1 

echo "$ret"
echo "$e"

prints as desired:

hello
4

Note that this solution:

  • Works for e=1000, too.
  • Preserves $? if you need $?

The only bad sideffects are:

  • It needs a modern bash.
  • It forks quite more often.
  • It needs the annotation (named after your function, with an added _)
  • It sacrifices file descriptor 3.
    • You can change it to another FD if you need that.
      • In _capture just replace all occurances of 3 with another (higher) number.

The following (which is quite long, sorry for that) hopefully explains, how to adpot this recipe to other scripts, too.

The problem

d() { let x++; date +%Y%m%d-%H%M%S; }

x=0
d1=$(d)
d2=$(d)
d3=$(d)
d4=$(d)
echo $x $d1 $d2 $d3 $d4

outputs

0 20171129-123521 20171129-123521 20171129-123521 20171129-123521

while the wanted output is

4 20171129-123521 20171129-123521 20171129-123521 20171129-123521

The cause of the problem

Shell variables (or generally speaking, the environment) is passed from parental processes to child processes, but not vice versa.

If you do output capturing, this usually is run in a subshell, so passing back variables is difficult.

Some even tell you, that it is impossible to fix. This is wrong, but it is a long known difficult to solve problem.

There are several ways on how to solve it best, this depends on your needs.

Here is a step by step guide on how to do it.

Passing back variables into the parental shell

There is a way to pass back variables to a parental shell. However this is a dangerous path, because this uses eval. If done improperly, you risk many evil things. But if done properly, this is perfectly safe, provided that there is no bug in bash.

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }

d() { let x++; d=$(date +%Y%m%d-%H%M%S); _passback x d; }

x=0
eval `d`
d1=$d
eval `d`
d2=$d
eval `d`
d3=$d
eval `d`
d4=$d
echo $x $d1 $d2 $d3 $d4

prints

4 20171129-124945 20171129-124945 20171129-124945 20171129-124945

Note that this works for dangerous things, too:

danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"

prints

; /bin/echo *

This is due to printf '%q', which quotes everything such, that you can re-use it in a shell context safely.

But this is a pain in the a..

This does not only look ugly, it also is much to type, so it is error prone. Just one single mistake and you are doomed, right?

Well, we are at shell level, so you can improve it. Just think about an interface you want to see, and then you can implement it.

Augment, how the shell processes things

Let's go a step back and think about some API which allows us to easily express, what we want to do.

Well, what do we want do do with the d() function?

We want to capture the output into a variable. OK, then let's implement an API for exactly this:

# This needs a modern bash 4.3 (see "help declare" if "-n" is present,
# we get rid of it below anyway).
: capture VARIABLE command args..
capture()
{
local -n output="$1"
shift
output="$("$@")"
}

Now, instead of writing

d1=$(d)

we can write

capture d1 d

Well, this looks like we haven't changed much, as, again, the variables are not passed back from d into the parent shell, and we need to type a bit more.

However now we can throw the full power of the shell at it, as it is nicely wrapped in a function.

Think about an easy to reuse interface

A second thing is, that we want to be DRY (Don't Repeat Yourself). So we definitively do not want to type something like

x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4

The x here is not only redundant, it's error prone to always repeate in the correct context. What if you use it 1000 times in a script and then add a variable? You definitively do not want to alter all the 1000 locations where a call to d is involved.

So leave the x away, so we can write:

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }

d() { let x++; output=$(date +%Y%m%d-%H%M%S); _passback output x; }

xcapture() { local -n output="$1"; eval "$("${@:2}")"; }

x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4

outputs

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414

This already looks very good. (But there still is the local -n which does not work in oder common bash 3.x)

Avoid changing d()

The last solution has some big flaws:

  • d() needs to be altered
  • It needs to use some internal details of xcapture to pass the output.
    • Note that this shadows (burns) one variable named output, so we can never pass this one back.
  • It needs to cooperate with _passback

Can we get rid of this, too?

Of course, we can! We are in a shell, so there is everything we need to get this done.

If you look a bit closer to the call to eval you can see, that we have 100% control at this location. "Inside" the eval we are in a subshell, so we can do everything we want without fear of doing something bad to the parental shell.

Yeah, nice, so let's add another wrapper, now directly inside the eval:

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
# !DO NOT USE!
_xcapture() { "${@:2}" > >(printf "%q=%q;" "$1" "$(cat)"); _passback x; }  # !DO NOT USE!
# !DO NOT USE!
xcapture() { eval "$(_xcapture "$@")"; }

d() { let x++; date +%Y%m%d-%H%M%S; }

x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4

prints

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414                                                    

However, this, again, has some major drawback:

  • The !DO NOT USE! markers are there, because there is a very bad race condition in this, which you cannot see easily:
    • The >(printf ..) is a background job. So it might still execute while the _passback x is running.
    • You can see this yourself if you add a sleep 1; before printf or _passback. _xcapture a d; echo then outputs x or a first, respectively.
  • The _passback x should not be part of _xcapture, because this makes it difficult to reuse that recipe.
  • Also we have some unneded fork here (the $(cat)), but as this solution is !DO NOT USE! I took the shortest route.

However, this shows, that we can do it, without modification to d() (and without local -n)!

Please note that we not neccessarily need _xcapture at all, as we could have written everyting right in the eval.

However doing this usually isn't very readable. And if you come back to your script in a few years, you probably want to be able to read it again without much trouble.

Fix the race

Now let's fix the race condition.

The trick could be to wait until printf has closed it's STDOUT, and then output x.

There are many ways to archive this:

  • You cannot use shell pipes, because pipes run in different processes.
  • One can use temporary files,
  • or something like a lock file or a fifo. This allows to wait for the lock or fifo,
  • or different channels, to output the information, and then assemble the output in some correct sequence.

Following the last path could look like (note that it does the printf last because this works better here):

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }

_xcapture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; _passback x >&3)"; } 3>&1; }

xcapture() { eval "$(_xcapture "$@")"; }

d() { let x++; date +%Y%m%d-%H%M%S; }

x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4

outputs

4 20171129-144845 20171129-144845 20171129-144845 20171129-144845

Why is this correct?

  • _passback x directly talks to STDOUT.
  • However, as STDOUT needs to be captured in the inner command, we first "save" it into FD3 (you can use others, of course) with '3>&1' and then reuse it with >&3.
  • The $("${@:2}" 3<&-; _passback x >&3) finishes after the _passback, when the subshell closes STDOUT.
  • So the printf cannot happen before the _passback, regardless how long _passback takes.
  • Note that the printf command is not executed before the complete commandline is assembled, so we cannot see artefacts from printf, independently how printf is implemented.

Hence first _passback executes, then the printf.

This resolves the race, sacrificing one fixed file descriptor 3. You can, of course, choose another file descriptor in the case, that FD3 is not free in your shellscript.

Please also note the 3<&- which protects FD3 to be passed to the function.

Make it more generic

_capture contains parts, which belong to d(), which is bad, from a reusability perspective. How to solve this?

Well, do it the desparate way by introducing one more thing, an additional function, which must return the right things, which is named after the original function with _ attached.

This function is called after the real function, and can augment things. This way, this can be read as some annotation, so it is very readable:

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_capture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; "$2_" >&3)"; } 3>&1; }
capture() { eval "$(_capture "$@")"; }

d_() { _passback x; }
d() { let x++; date +%Y%m%d-%H%M%S; }

x=0
capture d1 d
capture d2 d
capture d3 d
capture d4 d
echo $x $d1 $d2 $d3 $d4

still prints

4 20171129-151954 20171129-151954 20171129-151954 20171129-151954

Allow access to the return-code

There is only on bit missing:

v=$(fn) sets $? to what fn returned. So you probably want this, too. It needs some bigger tweaking, though:

# This is all the interface you need.
# Remember, that this burns FD=3!
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }

# Here is your function, annotated with which sideffects it has.
fails_() { passback x y; }
fails() { x=$1; y=69; echo FAIL; return 23; }

# And now the code which uses it all
x=0
y=0
capture wtf fails 42
echo $? $x $y $wtf

prints

23 42 69 FAIL

There is still a lot room for improvement

  • _passback() can be elmininated with passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }

  • _capture() can be eliminated with capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }

  • The solution pollutes a file descriptor (here 3) by using it internally. You need to keep that in mind if you happen to pass FDs.
    Note thatbash 4.1 and above has {fd} to use some unused FD.
    (Perhaps I will add a solution here when I come around.)
    Note that this is why I use to put it in separate functions like _capture, because stuffing this all into one line is possible, but makes it increasingly harder to read and understand

  • Perhaps you want to capture STDERR of the called function, too. Or you want to even pass in and out more than one filedescriptor from and to variables.
    I have no solution yet, however here is a way to catch more than one FD, so we can probably pass back the variables this way, too.

Also do not forget:

This must call a shell function, not an external command.

There is no easy way to pass environment variables out of external commands. (With LD_PRELOAD= it should be possible, though!) But this then is something completely different.

Last words

This is not the only possible solution. It is one example to a solution.

As always you have many ways to express things in the shell. So feel free to improve and find something better.

The solution presented here is quite far from being perfect:

  • It was nearly not tested at all, so please forgive typos.
  • There is a lot of room for improvement, see above.
  • It uses many features from modern bash, so probably is hard to port to other shells.
  • And there might be some quirks I haven't thought about.

However I think it is quite easy to use:

  • Add just 4 lines of "library".
  • Add just 1 line of "annotation" for your shell function.
  • Sacrifices just one file descriptor temporarily.
  • And each step should be easy to understand even years later.

1 Comment

never in my life I have seen such an extensive reply taken from so many angles. I bow to you @Tino
19

Maybe you can use a file, write to file inside function, read from file after it. I have changed e to an array. In this example blanks are used as separator when reading back the array.

#!/bin/bash

declare -a e
e[0]="first"
e[1]="secondddd"

function test1 () {
 e[2]="third"
 e[1]="second"
 echo "${e[@]}" > /tmp/tempout
 echo hi
}

ret=$(test1)

echo "$ret"

read -r -a e < /tmp/tempout
echo "${e[@]}"
echo "${e[0]}"
echo "${e[1]}"
echo "${e[2]}"

Output:

hi
first second third
first
second
third

1 Comment

Wow, a lot of info. I get: passback: command not found . :(
16

I had a similar problem when I wanted to remove temporary files I had created automatically. The solution I came up with was not to use command substitution, but rather to pass the name of the variable, that should take the final result, into the function. E.g.

#!/usr/bin/env bash

# array that keeps track of tmp-files
remove_later=()

# function that manages tmp-files
new_tmp_file() {
  file=$(mktemp)
  remove_later+=( "$file" )
  # assign value (safe form of `eval "$1=$file"`)
  printf -v "$1" -- "$file"
}

# function to remove all tmp-files
remove_tmp_files() { rm -- "${remove_later[@]}"; }

# define trap to remove all tmp-files upon EXIT
trap remove_tmp_files EXIT

# generate tmp-files
new_tmp_file tmpfile1
new_tmp_file tmpfile2

So, adapting this to the OP, it would be:

#!/usr/bin/env bash
    
e=2
    
function test1() {
  e=4
  printf -v "$1" -- "hello"
}
    
test1 ret
    
echo "$ret"
echo "$e"

Works and has no restrictions on the "return value".

3 Comments

This is an underappreciated solution.
A couple of modifications I would implement: (1) avoid the usage of eval and use printf -v "$1" -- "%s" "$file" instead. (2) define remove_later to be an array declare -a remove_later and use it accordingly.
Thank you very much @kvantour ! I fully agree with your proposals, especially the array solution, which is much cleaner. Do you want to make the changes? I don't get around to it currently...
15

What you are doing, you are executing test1

$(test1)

in a sub-shell( child shell ) and Child shells cannot modify anything in parent.

You can find it in bash manual

Please Check: Things results in a subshell here

Comments

8

Assuming that local -n is available, the following script lets the function test1 modify a given global variable:

#!/bin/bash

e=2

function test1() {
  local -n var=$1
  var=4
  echo "hello"
}

test1 e
echo "$e"

Which gives the following output:

hello
4

Note (update): This answer demonstrates modifying a global variable in a function without hard-coding the name of the variable in the function. As covered well by other answers, the specific issue of the OP arises from running the function in a subshell. This is not addressed by this answer.

5 Comments

Why would using a named reference be necessary for the assigning of a global var from inside a function? A simple e=4 would suffice. The problem arises, when the assignment is done from another subshell, where your example doesn't work either, as the following shows: e=2; ret=$(test1 e); echo "$ret"; echo $e
@fozzybear The construct in this answer lets a function modify a given variable without its name being hard-coded in the function. It is often useful, which is why I wrote this as a supplement to other answers. It is not useful in the case you describe, but the OP does not specifically target that problem.
Understood. Perhaps then you should mention in your post, that your suggestion specifically acts as a workaround for hard-coded var names, not for altering global vars from a subshell, as that obviously was the OP's real problem, because of ret=$(test1).
@fozzybear I have tried to clarify in a note. It is a while ago, but I think I wrote this answer because that was what I came looking for under the headline of this question.
If nothing else, this is an interesting bit of code. Bravo.
2

I'm not sure if this works on your terminal, but I found out that if you don't provide any outputs whatsoever it gets naturally treated as a void function, and can make global variable changes. Here's the code I used:

let ran1=$(( (1<<63)-1)/3 ))
let ran2=$(( (1<<63)-1)/5 ))
let c=0
function randomize {
    c=$(( ran1+ran2 ))
    ran2=$ran1
    ran1=$c
    c=$(( c > 0 ))
}

It's a simple randomizer for games that effectively modifies the needed variables.

1 Comment

Didn't try, but inline var assignement in a subshell was the OP's original problem, to capture the output of a function. Running a void function in this context makes no sense. So to me, there's no real difference here, to simply directly calling randomize(). Your approach drastically reduces the re-usability of the function, because it doesn't use generic var references and the function can only run in the very contexts of the (hardcoded in the function) vars to modify.
1

It's because command substitution is performed in a subshell, so while the subshell inherits the variables, changes to them are lost when the subshell ends.

Reference:

Command substitution, commands grouped with parentheses, and asynchronous commands are invoked in a subshell environment that is a duplicate of the shell environment

2 Comments

@JohnDoe I'm not sure it's possible. You might have to rethink your design of the script.
Oh, but I need to assing a global array within a function, if not, I'd have to repeat a lot of code (repeat the code of the function -30 lines- 15 times -one per call-). There is no other way, isn't it?
1

A solution to this problem, without having to introduce complex functions and heavily modify the original one, is to store the value in a temporary file and read / write it when needed.

This approach helped me greatly when I had to mock a bash function called multiple times in a bats test case.

For example, you could have:

# Usage read_value path_to_tmp_file
function read_value {
  cat "${1}"
}

# Usage: set_value path_to_tmp_file the_value
function set_value {
  echo "${2}" > "${1}"
}
#----

# Original code:

function test1() {
  e=4
  set_value "${tmp_file}" "${e}"
  echo "hello"
}


# Create the temp file
# Note that tmp_file is available in test1 as well
tmp_file=$(mktemp)

# Your logic
e=2
# Store the value
set_value "${tmp_file}" "${e}"

# Run test1
test1

# Read the value modified by test1
e=$(read_value "${tmp_file}")
echo "$e"

The drawback is that you might need multiple temp files for different variables. And also you might need to issue a sync command to persist the contents on the disk between one write and read operations.

Comments

0

Rather than fight with bash to have one function modify a global variable and also return another value, make two functions: one for modifying the global variable and another to return a value.

#!/bin/bash

e=2

function test1() {
  echo "hello"
}

function test2() {
  e2=4
  echo $e2
}

ret=$(test1)
e=$(test2)

echo "$ret"
echo "$e"

Comments

0

To get the result AND exit status from a command executed in a subshell, the following approach can be used.

Here, the command's execution result is stored in an array, to which the command's exit status is appended as last element, which can be split off the preceding results as required:

#! /bin/bash

# Function accepting name ref of array containing a command and args
function getCmdResultAndExitStatus() {
  local printStatus
  if [[ ${1^^} == '-S' ]]; then
    printStatus='TRUE'
    shift
  fi
  local -n c=$@
  # define local tmp array here, assign inside if to avoid exit status pollution
  local -a tmp
  if [[ $printStatus ]]; then
    # Quoting double-quotes to avoid array expansion per word
    tmp='"'$("${c[@]}" 2>&1)'"'
    printf '%s\n' "${tmp[@]}" $?
  else
    tmp='"'$("${c[@]}" 2>&1)'"'
    printf '%s' "${tmp[@]}"
  fi
}

# Command with args 
declare cmd=(ls foo bar)
# Assign executed cmd result and exit status from function to array
declare -a result="($(getCmdResultAndExitStatus -s cmd))"
declare -i resultLen=${#result[@]}
# Split result from exit status (last array element)
echo -e "RESULT: ${result[@]:0:$(($resultLen - 1))}\nCNT: $resultLen"
echo -e "EXIT_STATUS: ${result[@]: -1}"

declare -a result2="($(getCmdResultAndExitStatus cmd))"
echo -e "RESULT_ONLY: ${result2[@]}"
echo -e "LEN: ${#result2[@]}"

The command and its arguments are defined in an array and passed as a named reference to the function getCmdResultAndExitStatus(), where execution happens (without the use of eval). The internal assignment is required for cmd execution.

Finally, the result is printed to stdout, with the exit status appended as last element, if -s was passed to the function as first parameter.

Both can then be captured into an array/var and split, to get the separate result and exit code, if any.

The exit status could also be inserted as first result element, of course.

Comments

0

Doesn't seem there is any variables scope limitation in Bash, as long as you don't assign the output "return" of the function to a variable, If you assign the output of the function to a variable the function will be executed in a separate thread which results in isolating the function from the main code variables.

I just tested the following code in the following bash versions and Linux distributions, and it always works:

Kali GNU 2024.2, GNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu)

Debian 11, GNU bash, version 5.1.4(1)-release (x86_64-pc-linux-gnu)

Rocky Linux 8.10, GNU bash, version 4.4.20(1)-release (x86_64-redhat-linux-gnu)

#!/bin/bash

let ran1=$(( ((1<<63)-1)/3 ))
let ran2=$(( ((1<<63)-1)/5 ))
let c=0
function randomize {
    c=$(( ran1+ran2 ))
    ran2=$ran1
    ran1=$c
    c=$(( c > 0 ))
}

randomize
echo $c

the output of the above code is:

1

This other code works also, the function should return a numeric value between 0 and 255. and you can read the exit code using the variable $?

#!/bin/bash

let ran1=$(( ((1<<63)-1)/3 ))
let ran2=$(( ((1<<63)-1)/5 ))
let c=0
function randomize {
    c=$(( ran1+ran2 ))
    ran2=$ran1
    ran1=$c
    c=$(( c > 0 ))
    return 15
}

randomize
echo  $?
echo $c

the output of the above code is:

15
1

while the following code doesn't work:

#!/bin/bash

let ran1=$(( ((1<<63)-1)/3 ))
let ran2=$(( ((1<<63)-1)/5 ))
let c=0
function randomize {
    c=$(( ran1+ran2 ))
    ran2=$ran1
    ran1=$c
    c=$(( c > 0 ))
    echo "Function output."
}

ret=randomize
echo $ret
echo $c

This code on the other hand outputs the following:

randomize
0

The conclusion is that when the output of a Bash function is assigned to a variable, the function is executed in a different thread which limits the scope of variables and prevents the function from changing the global variables of the main code.

So the easiest solution would be to avoid assigning the output of a function as a return value, and instead try to write it to a global variable.

Comments

0

As an addition to the nice solution from Tino above, I've eliminated the requirement of 'co-functions' like fails_() { passback x y; } and replaced it by getCaptureFuncArgVarNames(), which dynamically extracts the provided function's var arg names. I've made some adaptions and refinements to the original code, too, adding comments (which helped me better understanding it):

# Generates and prints function arg var assignment strings
passback() { 
    #set -- "$@" 
    while [[ 1 -lt $# ]]; do 
        printf '%q=%q;' "$1" "${!1}"
        shift
    done
    return $1
}

# Get the var names of capture()'s function arg var/value pair arguments, omitting values
#
# For example function call: 
# capture var fails x 42 y
# prints 'x y'
# arg 1: function arg name 1 
# arg 2: function arg val 1 (ignored)
# arg 3: function arg name 2
# arg 4:...
getCaptureFuncArgVarNames() {
    local -i argCnt=0
    local -a funcArgVarNames
    # Skip storage var and function name arg, print only function var name args
    for arg in "$@"; do
        if [[ $((argCnt++ % 2)) -eq 0 ]]; then
            local -n ref=$arg
            funcArgVarNames+=("${!ref}")
        fi
    done
    printf '%s\n' "${funcArgVarNames[@]}"
}

# Captures the result of a an evaluated function string (with var/value pair arguments)
# into a given var, returning the function's exit status.
#
# arg 1: capturing var name 
# arg 2: function name 
# arg 3: function arg name 1 
# arg 4: function arg val 1 
# arg 5: function arg name 2
# arg 6:...
# return exit staus of evaluated function with vaue assignment
capture() { 
    local funcArgVarNames="$(getCaptureFuncArgVarNames ${@:3})" 
    eval "$(
        {   out="$("${@:2}" 3<&- 
            passback $funcArgVarNames $? >&3)" 
            ret=$? 
            printf "%q=%q;" "$1" "$out"; 
        } 3>&1 
        echo "(exit $ret)
    ")" 
}

# Test function callee, using named reference arg names, which get assigned the new 
# values from corresponding value args
#
# arg1: named ref of var1 to re-assign
# arg2: value to assign to var1
# arg3: named ref of var2 to re-assign
# ...
# return exit status code (23)
func_fails() {
    local -n argRef1="$1" argRef2="$3"
    local argVal1="$2" argVal2=69 
    # Assignment of external and internal values to provided arg vars refs
    argRef1="$argVal1"
    argRef2="$argVal2"
    echo FAIL
    return 23
}

declare x=0 y=0
echo -e "\nBEFORE F_CALL\nARG_VARS:\t$!x=$x $!y=$y\nRESULT_VAR:\t$var\nEXIT_STATUS:\t$?"
capture var func_fails x 42 y
echo -e "\nAFTER F_CALL\nARG_VARS:\t$!x=$x $!y=$y\nRESULT_VAR:\t$var\nEXIT_STATUS:\t$?"

Comments

-1

You can always use an alias:

alias next='printf "blah_%02d" $count;count=$((count+1))'

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.