12

Here's a shell script:

globvar=0

function myfunc {
  let globvar=globvar+1
  echo "myfunc: $globvar"
}

myfunc
echo "something" | myfunc

echo "Global: $globvar"

When called, it prints out the following:

$ sh zzz.sh
myfunc: 1
myfunc: 2
Global: 1
$ bash zzz.sh
myfunc: 1
myfunc: 2
Global: 1
$ zsh zzz.sh
myfunc: 1
myfunc: 2
Global: 2

The question is: why this happens and what behavior is correct?

P.S. I have a strange feeling that function behind the pipe is called in a forked shell... So, can there be a simple workaround?

P.P.S. This function is a simple test wrapper. It runs test application and analyzes its output. Then it increments $PASSED or $FAILED variables. Finally, you get a number of passed/failed tests in global variables. The usage is like:

test-util << EOF | myfunc
input for test #1
EOF
test-util << EOF | myfunc
input for test #2
EOF
echo "Passed: $PASSED, failed: $FAILED"
codeforester
  • 39,467
  • 16
  • 112
  • 140
zserge
  • 2,212
  • 2
  • 31
  • 40
  • Have you read: http://tldp.org/LDP/abs/html/localvar.html ? – Dave Jarvis Nov 23 '10 at 15:13
  • well, to my mind $globvar is not a local variable, because calling myfunc() without a pipe increments the global variable $globvar pretty well. The problem is that calling function over pipe doesn't work in bash/sh. – zserge Nov 23 '10 at 15:15

3 Answers3

7

Korn shell gives the same results as zsh, by the way.

Please see BashFAQ/024. Pipes create subshells in Bash and variables are lost when subshells exit.

Based on your example, I would restructure it something like this:

globvar=0

function myfunc {
    echo $(($1 + 1))
}

myfunc "$globvar"
globalvar=$(echo "something" | myfunc "$globalvar")
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • Thank for the link! The question is how to pass back two variables: $PASSED and $FAILED. I understand, that I can look for $?, if it equals zero - increment $PASSED by my own, otherwise - increment $FAILED. I just would like to as little code as possible outside the myfunc(). – zserge Nov 23 '10 at 15:36
  • @zserge: One way would be like this: `myfunc() { echo "two words"; }; read word1 word2 <<< $(myfunc)` or `array=($(myfunc))`. – Dennis Williamson Nov 23 '10 at 15:49
3

Piping something into myfunc in sh or bash causes a new shell to spawn. You can confirm this by adding a long sleep in myfunc. While it's sleeping call ps and you'll see a subprocess. When the function returns, that sub shell exits without changing the value in the parent process.

If you really need that value to be changed, you'll need to return a value from the function and check $PIPESTATUS after, I guess, like this:

globvar=0

function myfunc {
  let globvar=globvar+1
  echo "myfunc: $globvar"
  return $globvar
}

myfunc
echo "something" | myfunc
globvar=${PIPESTATUS[1]}

echo "Global: $globvar"
Matt K
  • 13,370
  • 2
  • 32
  • 51
0

The problem is 'which end of a pipeline using built-ins is executed by the original process?'

In zsh, it looks like the last command in the pipeline is executed by the main shell script when the command is a function or built-in.

In Bash (and sh is likely to be a link to Bash if you're on Linux), then either both commands are run in a sub-shell or the first command is run by the main process and the others are run by sub-shells.

Clearly, when the function is run in a sub-shell, it does not affect the variable in the parent shell (only the global in the sub-shell).

Consider adding an extra test:

echo Something | { myfunc; echo $globvar; }
echo $globvar
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278