11

I like to use set -x in scripts to show what's going on, especially if the script is going to run in a CI/CD pipeline and I might need to debug some failure post-hoc.

One annoyance with doing this is that if I want to echo some text to the user (e.g., a status message or "I'm starting to do $X") then that message gets output twice - once for the echo command itself being echoed, and then once as the output of that echo command.

What's a good way to make this nicer? One solution is this:

set -x

... bunch of normal commands that get echoed

(
  # Temporarily don't echo, so we don't double-echo
  set +x
  echo "Here is my status message"
)

... rest of commands get echoed again

But the two problems with that are

  1. That's a lot of machinery to write every time I want to tell the user something, and it's "non-obvious" enough that it probably requires the comment every time
  2. It echoes the set +x too, which is undesirable.

Is there another option that works well?

Something like Make's feature of prepending an @ to suppress echoing would be great, but I've not been able to find such a feature in Bash.

14
  • 1. you can convert it into a function but of course it won't help with 2) Nov 16, 2022 at 17:53
  • 1
    @ArkadiuszDrabczyk Converting it to a function doesn't even help with 1., because you have to call that function with the message as an argument, and that will be echoed. Nov 16, 2022 at 19:50
  • It helps with 1 because you don't have to disable tracing explicitly before each log Nov 16, 2022 at 20:58
  • Don't use Bash then.
    – Marco
    Nov 17, 2022 at 4:04
  • 1
    Bash is fine for simple scripts, as soon as it gets complex, it's better to use a higher level language. I don't (seriously) get why so many people insist on doing everything in bash - it's stupid.
    – Marco
    Nov 18, 2022 at 7:52

3 Answers 3

14

You can achieve this in Bash by not using set -x and instead trapping DEBUG and doing your own tracing:

#!/bin/bash

set -T

trap '! [[ "$BASH_COMMAND" =~ ^(echo|printf) ]] &&
      printf "+ %s\n" "$BASH_COMMAND"' DEBUG

foo=bar
echo This is a test
echo $foo
[[ $foo = bar ]] && /usr/bin/printf 'Matched\n'

The idea is to add commands you want to ignore to the regex in the trap line. Running the above produces

+ foo=bar
This is a test
bar
+ [[ $foo = bar ]]
+ /usr/bin/printf 'Matched\n'
Matched

set -T ensures that the trap is inherited by shell functions.

You can add a separate mechanism to enable and disable the trap, e.g. with a shell variable:

#!/bin/bash

set -T

status() {
        NOTRACE=1 echo "$@"
}

trap '! [[ "$BASH_COMMAND" =~ ^(status|NOTRACE=) ]] &&
      printf "+ %s\n" "$BASH_COMMAND"' DEBUG

foo=bar
test=$(echo baz)
echo This is a test
status This is a status report
echo $foo
[[ $foo = bar ]] && /usr/bin/printf 'Matched\n'

This produces

+ foo=bar
+ test=$(echo baz)
+ echo This is a test
This is a test
This is a status report
+ echo $foo
bar
+ [[ $foo = bar ]]
+ /usr/bin/printf 'Matched\n'
Matched
2
  • Interesting idea. The problem I'm seeing, though, is that this would affect every echo, including echo foo > file.txt, so I'd want to have a special command like status for non-echoing output - but then the echo inside that command gets echoed... Nov 17, 2022 at 23:11
  • See the update, all this can easily be dealt with by changing the expression used to disable echoing. Nov 18, 2022 at 11:40
10

This is a horrible kluge, and I feel dirty for suggesting it, but... you could do this with a magic alias. The key to this trick is that aliases are expanded as part of the parsing phase of command execution, so set -x won't make anything print as they expand (unlike a function). So you can make an alias that prepends the "turn off -x" boilerplate before the echo command, and then it turns out you also need a function to run the "turn -x back on" boilerplate at the end.

You also need to turn on alias expansion in your script. It's normally disabled, so that e.g. if you have something like grep aliased to grep --color, that won't make color codes get randomly injected up whenever the script uses grep. So it's safest to run unalias -a first, to remove any potentially troublesome aliases.

Anyway, here's the code:

unalias -a
shopt -s expand_aliases
alias cleanecho='{ set +x; } 2>/dev/null; resetx_after echo'
resetx_after() { "$@"; set -x; }

set -x
cleanecho "Ha, ha, you can't see the command that printed this!"

How it works: a command like cleanecho "something" expands to:

{ set +x; } 2>/dev/null; resetx_after echo "something"

{ set +x; } 2>/dev/null turns off -x mode (with its own trace redirected to /dev/null). Then resetx_after echo "something" runs, executing:

{ echo "something"; set -x; }

...which prints the string and then turns -x tracing back on.

BTW, if you want to be able to use other commands like printf similarly, you could add similar aliases for them:

alias cleanprintf='{ set +x; } 2>/dev/null; resetx_after printf'

...or just make a generic don't-trace-this alias to use as a prefix:

alias notrace='{ set +x; } 2>/dev/null; resetx_after'
notrace printf 'set -x disabled for this command\n'
1
  • 1
    oooh nice, I'm going to have to meditate on whether to add this to our setup scripts... Nov 17, 2022 at 22:58
-2

Easy. And you already half way solved it: just move set -x into the block

#!/bin/sh
echo a
(
    set -x
    echo b
)
echo c
$ a.sh
a
+ echo b
b
c

If you want to not trace a part of the script - do the tracing in two parts

#!/bin/sh
echo a
(
    set -x
    echo b
)
echo c
(
    set -x
    echo d
)
echo e

Just trace the suspicious pieces instead of whole script.

4
  • I think you misunderstood the question, this seems to be the opposite of what I'm asking. Nov 16, 2022 at 18:59
  • @KenWilliams, though they're right in that if you don't want the set +x visible in the execution trace, you need some way to do it implicitly. Any method of running (the equivalent of) set +x would show in the trace, as would e.g. any variable assignment that modifies the trace output itself.
    – ilkkachu
    Nov 16, 2022 at 19:55
  • @KenWilliams You want to not trace a piece of script? Then trace everything else, safe for that piece. Do not turn the tracing on for the whole script. I updated the answer to show it.
    – White Owl
    Nov 16, 2022 at 23:19
  • 7
    This requires that the traced parts of the script run in subshells, meaning that they can't do things like set variables that'll survive past the end of a traced section. It also can't handle having an untraced part in the middle of a complex command (e.g. inside a loop orif or case or...). Nov 17, 2022 at 0:10

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .