Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bash variable defaulting doesn't work if followed by pipe (bash bug?)

Tags:

linux

bash

I've just discovered a strange behaviour in bash that I don't understand. The expression

${variable:=default}

sets variable to the value default if it isn't already set. Consider the following examples:

#!/bin/bash
file ${foo:=$1}
echo "foo >$foo<"
file ${bar:=$1} | cat
echo "bar >$bar<"

The output is:

$ ./test myfile.txt
myfile.txt: ASCII text
foo >myfile.txt<
myfile.txt: ASCII text
bar ><

You will notice that the variable foo is assigned the value of $1 but the variable bar is not, even though the result of its defaulting is presented to the file command.

If you remove the innocuous pipe into cat from line 4 and re-run it, then it both foo and bar get set to the value of $1

Am I missing somehting here, or is this potentially a bash bug?

(GNU bash, version 4.3.30)

like image 450
starfry Avatar asked Mar 19 '23 02:03

starfry


2 Answers

In second case file is a pipe member and runs as every pipe member in its own shell. When file with its subshell ends, $b with its new value from $1 no longer exists.

Workaround:

#!/bin/bash
file ${foo:=$1}
echo "foo >$foo<"

: "${bar:=$1}"     # Parameter Expansion before subshell

file $bar | cat
echo "bar >$bar<"
like image 144
Cyrus Avatar answered Mar 26 '23 01:03

Cyrus


It's not a bug. Parameter expansion happens when the command is evaluated, not parsed, but a command that is part of a pipeline is not evaluated until the new process has been started. Changing this, aside from likely breaking some existing code, would require extra level of expansion before evaluation occurs.

A hypothetical bash session:

> foo=5
> bar='$foo'
> echo "$bar"
$foo
# $bar expands to '$foo' before the subshell is created, but then `$foo` expands to 5
# during the "normal" round of parameter expansion.
> echo "$bar" | cat
5

To avoid that, bash would need some way of marking pieces of text that result from the new first round of pre-evaluation parameter expansion, so that they do not undergo a second round of evaluation. This type of bookkeeping would quickly lead to unmaintainable code as more corner cases are found to be handled. Far simpler is to just accept that parameter expansions will be deferred until after the subshell starts.

The other alternative is to allow each component to run in the current shell, something that is allowed by the POSIX standard, but is not required, either. bash made the choice long ago to execute each component in a subshell, and reversing that would break too much existing code that relies on the current behavior. (bash 4.2 did introduce the lastpipe option, allowing the last component of a pipeline to execute in the current shell if explicitly enabled.)

like image 21
chepner Avatar answered Mar 26 '23 01:03

chepner