I have a .sh-script that uses set -o xtrace
to print all following commands.
I want to color these commands. I tried to use the PS4 variable like this:
export PS4='\[\e[36m\]\+ \[\e[m\]'
But this colors only the +
-character and if i leave out \[\e[m\]
my complete output and the output of the executed program is colored.
Is there another variable that is appended after the printed command where I can reset the color or is there another way.
Thanks.
Because of the way terminal coloring works, combined with the say bash tracing works, it's pretty ugly to implement, but here's how you do it:
First, you need to find some unique string that will appear neither in the trace, nor in stdout, nor in stderr. In my example, it's @@@
.
Then, if your script is called example.sh
, you run
$ ./color_trace.sh example.sh
Where ./color_trace.sh
is this script:
#!/bin/bash
RED=1
UNIQUE_STR='@@@'
COLOR=$RED
normal()
{
tput sgr0 # tell the terminal to not style the output
}
colored()
{
tput setaf $COLOR # tell the terminal to set forward color to $COLOR
}
export PS4="+${UNIQUE_STR}" # artificially insert $UNIQUE_STR to the trace
exec &> >(sed "s/\(\+\)*\+\(${UNIQUE_STR}\)\(.*\)$/$(colored)\1+\3$(normal)/") # see below
set -x # set trace
source "$@" # run the commands in the current shell so that all settings apply
This will color trace commands red.
The way it does it is as follows:
$UNIQUE_STR
to the outputexec &> >(...)
)sed
) assumes the trace is of the form +++${UNIQUE_STR} <some interesting trace>
$UNIQUE_STR
(s/...\(${UNIQUE_STR}\).../
) and remove it (/...\1+\3.../
, where \1
is ++
and \3
is <some interesting trace>
)$UNIQUE_STR
, tell the terminal to set the coloring to red before the line, and to unset coloring after the line (/$(colored)...$(normal)/
).First, the functions colored()
and normal()
don't accept a piece of text to color. Instead, they instruct the terminal to set the color to whatever is going to be written next, and after it's written, instruct it to unset coloring. This is just how coloring, or any kind of styling, works in Unix terminals.
Second, The trace goes to stderr, so it will be mixed with other output of stderr. This could be changed using BASH_XTRACEFD
, but it wouldn't work well here.
Since you want coloring, it is assumed that you want to differentiate the trace from other output, so it is assumed that all the output, including stdout, stderr, and the trace, is going to the terminal.
If everything is going to the terminal, then in order for the trace to be properly interleaved with the actual output, all output - stdout, stderr, and the trace - need to be executed on the same file descriptor. That's why we have to redirect everything through sed
, not just the trace. If we only redirected the trace, then you'll get a bunch of output from various commands, and only afterwards you'll see all the traces for these commands, e.g.
... actual output from /etc/passwd...
... actual output from /etc/group...
+source ./script.sh
++cat_file /etc/passwd
++cat /etc/passwd
++cat_file /etc/group
++cat /etc/group
If it weren't for the file descriptor problem, we wouldn't need UNIQUE_STR
, we could have just used something like:
exec 19> >(sed "s/^.*$/$(colored)&$(normal)/")
BASH_XTRACEFD=19
set -x
source "$@"
It's worth noting that even in this version we would have to turn colors on and off every line, because lines of "real output" need to be uncolored.
The introduction of PS0
in Bash 4.4 almost allows us to do what you need; PS0='\e[0m'
writes a reset code to the terminal before each command runs, but unfortunately it's printed before, not after, PS4
is printed, so that's no good. As others have said I don't think you can do what you want today with bash; PS4
simply isn't an expressive enough hook to properly color the command trace, and there's no other hook that fires at the right time.
Depending on your exact goal, you may be able to use the DEBUG
trap instead of -x
. It doesn't perfectly replicate -x
, but we can get a similar effect.
$ debug() {
# print a '+' for every element in BASH_LINENO, similar to PS4's behavior
printf '%s' "${BASH_LINENO[@]/*/+}"
# Then print the current command, colored
printf ' \e[36m%s\e[0m\n' "$BASH_COMMAND"
}
$ trap debug DEBUG
$ shopt -s extdebug # necessary for the DEBUG trap to carry into functions
This mostly works, though the debug trap doesn't exactly mirror -x
's behavior, so the output is a little different. Here's an example:
$ foo() { bar "$@"; }
$ bar() { printf '%s\n' "$@" | grep baz; }
$ foo biff bang baz
+ foo biff bang baz
++ foo biff bang baz
++ bar "$@"
+++ bar "$@"
+++ printf '%s\n' "$@"
+++ grep baz
baz
I'm not immediately certain why the function calls foo
and bar
print twice, though I think the second entries are the DEBUG trap entering the function, which we might be able to detect and suppress somehow, e.g. by inspecting FUNCNAME
or BASH_LINENO
. I'll update this answer if I work that out.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With