infertux / bashcov

Code coverage tool for Bash
MIT License
150 stars 19 forks source link

Shell flags inherited by child shells #50

Open CyberShadow opened 5 years ago

CyberShadow commented 5 years ago

Consider these scripts:

cat > child.sh <<'EOF'
#!/bin/bash
echo $unset
echo "Survived!"
EOF

cat > parent.sh <<'EOF'
#!/bin/bash
set -eu
./child.sh
EOF

chmod +x parent.sh child.sh

The behavior is different depending on whether the script is run under bashcov or not:

$ ./parent.sh          

Survived!

$ bashcov ./parent.sh
./child.sh: line 3: unset: unbound variable
Run completed using bashcov 1.8.2 with Bash 5.0, Ruby 2.6.2, and SimpleCov 0.15.1.
Coverage report generated for /bin/bash ./parent.sh to /home/vladimir/tmp/2019-04-04-scratch/04:43:13/coverage. 2 / 4 LOC (50.0%) covered.
tomeon commented 5 years ago

@CyberShadow -- looks like this is due to the fact that Bashcov exports SHELLOPTS=xtrace into Bash's environment:

cat > child.sh <<'EOF'
#!/usr/bin/env bash
printf 1>&2 '%s: SHELLOPTS=%s\n' "${BASH_SOURCE[0]}" "$SHELLOPTS"
echo $unset
echo "Survived!"
EOF

cat > parent.sh <<'EOF'
#!/usr/bin/env bash
set -eu
printf 1>&2 '%s: SHELLOPTS=%s\n' "${BASH_SOURCE[0]}" "$SHELLOPTS"
./child.sh
EOF

chmod +x parent.sh child.sh

Works as expected when run without touching SHELLOPTS:

$ ./parent.sh
./parent.sh: SHELLOPTS=braceexpand:errexit:hashall:interactive-comments:nounset
./child.sh: SHELLOPTS=braceexpand:hashall:interactive-comments

Survived!

Also works as expected when run with bash -x:

$ bash -x ./parent.sh
+ set -eu
+ printf '%s: SHELLOPTS=%s\n' ./parent.sh braceexpand:errexit:hashall:interactive-comments:nounset:xtrace
./parent.sh: SHELLOPTS=braceexpand:errexit:hashall:interactive-comments:nounset:xtrace
+ ./child.sh
./child.sh: SHELLOPTS=braceexpand:hashall:interactive-comments

Survived!

But, when called with called with env SHELLOPTS=xtrace:

$ env SHELLOPTS=xtrace ./parent.sh 
+ set -eu
+ printf '%s: SHELLOPTS=%s\n' ./parent.sh braceexpand:errexit:hashall:interactive-comments:nounset:xtrace
./parent.sh: SHELLOPTS=braceexpand:errexit:hashall:interactive-comments:nounset:xtrace
+ ./child.sh
+ printf '%s: SHELLOPTS=%s\n' ./child.sh braceexpand:errexit:hashall:interactive-comments:nounset:xtrace
./child.sh: SHELLOPTS=braceexpand:errexit:hashall:interactive-comments:nounset:xtrace
./child.sh: line 3: unset: unbound variable

Interestingly, it looks like the issue crops up whenever the parent explicitly defines SHELLOPTS, not just when it enables xtrace:

$ env SHELLOPTS=emacs ./parent.sh 
./parent.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: line 3: unset: unbound variable

This behavior goes back a while:

matt@ratmaster /tmp/tmp.LXRydMzQPe :( $ for bv in 3.2 4.0 4.1 4.2 4.3 4.4 5.0; do printf 1>&2 -- '# %s\n' "$bv"; docker run -it --rm -v "${PWD}:/shellopts" --workdir /shellopts -u "$(id -u):$(id -g)" "bash:${bv}" env SHELLOPTS=emacs bash /shellopts/parent.sh; done
# 3.2
/shellopts/parent.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: line 3: unset: unbound variable
# 4.0
/shellopts/parent.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: line 3: unset: unbound variable
# 4.1
/shellopts/parent.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: line 3: unset: unbound variable
# 4.2
/shellopts/parent.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: line 3: unset: unbound variable
# 4.3
/shellopts/parent.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: line 3: unset: unbound variable
# 4.4
/shellopts/parent.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: line 3: unset: unbound variable
# 5.0
/shellopts/parent.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: SHELLOPTS=braceexpand:emacs:errexit:hashall:interactive-comments:nounset
./child.sh: line 3: unset: unbound variable

Hypothesis: using env SHELLOPTS=xtrace results in the equivalent of set -x; export SHELLOPTS, causing child processes of Bash to inherit SHELLOPTS. Further adjustments of SHELLOPTS (e.g., set -eu) are now inherited by child processes, too. So, revising ./parent.sh to run export SHELLOPTS:

cat > child.sh <<'EOF'
#!/usr/bin/env bash
printf 1>&2 '%s: SHELLOPTS=%s\n' "${BASH_SOURCE[0]}" "$SHELLOPTS"
echo $unset
echo "Survived!"
EOF

cat > parent.sh <<'EOF'
#!/usr/bin/env bash
set -eu
printf 1>&2 '%s: SHELLOPTS=%s\n' "${BASH_SOURCE[0]}" "$SHELLOPTS"
export SHELLOPTS
./child.sh
EOF

chmod +x parent.sh child.sh

Results in:

$ ./parent.sh 
./parent.sh: SHELLOPTS=braceexpand:errexit:hashall:interactive-comments:nounset
./child.sh: SHELLOPTS=braceexpand:errexit:hashall:interactive-comments:nounset
./child.sh: line 3: unset: unbound variable

At present, there is not much Bashcov can do about this issue -- setting SHELLOPTS=xtrace is essential to how it gathers coverage data for all shell scripts under test. A while back, I started working on a branch that globally enables executing tracing by setting BASH_ENV to the path of a script that runs (more or less) set -x, rather than by setting SHELLOPTS=xtrace. I've left this effort on the back burner for the last several months, but may be able to find some time in the coming weeks to complete it.

CyberShadow commented 5 years ago

Thanks. Would it make sense to make bashcov do the equivalent of bash -c "set -x ; source $script" instead of env SHELLOPTS=xtrace $script ?

tomeon commented 5 years ago

Would it make sense to make bashcov do the equivalent of bash -c "set -x ; source $script" instead of env SHELLOPTS=xtrace $script ?

I think this could be implemented as an optional execution mode, with a conspicuous note that using this mode may limit the number of scripts Bashcov is able to gather coverage data for, since (unlike env SHELLOPTS=xtrace) set -x doesn't globally enable execution tracing.