15

I've written a quick-and-dirty script to time some reports from a web service:

BASE_URL='http://example.com/json/webservice/'
FIRST=1
FINAL=10000

for report_code in $(seq 1 $FINAL); do
  (time -p response=$(curl --write-out %{http_code} --silent -O ${BASE_URL}/${report_code}) ) 2> ${report_code}.time

  echo $response  # <------- this is out of scope!  How do I fix that?
  if [[ $response = '404' ]]; then
    echo "Deleting report # ${report_code}!"
    rm ${report_code}
  else
    echo "${report_code} seems to be good!"
  fi
done

I need to wrap the time command in a subshell so I can redirect its output, but that makes the value of $response unavailable to the parent shell. How do I get around this problem?

iconoclast
  • 9,198
  • 13
  • 57
  • 97

4 Answers4

13

You can't bring a variable's value from a subshell to its parent, not without doing some error-prone marshalling and cumbersome communication.

Fortunately, you don't need a subshell here. Redirection only requires command grouping with { … }, not a subshell.

{ time -p response=$(curl --write-out '%{http_code}' --silent -O "${BASE_URL}/${report_code}"); } 2> "${report_code}.time"

(Don't forget double quotes around variable substitutions.)

  • its funny that every programming language can do that. i already spent 1h googling with no possible solution in shell. im disapointed. – To Kra Dec 28 '16 at 17:49
  • 1
    @ToKra No. No programming language can do this. (Almost) any programming language can bring a variable's value from a subroutine or instruction block to its parent, and that's precisely what I explain in my answer: use command grouping { … } instead of a subshell ( … ). – Gilles 'SO- stop being evil' Dec 28 '16 at 17:51
  • 1
    Note that the final statement needs the semicolon included in this answer if it is on the same line as the closing brace. Otherwise you'll get a syntax error. – ATLief Aug 09 '21 at 23:06
4

Fellow U&L users: Before downvoting my answer for using C-style with main() function, please visit this link: https://unix.stackexchange.com/a/313561/85039 Using main functions in scripts is a common practice, used by many professionals in the field.


As Gilles pointed out, subshells cannot make variables available outside of their environment. But let's approach this problem from another angle - if you write your script in functions, it's possible to declare variable as local and that can be edited.

From bash 4.3's manual, local description:

...When local is used within a function, it causes the variable name to have a visible scope restricted to that function and its children...

Example:

#!/bin/bash

outter()
{
    for i in $(seq 1 3)
    do
        var=$i
    done
}

main()
{
    local var=0
    outter
    echo $var
}
main "$@"
$ ./testscript.sh                                                                                                        
3

As you can see after 3 iterations of the looping function, the variable is modified.

0

You can redirect your variable in a test.txt and get it in the parent shell.

testfn()
{
echo "test" > test.txt # redirect your variable in a test.txt
}

testfn & # execute your function in the subshell

testresult=$(cat test.txt) # get your variable in the parent shell printf '%s\n' "$testresult"

Kusalananda
  • 333,661
akmot
  • 21
  • A race condition is introduced by the use of the ampersand ("&") here. Other than causing testfn to run in a subshell, it also causes testfn to run in the background asynchronously. This problem does not manifest here due to echo's speed, but consider changing echo to a curl command like OP or adding a sleep in front of echo. Potential results include "test.txt: No such file or directory" or an empty string.

    To resolve, keep the parentheses like OP (e.g., (testfn)) or add a wait after testfn & but before testresult=[...]

    – Stanley Yu Jun 15 '22 at 04:48
0

As of bash 4.3 you can do this with nameref variables

#!/usr/bin/env bash

outvar is a nameref variable that allows us to set the value of a variable

outside our scope

addfoo() { local x="$1" local -n outvar=$2

x="foo$x"

outvar="$x" }

declare b=“loll”

addfoo "bar" b

echo "$b”

This will output foobar as the function addfoo modifies the b variable, even though b is outside its scope.

Namerefs are useful for extracting values from a function without using a sub shell $(addfoo).

koehn
  • 163