14

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?

Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
iconoclast
  • 9,057
  • 12
  • 56
  • 95

3 Answers3

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.)

Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
  • 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.

Sergiy Kolodyazhnyy
  • 16,187
  • 11
  • 53
  • 104
  • Hmm, unintuitive behavior for C programmers. I'd normally expect `outter` to refer to a global instance of `var`. – Ruslan May 14 '17 at 16:20
  • This answer has some flaws. `"$var"` is not needed in the call out `outter`. It does not do anything. And also `local var=0` does not do anything; the call ouf `outter` does overwrite `var`, as you stated. – Golar Ramblar Jul 18 '18 at 14:25
  • @GolarRamblar I've removed `"$var"` as positional argument to `outter`; to be fair, this is out of a habit. Can you elaborate as to `local var=0` part ? – Sergiy Kolodyazhnyy Jul 18 '18 at 14:45
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
  • 320,670
  • 36
  • 633
  • 936
akmot
  • 21
  • 5
  • 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