danidc
danidc

Reputation: 1339

Shell option changes (e.g. set -o xtrace) are not applied from inside parenthesis

I am trying to create an alias for my bashrc which enables the options nounset and xtrace if xtrace is not set, and which disables them if they are set, allowing me to toggle the "debug mode".

I have this:

alias debug="[[ \$(set -o | grep xtrace) = *off ]] && (set -o nounset; set -o xtrace; echo 'Debug mode ON') || (set +o nounset; set +o xtrace; echo 'Debug mode OFF')"

My problem is that this seems to work looking at the terminal output, but when I run set -o neither of the options nounset and xtrace have changed. The reason is that both options are inside a parenthesis (), so the options are not applied to the current terminal.

For example, this does not work either (xtrace stays off):

(set -o xtrace)

How do I solve this? Is there a way to write the alias above without the parenthesis, or to apply the options from inside the parenthesis? And why does this happen?

Upvotes: 0

Views: 1479

Answers (2)

Jonathan Leffler
Jonathan Leffler

Reputation: 754050

Parentheses in either $(…) or (set -o ...;) create a sub-shell. Changes made in the sub-shell have no effect on the parent shell, whether it is environment variables or shell options.

You seem to want to noisily toggle the tracing flag. That's also know as the -x flag, and is visible in $-, a parameter containing the currently set single-letter flags. You could also use set -u and set +u for the set -o nounset option.

Therefore, you could use some variant on:

alias debug='case "$-" in
             (*x*) set +xu; echo "Debug mode OFF";;
             (*)   set -xu; echo "Debug mode ON";;
             esac'

As written, that can be flattened onto a single line if you prefer. There are other ways to write that, using if and appropriate conditionals. I still like case, but I learned shell scripting before there was a Bash with the [[ command.

You can also use { and } instead of the sub-shell notations. Therefore a minimalist set of changes to your script would be:

alias debug="[[ \$(set -o | grep xtrace) = *off ]] && { set -o nounset; set -o xtrace; echo 'Debug mode ON'; } || { set +o nounset; set +o xtrace; echo 'Debug mode OFF'; }"

Note that there must be an 'end of command' before the } — practically, that means newline or semicolon. And the { and } must be separate from surrounding symbols. I don't like the use of $(set -o | grep xtrace); that's very heavy by comparison with using $-. You can get around that by using $- again:

alias debug='[[ ! $- =~ x ]] && { set -o nounset -o xtrace; echo "Debug mode ON"; } || { set +o nounset +o xtrace; echo "Debug mode OFF"; }'

That's a bit noisy when you turn debug on; it would be better written as:

alias debug='[[ ! $- =~ x ]] && { echo "Debug mode ON"; set -o nounset -o xtrace; } || { set +o nounset +o xtrace; echo "Debug mode OFF"; }'

The echo is done while the tracing is off. You still get to see the testing when debugging is currently on and you're about to turn it off; that's unavoidable.

You could also use an explicit if/then/else/fi:

alias debug='if [[ $- =~ x ]]; then set +o nounset +o xtrace; echo "Debug mode OFF"; else echo "Debug mode ON"; set -o nounset -o xtrace; fi'

Or compress that down to:

alias debug='if [[ $- =~ x ]]; then set +xu; echo "Debug mode OFF"; else echo "Debug mode ON"; set -xu; fi'

Upvotes: 1

ephemient
ephemient

Reputation: 204778

alias debug="[[ \$(set -o | grep xtrace) = *off ]] && { set -o nounset; set -o xtrace; echo 'Debug mode ON'; } || { set +o nounset; set +o xtrace; echo 'Debug mode OFF'; }"

{ } can group together multiple commands without creating a subshell as () does. The syntax is a bit different; you need to make sure to leave whitespace between { } and any words, otherwise it can be interpreted as {a,b} → a b brace expansion, and the commands need to be terminated with a ; (or &).

Upvotes: 1

Related Questions