Suppress execution trace for echo command?

29

5

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?

Christian

Posted 2014-09-03T09:22:08.273

Reputation: 511

Answers

21

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.

G-Man Says 'Reinstate Monica'

Posted 2014-09-03T09:22:08.273

Reputation: 6 509

2I 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' – 2016-11-01T03:20:55.310

dude, thanks for going deep! – Jan-Philip Gehrcke – 2019-12-13T14:39:58.620

12

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? :)

Christian

Posted 2014-09-03T09:22:08.273

Reputation: 511

2

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"

user5071535

Posted 2014-09-03T09:22:08.273

Reputation: 380

2

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

kenorb

Posted 2014-09-03T09:22:08.273

Reputation: 16 795

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. – Tyler Rick – 2018-12-10T19:33:34.187

Having tracing in your script could impact performance. Secondly redirecting &2 to NULL could be not the same, when you expect some other errors. – kenorb – 2018-12-10T22:15:45.683

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. – Tyler Rick – 2018-12-12T00:43:15.267

1

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() {
    echo "$(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".

You could also use cat - directly without the echo, but I like this way because it allows me to add any other strings to the output. For example, I tend to use it like this:

BASENAME="$(basename "$0")"  # Complete file name
...
echo "[${BASENAME}] $(cat -)"

And now it works beautifully:

false || echo "Command failed"
> [test.sh] Command failed

j1elo

Posted 2014-09-03T09:22:08.273

Reputation: 111

0

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.

Kamil Maciorowski

Posted 2014-09-03T09:22:08.273

Reputation: 38 429

0

In a Makefile you could use the @ symbol.

Example Usage: @echo 'message'

From this GNU doc:

When a line starts with ‘@’, the echoing of that line is suppressed. The ‘@’ is discarded before the line is passed to the shell. Typically you would use this for a command whose only effect is to print something, such as an echo command to indicate progress through the makefile.

derek

Posted 2014-09-03T09:22:08.273

Reputation: 11

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. – None – 2017-05-30T03:19:37.607

Wow, sorry I completely misread the question. Yes, @ only works when you are echoing in makefiles. – derek – 2017-06-08T15:57:05.760

@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. – Shakaron – 2017-11-14T01:47:48.223

-1

Editied 29 Oct 2016 per moderator suggestions that the original did not contain enough information to understand what is happening.

This "trick" produces just one line of message output at the terminal when xtrace is active:

The original question is Is there a way to leave -x enabled, but only output Message instead of the two lines. This is an exact quote from the question.

I understand the question to be, how to "leave set -x enabled AND produce a single line for a message"?

  • In the global sense, this question is basically about aesthetics -- the questioner wants to produce a single line instead of the two virtually duplicate lines produced while xtrace is active.

So in summary, the OP requires:

  1. To have set -x in effect
  2. Produce a message, human readable
  3. Produce only a single line of message output.

The OP does not require that the echo command be used. They cited it as an example of message production, using the abbreviation e.g. which stands for Latin exempli gratia, or "for example".

Thinking "outside the box" and abandoning the use of echo to produce messages, I note that an assignment statement can fulfill all requirements.

Do an assignment of a text string (containing the message) to an inactive variable.

Adding this line to a script doesn't alter any logic, yet produces a single line when tracing is active: (If you are using the variable $echo, just change the name to another unused variable)

echo="====================== Divider line ================="

What you will see on the terminal is only 1 line:

++ echo='====================== Divider line ================='

not two, as the OP dislikes:

+ echo '====================== Divider line ================='
====================== Divider line =================

Here is sample script to demonstrate. Note that variable substitution into a message (the $HOME directory name 4 lines from the end) works, so you can trace variables using this method.

#!/bin/bash -exu
#
#  Example Script showing how, with trace active, messages can be produced
#  without producing two virtually duplicate line on the terminal as echo does.
#
dummy="====================== Entering Test Script ================="
if [[ $PWD == $HOME ]];  then
  dummy="*** "
  dummy="*** Working in home directory!"
  dummy="*** "
  ls -la *.c || :
else
  dummy="---- C Files in current directory"
  ls -la *.c || :
  dummy="----. C Files in Home directory "$HOME
  ls -la  $HOME/*.c || :
fi

And here is the output from running it in the root, then the home directory.

$ cd /&&DemoScript
+ dummy='====================== Entering Test Script ================='
+ [[ / == /g/GNU-GCC/home/user ]]
+ dummy='---- C Files in current directory'
+ ls -la '*.c'
ls: *.c: No such file or directory
+ :
+ dummy='----. C Files in Home directory /g/GNU-GCC/home/user'
+ ls -la /g/GNU-GCC/home/user/HelloWorld.c /g/GNU-GCC/home/user/hw.c
-rw-r--r-- 1 user Administrators 73 Oct 10 22:21 /g/GNU-GCC/home/user/HelloWorld.c
-rw-r--r-- 1 user Administrators 73 Oct 10 22:21 /g/GNU-GCC/home/user/hw.c
+ dummy=---------------------------------

$ cd ~&&DemoScript
+ dummy='====================== Entering Test Script ================='
+ [[ /g/GNU-GCC/home/user == /g/GNU-GCC/home/user ]]
+ dummy='*** '
+ dummy='*** Working in home directory!'
+ dummy='*** '
+ ls -la HelloWorld.c hw.c
-rw-r--r-- 1 user Administrators 73 Oct 10 22:21 HelloWorld.c
-rw-r--r-- 1 user Administrators 73 Oct 10 22:21 hw.c
+ dummy=---------------------------------

HiTechHiTouch

Posted 2014-09-03T09:22:08.273

Reputation: 101

1Note, 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 – 2016-10-27T22:06:53.353

Note, this answer is being discussed on meta Have I been penalized for an answer 1 moderator didn't like?

– DavidPostill – 2016-10-27T22:07:54.390

@David I have enlarged the explanation, per comments in the meta forum. – HiTechHiTouch – 2016-10-30T04:53:07.723

@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, – HiTechHiTouch – 2016-10-30T04:55:42.950

And what if the OP wants to do something like echo "Here are the files in this directory:" *? – G-Man Says 'Reinstate Monica' – 2016-11-01T03:22:53.963

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 – 2016-11-09T11:12:01.720

@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". – HiTechHiTouch – 2016-11-09T11:16:15.617