bashscopeifs

How does one properly assign temporary Bash variables on a per-command basis?


Bash seems to behave unpredictably in regards to temporary, per-command variable assignment, specifically with IFS.

I often assign IFS to a temporary value in conjunction with the read command. I would like to use the same mechanic to tailor output, but currently resort to a function or subshell to contain the variable assignment.

$ while IFS=, read -a A; do
>   echo "${A[@]:1:2}"                # control (undesirable)
> done <<< alpha,bravo,charlie
bravo charlie

$ while IFS=, read -a A; do
>   IFS=, echo "${A[*]:1:2}"          # desired solution (failure)
> done <<< alpha,bravo,charlie
bravo charlie

$ perlJoin(){ local IFS="$1"; shift; echo "$*"; }
$ while IFS=, read -a A; do
>   perlJoin , "${A[@]:1:2}"          # function with local variable (success)
> done <<< alpha,bravo,charlie
bravo,charlie

$ while IFS=, read -a A; do
>   (IFS=,; echo "${A[*]:1:2}")       # assignment within subshell (success)
> done <<< alpha,bravo,charlie
bravo,charlie

If the second assignment in the following block does not affect the environment of the command, and it does not generate an error, then what is it for?

$ foo=bar
$ foo=qux echo $foo
bar

Solution

  • $ foo=bar
    $ foo=qux echo $foo
    bar
    

    This is a common bash gotcha -- and https://www.shellcheck.net/ catches it:

    
    foo=qux echo $foo
    ^-- SC2097: This assignment is only seen by the forked process.
                 ^-- SC2098: This expansion will not see the mentioned assignment.
    

    The issue is that the first foo=bar is setting a bash variable, not an environment variable. Then, the inline foo=qux syntax is used to set an environment variable for echo -- however echo never actually looks at that variable. Instead $foo gets recognized as a bash variable and replaced with bar.

    So back to your main question, you were basically there with your final attempt using the subshell -- except that you don't actually need the subshell:

    while IFS=, read -a A; do
      IFS=,; echo "${A[*]:1:2}"
    done <<< alpha,bravo,charlie
    

    outputs:

    bravo,charlie
    

    For completeness, here's a final example that reads in multiple lines and uses a different output separator to demonstrate that the different IFS assignments aren't stomping on each other:

    while IFS=, read -a A; do
      IFS=:; echo "${A[*]:1:2}"
    done < <(echo -e 'alpha,bravo,charlie\nfoo,bar,baz')
    

    outputs:

    bravo:charlie
    bar:baz