Suppress execution trace for echo command?

I'm running shell scripts from Jenkins, which kicks off shell scripts with the shebang options #!/bin/sh -ex.

According to Bash Shebang for dummies?, -x, "causes the shell to print an execution trace", which is great for most purposes - except for echos:

echo "Message"

produces the output

+ echo "Message"
Message

which is a bit redundant, and looks a bit strange. Is there a way to leave -x enabled, but only output

Message

instead of the two lines above, e.g. by prefixing the echo command with a special command character, or redirecting output?


Solution 1:

When you are up to your neck in alligators, it’s easy to forget that the goal was to drain the swamp.                   — popular saying

The question is about echo, and yet the majority of the answers so far have focused on how to sneak a set +x command in.  There’s a much simpler, more direct solution:

{ echo "Message"; } 2> /dev/null

(I acknowledge that I might not have thought of the { …; } 2> /dev/null if I hadn’t seen it in the earlier answers.)

This is somewhat cumbersome, but, if you have a block of consecutive echo commands, you don’t need to do it on each one individually:

{
  echo "The quick brown fox"
  echo "jumps over the lazy dog."
} 2> /dev/null

Note that you don’t need semicolons when you have newlines.

You can reduce the typing burden by using kenorb’s idea of opening /dev/null permanently on a non-standard file descriptor (e.g., 3) and then saying 2>&3 instead of 2> /dev/null all the time.


The first four answers at the time of this writing require doing something special (and, in most cases, cumbersome) every time you do an echo.  If you really want all echo commands to suppress the execution trace (and why wouldn’t you?), you can do so globally, without munging a lot of code.  First, I noticed that aliases aren’t traced:

$ myfunc()
> {
>     date
> }
$ alias myalias="date"
$ set -x
$ date
+ date
Mon, Oct 31, 2016  0:00:00 AM           # Happy Halloween!
$ myfunc
+ myfunc                                # Note that function call is traced.
+ date
Mon, Oct 31, 2016  0:00:01 AM
$ myalias
+ date                                  # Note that it doesn’t say  + myalias
Mon, Oct 31, 2016  0:00:02 AM

(Note that the following script snippets work if the shebang is #!/bin/sh, even if /bin/sh is a link to bash.  But, if the shebang is #!/bin/bash, you need to add a shopt -s expand_aliases command to get aliases to work in a script.)

So, for my first trick:

alias echo='{ set +x; } 2> /dev/null; builtin echo'

Now, when we say echo "Message", we’re calling the alias, which doesn’t get traced.  The alias turns off the trace option, while suppressing the trace message from the set command (using the technique presented first in user5071535’s answer), and then executes the actual echo command.  This lets us get an effect similar to that of user5071535’s answer without needing to edit the code at every echo command.  However, this leaves trace mode turned off.  We can’t put a set -x into the alias (or at least not easily) because an alias only allows a string to be substituted for a word; no part of the alias string can be injected into the command after the arguments (e.g., "Message").  So, for example, if the script contains

date
echo "The quick brown fox"
echo "jumps over the lazy dog."
date

the output would be

+ date
Mon, Oct 31, 2016  0:00:03 AM
The quick brown fox
jumps over the lazy dog.
Mon, Oct 31, 2016  0:00:04 AM           # Note that it doesn’t say  + date

so you still need to turn the trace option back on after displaying message(s) — but only once after every block of consecutive echo commands:

date
echo "The quick brown fox"
echo "jumps over the lazy dog."
set -x
date


It would be nice if we could make the set -x automatic after an echo — and we can, with a bit more trickery.  But before I present that, consider this.  The OP is starting with scripts that use a #!/bin/sh -ex shebang.  Implicitly the user could remove the x from the shebang and have a script that works normally, without execution tracing.  It would be nice if we could develop a solution that retains that property.  The first few answers here fail that property because they turn tracing “back” on after echo statements, unconditionally, without regard to whether it was already on.  This answer conspicuously fails to recognize that issue, as it replaces echo output with trace output; therefore, all the messages vanish if tracing is turned off.  I will now present a solution that turns tracing back on after an echo statement conditionally — only if it was already on.  Downgrading this to a solution that turns tracing “back” on unconditionally is trivial and is left as an exercise.

alias echo='{ save_flags="$-"; set +x;} 2> /dev/null; echo_and_restore'
echo_and_restore() {
        builtin echo "$*"
        case "$save_flags" in
         (*x*)  set -x
        esac
}

$- is the options list; a concatenation of the letters corresponding to all the options that are set.  For example, if the e and x options are set, then $- will be a jumble of letters that includes e and x.  My new alias (above) saves the value of $- before turning tracing off.  Then, with tracing turned off, it throws control over into a shell function.  That function does the actual echo and then checks to see whether the x option was turned on when the alias was invoked.  If the option was on, the function turns it back on; if it was off, the function leaves it off.

You can insert the above seven lines (eight, if you include an shopt) at the beginning of the script and leave the rest alone.

This would allow you

  1. to use any of the following shebang lines:
    #!/bin/sh -ex
    #!/bin/sh -e
    #!/bin/sh –x
    or just plain
    #!/bin/sh
    and it should work as expected.
  2. to have code like
    (shebang)
    command1
    command2
    command3
    set -x
    command4
    command5
    command6
    set +x
    command7
    command8
    command9
    and
    • Commands 4, 5, and 6 will be traced — unless one of them is an echo, in which case it will be executed but not traced.  (But even if command 5 is an echo, command 6 still will be traced.)
    • Commands 7, 8, and 9 will not be traced.  Even if command 8 is an echo, command 9 still will not be traced.
    • Commands 1, 2, and 3 will be traced (like 4, 5, and 6) or not (like 7, 8, and 9) depending on whether the shebang includes x.

P.S. I have discovered that, on my system, I can leave out the builtin keyword in my middle answer (the one that’s just an alias for echo).  This is not surprising; bash(1) says that, during alias expansion, …

… a word that is identical to an alias being expanded is not expanded a second time.  This means that one may alias ls to ls -F, for instance, and bash does not try to recursively expand the replacement text.

Not too surprisingly, the last answer (the one with echo_and_restore) fails if the builtin keyword is omitted1.  But, oddly it works if I delete the builtin and switch the order:

echo_and_restore() {
        echo "$*"
        case "$save_flags" in
         (*x*)  set -x
        esac
}
alias echo='{ save_flags="$-"; set +x;} 2> /dev/null; echo_and_restore'

__________
1 It seems to give rise to undefined behavior.  I’ve seen

  • an infinite loop (probably because of unbounded recursion),
  • a /dev/null: Bad address error message, and
  • a core dump.

Solution 2:

I found a partial solution over at InformIT:

#!/bin/bash -ex
set +x; 
echo "shell tracing is disabled here"; set -x;
echo "but is enabled here"

outputs

set +x; 
shell tracing is disabled here 
+ echo "but is enabled here"
but is enabled here

Unfortunately, that still echoes set +x, but at least it's quiet after that. so it's at least a partial solution to the problem.

But is there maybe a better way to do this? :)

Solution 3:

This way improves upon your own solution by getting rid of the set +x output:

#!/bin/bash -ex
{ set +x; } 2>/dev/null
echo "shell tracing is disabled here"; set -x;
echo "but is enabled here"

Solution 4:

Put set +x inside the brackets, so it would apply for local scope only.

For example:

#!/bin/bash -x
exec 3<> /dev/null
(echo foo1 $(set +x)) 2>&3
($(set +x) echo foo2) 2>&3
( set +x; echo foo3 ) 2>&3
true

would output:

$ ./foo.sh 
+ exec
foo1
foo2
foo3
+ true

Solution 5:

Execution trace goes to stderr, filter it this way:

./script.sh 2> >(grep -v "^+ echo " >&2)

Some explanation, step by step:

  • stderr is redirected… – 2>
  • …to a command. – >(…)
  • grep is the command…
  • …which requires beginning of the line… – ^
  • …to be followed by + echo
  • …then grep inverts the match… – -v
  • …and that discards all the lines you don't want.
  • The result would normally go to stdout; we redirect it to stderr where it belongs. – >&2

The problem is (I guess) this solution may desynchronize the streams. Because of filtering stderr may be a little late in relation to stdout (where echo output belongs by default). To fix it you can join the streams first if you don't mind having them both in stdout:

./script.sh > >(grep -v "^+ echo ") 2>&1

You can build such a filtering into the script itself but this approach is prone to desynchronization for sure (i.e. it has occurred in my tests: execution trace of a command might appear after the output of immediately following echo).

The code looks like this:

#!/bin/bash -x

{
 # original script here
 # …
} 2> >(grep -v "^+ echo " >&2)

Run it without any tricks:

./script.sh

Again, use > >(grep -v "^+ echo ") 2>&1 to maintain the synchronization at the cost of joining the streams.


Another approach. You get "a bit redundant" and strange-looking output because your terminal mixes stdout and stderr. These two streams are different animals for a reason. Check if analyzing stderr only fits your needs; discard stdout:

./script.sh > /dev/null

If you have in your script an echo printing debug/error message to stderr then you may get rid of redundancy in a way described above. Full command:

./script.sh > /dev/null 2> >(grep -v "^+ echo " >&2)

This time we are working with stderr only, so desynchronization is no longer a concern. Unfortunately this way you won't see a trace nor output of echo that prints to stdout (if any). We could try to rebuild our filter to detect redirection (>&2) but if you look at echo foobar >&2, echo >&2 foobar and echo "foobar >&2" then you will probably agree that things get complicated.

A lot depends on echos you have in your script(s). Think twice before you implement some complex filter, it may backfire. It's better to have a bit of redundancy than to accidentally miss some crucial information.


Instead of discarding execution trace of an echo we can discard its output – and any output except the traces. To analyze execution traces only, try:

./script.sh > /dev/null 2> >(grep "^+ " >&2)

Foolproof? No. Think what will happen if there's echo "+ rm -rf --no-preserve-root /" >&2 in the script. Somebody might get heart attack.


And finally…

Fortunately there is BASH_XTRACEFD environmental variable. From man bash:

BASH_XTRACEFD
If set to an integer corresponding to a valid file descriptor, bash will write the trace output generated when set -x is enabled to that file descriptor.

We can use it like this:

(exec 3>trace.txt; BASH_XTRACEFD=3 ./script.sh)
less trace.txt

Note the first line spawns a subshell. This way the file descriptor won't stay valid nor the variable assigned in the current shell afterwards.

Thanks to BASH_XTRACEFD you can analyze traces free of echos and any other outputs, whatever they may be. It's not exactly what you wanted but my analysis makes me think this is (in general) The Right Way.

Of course you can use another method, especially when you need to analyze stdout and/or stderr along with your traces. You just need to remember there are certain limitations and pitfalls. I tried to show (some of) them.