Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Colored xtrace output

Tags:

bash

shell

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.

like image 217
bergdev Avatar asked Sep 26 '14 20:09

bergdev


2 Answers

The Gist

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.

Explanation

The way it does it is as follows:

  • Insert $UNIQUE_STR to the output
  • Route all output - stdout and stderr - through a command (exec &> >(...))
  • The command (sed) assumes the trace is of the form +++${UNIQUE_STR} <some interesting trace>
  • Find lines with $UNIQUE_STR (s/...\(${UNIQUE_STR}\).../) and remove it (/...\1+\3.../, where \1 is ++ and \3 is <some interesting trace>)
  • If the line does containe $UNIQUE_STR, tell the terminal to set the coloring to red before the line, and to unset coloring after the line (/$(colored)...$(normal)/).

What doesn't work

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.

like image 92
root Avatar answered Nov 07 '22 14:11

root


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.

like image 20
dimo414 Avatar answered Nov 07 '22 16:11

dimo414