Suppress execution trace for echo command?
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
I love the comprehensive and well explained answer by g-man, and consider it the best one provided so far. It cares about the context of the script, and doesn't force configurations when they aren't needed. So, if you're reading this answer first go ahead and check that one, all the merit is there.
However, in that answer there is an important piece missing: the proposed method won't work for a typical use case, i.e. reporting errors:
COMMAND || echo "Command failed!"
Due to how the alias is constructed, this will expand to
COMMAND || { save_flags="$-"; set +x; } 2>/dev/null; echo_and_restore "Command failed!"
and you guessed it, echo_and_restore
gets executed always, unconditionally. Given that the set +x
part didn't run, it means that the contents of that function will get printed, too.
Changing the last ;
to &&
wouldn't work either, because in Bash, ||
and &&
are left-associative.
I found a modification which works for this use case:
echo_and_restore() {
cat -
case "$save_flags" in
(*x*) set -x
esac
}
alias echo='({ save_flags="$-"; set +x; } 2>/dev/null; echo_and_restore) <<<'
It uses a subshell (the (...)
part) in order to group all commands, and then passes the input string through stdin as a Here String (the <<<
thing) which is then printed by cat -
. The -
is optional, but you know, "explicit is better than implicit".
The cat -
can be changed to personalize the output. For example, to prepend the name of the currently running script, you could change the function to something like this:
echo_and_restore() {
local BASENAME; BASENAME="$(basename "$0")" # File name of the current script.
echo "[$BASENAME] $(cat -)"
case "$save_flags" in
(*x*) set -x
esac
}
And now it works beautifully:
false || echo "Command failed"
> [test.sh] Command failed
Christian
Updated on September 18, 2022Comments
-
Christian over 1 year
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 outputMessage
instead of the two lines above, e.g. by prefixing the echo command with a special command character, or redirecting output?
-
DavidPostill over 7 yearsNote, even the edited version of your deleted answer (which this is a copy of), still doesn't answer the question, as the answer does not output just the message without the associated echo command, which was what the OP asked for.
-
DavidPostill over 7 yearsNote, this answer is being discussed on meta Have I been penalized for an answer 1 moderator didn't like?
-
HiTechHiTouch over 7 years@David I have enlarged the explanation, per comments in the meta forum.
-
HiTechHiTouch over 7 years@David Again I encourage you to read the OP very carefully, paying attention to the Latin ".e.g.". The poster does NOT require the echo command be used. The OP only references echo as an example (along with redirection) of potential methods to solve the problem. Turns out you do not need either,
-
G-Man Says 'Reinstate Monica' over 7 yearsI have seen some amazing magic tricks done with aliases, so I know my knowledge thereof is incomplete. If anybody can present a way to do the equivalent of
echo +x; echo "$*"; echo -x
in an alias, I’d like to see it. -
G-Man Says 'Reinstate Monica' over 7 yearsAnd what if the OP wants to do something like
echo "Here are the files in this directory:" *
? -
HiTechHiTouch over 7 yearsTry
m="Here are the files in this directory"; m=$(ls -A);
The idea is that anything you can express as, or convert to, an assignment operand will do. -
HiTechHiTouch over 7 years@G-Man (forgot to tag you on the previous comment). Also consider the line between "one liner" type messages, and full information dumps. At some point the needs grows beyond this q&d trick, and you end up writing a debugging "pretty printer".
-
GeneralTao almost 7 yearsHi, the doc you're referencing is for gnu make, not shell. Are you sure it works? I get the error
./test.sh: line 1: @echo: command not found
, but I'm using bash. -
derek almost 7 yearsWow, sorry I completely misread the question. Yes,
@
only works when you are echoing in makefiles. -
Shakaron over 6 years@derek I edited your original reply so it now clearly states that the solution is limited for Makefiles only. I was actually looking for this one, so I wanted your comment not to have a negative reputation. Hopefully, people will find it helpful too.
-
Ammar Bin Nasir over 5 yearsCorrect me if I'm wrong, but I don't think the
set +x
inside the subshell (or using subshells at all) is doing anything useful. You can remove it and get the same result. It's the redirecting stderr to /dev/null that is doing the work of temporarily "disabling" tracing... It seemsecho foo1 2>/dev/null
, etc., would be just as effective, and more readable. -
kenorb over 5 yearsHaving tracing in your script could impact performance. Secondly redirecting &2 to NULL could be not the same, when you expect some other errors.
-
Ammar Bin Nasir over 5 yearsCorrect me if I'm wrong, but in your example you already have tracing enabled in your script (with
bash -x
) and you are already redirecting&2
to null (since&3
was redirected to null), so I'm not sure how that comment is relevant. Maybe we just need a better example that illustrates your point, but in the given example at least, it still seems like it could be simplified without losing any benefit. -
Grigory Entin about 4 yearsI wonder what is the difference between
{ echo foo; } 2> /dev/null
and(echo foo) 2> /dev/null
. Both work for me and the latter looks a bit more straightforward... -
G-Man Says 'Reinstate Monica' about 4 years@GrigoryEntin: See Bash subshell creation with curly braces and Why does ( exit 1 ) not exit the script?, both of which link to the Bash Reference Manual. What those pages may not spell out is that, for the
echo
command, it probably doesn’t matter — because (1)echo
is a builtin command (unlike programs likecat
,date
andls
), … (Cont’d) -
G-Man Says 'Reinstate Monica' about 4 years(Cont’d) … and (2) it doesn’t change the state of the shell (unlike
cd
,exit
and assigning to a variable). See also What are the shell's control and redirection operators?, if you haven’t already. It doesn’t really address your question, but it is a good general reference. Disclosure: I wrote the answer about(
and)
. -
Grigory Entin about 4 years@G-ManSays'ReinstateMonica' Thanks! So given that the original question is exactly about
echo
command, it looks like(echo foo) 2> /dev/null
is a valid alternative? -
G-Man Says 'Reinstate Monica' about 4 years@GrigoryEntin: Well, the difference is that, potentially, any time you have command(s) in parentheses, the shell forks a subshell — did you read the thread about
( exit 1 )
? I said that it doesn’t matter forecho
, becauseecho
doesn’t change the state of the shell — but I meant that it doesn’t affect the functionality. Repeatedly forking unnecessary subshells will affect performance. Here are some more references for you: What's the POSIX specification on behavior of built-in commands with redirections and/or piping?, … (Cont’d) -
G-Man Says 'Reinstate Monica' about 4 years
-
G-Man Says 'Reinstate Monica' about 4 years(Cont’d) … See in particular this answer by Stéphane Chazelas, who is very knowledgeable in such matters: “Most shells including
bash
will implement [a separate shell execution environment] by evaluating the code inside(...)
in a child process …, but ksh93 [will not].” … “ksh93 doesn't fork in” the case of a simple command in parentheses. I’m not 100% sure that bash will actually fork a subshell to execute a builtin command, but that’s what Stéphane seems to be saying. So that’s why people prefer to avoid parentheses. -
Grigory Entin about 4 years@G-ManSays'ReinstateMonica' Yep, thanks for clarification. Yep, I read that thread about
(exit 1)
and understand the difference, the fact that()
is forking subshells and etc. My case is pretty simple (just some config tooling), and I rather interested in readability (especially for people not experienced in shells)/correctness in terms of exit statuses and etc rather than performance. So leaving performance aside, it looks like forecho
it's quite ok to use()
in my case. -
MoonLite over 2 yearsMinor diff tip: I was using
echo -e '\n------\nXXX\n---\n'
for extra newlines. That did not work well (it just echoes-e
and the\n
stay as is). But if I change your echo_and_restore function frombuiltin echo "$*"
tobuiltin echo $*
(without double quotes) the-e
works as expected. However that does mean some odd stuff happens if you happen to echo i.e./home/*
as it gets expanded... Perhaps better to use printf so I don't need the -e flag and just usebuiltin printf "$*"
(and replace all echo with printf). -
Admin almost 2 yearsTip (1): In some shells including
bash
you will need to sayshopt -s expand_aliases
in order to use aliases in a script. Tip (2): Changeecho_and_restore
torun_and_restore
. Change the start of the function tolocal cmd="$1"
thenshift
then$cmd "$*"
. Now you can create aliases for bothecho
andprintf
. Like this:alias echo='{ save_flags="$-"; set +x;} 2> /dev/null; run_and_restore "builtin echo"'
-
Admin almost 2 years@BryanRoach: (1) I covered that already: “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 ashopt -s expand_aliases
command to get aliases to work in a script.” (2) Please learn how quoting works. Your code doesn’t work properly forprintf
, and it has the same problem withecho
that my answer has (identified by MoonLite). -
Admin almost 2 years@G-ManSays'ReinstateMonica' (1) Oh, sorry! I should have checked more carefully. (2) My code works for the simple case of passing one string to print, but we can support all cases. None of the answers have gotten the quoting right. Instead of
"$*"
or$*
it should be"$@"
.