5

I can easily capture stdout from a function call (in subshell) to a variable with:

val="$(get_value)"

I can also modify variables (for instance, an array) in a shell by reference so to speak in the same shell with something like:

function array.delete_by_index {
    local array_name="$1"
    local key="$2"

    unset "$array_name[$key]"
    eval "$array_name=(\"\${$array_name[@]}\")"
}

array.delete_by_index "array1" 0

But what I'm struggling to figure out how to do is to do both at the same time, in a clean manner. An example of where I want this is popping a value from an array:

function array.pop {
    local array_name="$1"

    local last_index=$(( $(eval "echo \${#$array_name[@]}") - 1 ))
    local tmp="$array_name[\"$last_index\"]"
    echo "${!tmp}"

    # Changes "$array_name" here, but not in caller since this is a sub shell
    array.delete_by_index "$array_name" $last_index
}

val="$(array.pop "array1")"

It seems to me like all forms of capturing stdout to a variable require a subshell in bash, and using a sub shell will not allow me the ability to change a value by reference in the caller's context.

I'm wondering if anyone know a magical bashism to accomplish this? I do not particularly want a solution that uses any kind of file/fifo on the filesystem.

The 2nd answer in this question seems to suggest that this is possible in ksh using val="${ cmd; }", as this construct apparently allows for capturing output, but without using sub shells. So yes, I could technically switch to ksh, but I'd like to know if this is possible in bash.

  • 1
    Your script, it seems to me, has become complex enough that it should have been rewritten in a more powerful language. Do it now, it's not too late! – James Youngman Jan 8 '17 at 22:24
6

This works in both bash (since release 4.3) and ksh93. To "bashify" it, replace all typeset with local in the functions, and the typeset in the global scope with declare (while keeping all the options!). I honestly don't know why Bash has so many different names for things that are just variations of typeset.

function stack_push
{
    typeset -n _stack="$1"
    typeset element="$2"

    _stack+=("$element")
}

function stack_pop
{
    typeset -n _stack="$1"
    typeset -n _retvar="$2"

    _retvar="${_stack[-1]}"

    unset _stack[-1]
}

typeset -a stack=()

stack_push stack "hello"
stack_push stack "world"

stack_pop stack value
printf '%s ' "$value"

stack_pop stack value
printf '%s\n' "$value"

Using a nameref in the function, you avoid eval (I've never had to use eval anywhere in any script!). By providing the stack_pop function with a place to store the popped value, you avoid the subshell. By avoiding the subshell, the stack_pop function can modify the value of the stack variable in the outer scope.

The underscores in the local variables in the function is to avoid having a nameref that has the same name as the variable that it references (Bash doesn't like it, ksh doesn't mind, see this question).

In ksh you could write the stack_pop function like

function stack_pop
{
    typeset -n _stack="$1"

    printf '%s' "${_stack[-1]}"

    unset _stack[-1]
}

And then call it with

printf '%s %s\n' "${ stack_pop stack }" "${ stack_pop stack }"

(${ ... } is the same as $( ... ) but does not create a subshell)

But I'm not a big fan of this. IMHO, stack_pop should not have to send the data to stdout, and I should not have to call it with ${ ... } to get the data. I could possibly be more ok with my original stack_pop, and then add a stack_pop_print that does the above, if needed.

For Bash, you could go with the stack_pop in the beginning of my post, and then have a stack_top_print that just prints the top element of the stack to stdout, without removing it (which it can't because it would most likely be running in a $( ... ) subshell).

2

The only solution I could think of without using files or modifying the function was to run the command twice, in which one be ran in a subshell for capturing, then the other to modify the variable in the parent shell.

val="$(array.pop "array1")"
array.pop array1

If rewriting the function is an option:

function array.pop {
    local array_name="$1";
    local last_index=$(( $(eval "echo \${#$array_name[@]}") - 1 ));
    local tmp="$array_name[\"$last_index\"]";
    echo "${!tmp}"; 

    if [[ $2 ]]; then
        eval "$2=\"${!tmp}\""; # magic bashism here 
    fi

    array.delete_by_index "$array_name" "$last_index"; 
}

To be ran as array.pop 'array1' variablename, which will also initialize the variable even if it does not exist:

$ array1=(one two three)
$ array.pop array1 var
three
$ echo "$var"
three
$ echo "${array1[*]}"
one two

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.