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
- 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. - 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 anecho
, 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
.
- Commands 4, 5, and 6 will be traced — unless one of them is an
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
tols -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 tostderr
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 whenset -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.