Suppress execution trace for echo command?

60,536

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

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
Share:
60,536
Christian
Author by

Christian

Updated on September 18, 2022

Comments

  • Christian
    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 output

    Message
    

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

  • DavidPostill
    DavidPostill over 7 years
    Note, 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
    DavidPostill over 7 years
    Note, this answer is being discussed on meta Have I been penalized for an answer 1 moderator didn't like?
  • HiTechHiTouch
    HiTechHiTouch over 7 years
    @David I have enlarged the explanation, per comments in the meta forum.
  • HiTechHiTouch
    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'
    G-Man Says 'Reinstate Monica' over 7 years
    I 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'
    G-Man Says 'Reinstate Monica' over 7 years
    And what if the OP wants to do something like echo "Here are the files in this directory:" *?
  • HiTechHiTouch
    HiTechHiTouch over 7 years
    Try 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
    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
    GeneralTao almost 7 years
    Hi, 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
    derek almost 7 years
    Wow, sorry I completely misread the question. Yes, @ only works when you are echoing in makefiles.
  • Shakaron
    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
    Ammar Bin Nasir over 5 years
    Correct 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 seems echo foo1 2>/dev/null, etc., would be just as effective, and more readable.
  • kenorb
    kenorb over 5 years
    Having 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
    Ammar Bin Nasir over 5 years
    Correct 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
    Grigory Entin about 4 years
    I 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'
    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 like cat, date and ls), … (Cont’d)
  • G-Man Says 'Reinstate Monica'
    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
    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'
    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 for echo, because echo 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'
    G-Man Says 'Reinstate Monica' about 4 years
  • G-Man Says 'Reinstate Monica'
    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
    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 for echo it's quite ok to use () in my case.
  • MoonLite
    MoonLite over 2 years
    Minor 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 from builtin echo "$*" to builtin 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 use builtin printf "$*" (and replace all echo with printf).
  • Admin
    Admin almost 2 years
    Tip (1): In some shells including bash you will need to say shopt -s expand_aliases in order to use aliases in a script. Tip (2): Change echo_and_restore to run_and_restore. Change the start of the function to local cmd="$1" then shift then $cmd "$*". Now you can create aliases for both echo and printf. Like this: alias echo='{ save_flags="$-"; set +x;} 2> /dev/null; run_and_restore "builtin echo"'
  • Admin
    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 a shopt -s expand_aliases command to get aliases to work in a script.” (2) Please learn how quoting works. Your code doesn’t work properly for printf, and it has the same problem with echo that my answer has (identified by MoonLite).
  • Admin
    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 "$@".