1254

In a shell script, how do I echo all shell commands called and expand any variable names?

For example, given the following line:

ls $DIRNAME

I would like the script to run the command and display the following

ls /full/path/to/some/dir

The purpose is to save a log of all shell commands called and their arguments. Is there perhaps a better way of generating such a log?

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
Jack Nock
  • 14,703
  • 5
  • 22
  • 18
  • See also (duplicate): [How can I print each command before executing?](https://stackoverflow.com/questions/5750450/how-can-i-print-each-command-before-executing) – Gabriel Staples May 24 '21 at 03:37

16 Answers16

1432

set -x or set -o xtrace expands variables and prints a little + sign before the line.

set -v or set -o verbose does not expand the variables before printing.

Use set +x and set +v to turn off the above settings.

On the first line of the script, one can put #!/bin/sh -x (or -v) to have the same effect as set -x (or -v) later in the script.

The above also works with /bin/sh.

See the bash-hackers' wiki on set attributes, and on debugging.

$ cat shl
#!/bin/bash                                                                     

DIR=/tmp/so
ls $DIR

$ bash -x shl 
+ DIR=/tmp/so
+ ls /tmp/so
$
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
Tom
  • 43,810
  • 29
  • 138
  • 169
  • 19
    If you also want to see which numbered line is being executed see https://stackoverflow.com/a/17805088/1729501 – user13107 Apr 19 '18 at 06:52
  • 9
    what if I want to color the command when echoing to differentiate the command and its results output ? – Lewis Chan Sep 03 '18 at 10:06
  • (The ABS has a long history of being unresponsive to requests that they correct bad-practice examples, to the point of driving longstanding community members to create competing resources; moved links over to the bash-hackers' wiki -- the Wooledge wiki or the [official bash manual](https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin) would also be better resources). – Charles Duffy Mar 09 '19 at 22:51
  • 2
    @LewisChan : you can add a colored static or dynamic prefix, f.e. timestamp, to your commands, see https://stackoverflow.com/a/62620480/2693875 . – Greg Dubicki Jun 28 '20 at 09:18
  • ...and if you are using iTerm2, @LewisChan, then for non-scripts you can color the commands differently than the outputs with this https://apple.stackexchange.com/a/252001 . – Greg Dubicki Jun 28 '20 at 09:19
  • 2
    @AndreasDietrich Thanks. You'd make my day if you actually found a way to implement my joke question. Preferably with some powerful cli tools and only a few lines of code. – ADJenks Sep 10 '20 at 02:38
  • 3
    `bash -x foo.sh` was the crux of what I needed. (Posting as a comment because it wasn't immediately apparent whether that would work without modifying the script itself; it did. ) – ijoseph Sep 30 '20 at 21:29
  • `set -o verbose` does nothing for me (Arch Linux where `sh --version` yields `GNU bash, version 5.1.8`) – xeruf Jun 22 '21 at 18:10
  • The `+ ` prefix can be disabled with `PS4="\000"`. By default this output goes to stderr. To output to stdout, use `BASH_XTRACEFD=1`. – Martin Jul 25 '22 at 18:37
  • Note that the bash-hackers' wiki no longer exists. – Opus4210 Jun 13 '23 at 17:00
435

set -x will give you what you want.

Here is an example shell script to demonstrate:

#!/bin/bash
set -x #echo on

ls $PWD

This expands all variables and prints the full commands before output of the command.

Output:

+ ls /home/user/
file1.txt file2.txt
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
radman
  • 17,675
  • 11
  • 42
  • 58
  • 27
    Using the word "verbose" that way doesn't accomplish anything. You can do `set -o verbose` or `set -v` (only "verbose") or `set -o xtrace` or `set -x` (only "xtrace") or `set -xv` (both) or `set -o xtrace -o verbose` (both). – Dennis Williamson May 18 '10 at 01:03
  • this works good, but be aware that the "verbose" overwrites $1 – JasonS Jun 13 '13 at 02:04
134

I use a function to echo and run the command:

#!/bin/bash
# Function to display commands
exe() { echo "\$ $@" ; "$@" ; }

exe echo hello world

Which outputs

$ echo hello world
hello world

For more complicated commands pipes, etc., you can use eval:

#!/bin/bash
# Function to display commands
exe() { echo "\$ ${@/eval/}" ; "$@" ; }

exe eval "echo 'Hello, World!' | cut -d ' ' -f1"

Which outputs

$  echo 'Hello, World!' | cut -d ' ' -f1
Hello
wheeler
  • 2,823
  • 3
  • 27
  • 43
Soth
  • 2,901
  • 2
  • 27
  • 27
  • 1
    Not many votes for this answer. Is there a reason it's a bad idea? Worked for me, and seems to be exactly what I'm looking for... – fru1tbat Jul 22 '14 at 14:03
  • 3
    This is the best answer if you don't want every command printed. It avoids the `++ set +x` output when turned off, as well as looking cleaner. For just a single statement or two, though, bhassel's answer using a subshell is the most convenient. – Brent Faust Jan 09 '15 at 15:29
  • This is what I'm looking for! Not `set +x`, it affects all commands, which is too much! – Hlung May 10 '15 at 16:29
  • 19
    A major downside to this is that the output loses the quoting information. You can't differentiate between `cp "foo bar" baz` and `cp foo "bar baz"`, for example. So it's good for displaying progress information to a user; less so for debugging output or recording reproducible commands. Different use cases. In `zsh`, you can preserve quoting with the `:q` modifier: `exe() { echo '$' "${@:q}" ; "$@" ; }` – Andrew Janke Sep 16 '15 at 19:52
  • This does not seem to work with shell builtins, e.g. ```exe foo=bar; echo $foo``` will not work. – mdd Aug 19 '16 at 19:50
  • It seems to me a great solution. It can eventually be integrated with a `| tee -a $LOGFILE` to printout do std output and the log file as well. – liv913 Mar 12 '17 at 17:29
  • So the major downside above is only related to the output right? The actual command formatting for what is actually executed is not affected? – redfox05 May 17 '17 at 19:00
  • 1
    I love this solution and have been using it for a while. However, it seems to fail at complex commands: e.g. `exe (cut -d ' ' -f9,10 --complement ${folder}/hog_crop_px.txt) | paste - ${folder}/lbp_crop_px.txt > ${folder}/crop_px.tx` gives me `syntax error near unexpected token `cut'` even though it executes perfectly without the exe() command. Any suggestion? – penelope Oct 06 '17 at 15:58
  • @penelope you could use `exe eval"(cut -d ' ' -f9,10 --complement ${folder}/hog_crop_px.txt) | paste - ${folder}/lbp_crop_px.txt > ${folder}/crop_px.tx"` ----- OR ----- `exe cut -d ' ' -f9,10 --complement ${folder}/hog_crop_px.txt | paste - ${folder}/lbp_crop_px.txt > ${folder}/crop_px.tx` – Soth Oct 08 '17 at 01:41
  • Just modified the program so that `exe eval 'foo=bar; echo $foo'` will now work – Soth Oct 08 '17 at 02:31
  • @Soth I am trying your new function, and it seems like it should work okay with `eval`. However, I'm sure I've tried using your original function with and without brackets before I posted here and your second example was not working. – penelope Oct 26 '17 at 13:30
  • 3
    I don't like this answer. There are lots of edge cases where what you see is not what you get (especially with whitespace, quotes, escaped characters, variable/expression substitutions, etc), so don't blindly paste the echoed command into a terminal and assume it will run the same way. Also, the second technique is just a hack, and will strip out other instances of the word `eval` from your command. So don't expect it to work properly on `exe eval "echo 'eval world'"`! – mwfearnley Nov 27 '18 at 13:29
  • 1
    a drawback is that you cannot use this exe function in subshell commands like: `VAR=$(exe echo "hello world"); echo $VAR` The value of $VAR will be the echoed command plus the result of the command. like: 'echo hello world hello world' – Sebastian Thees Mar 05 '20 at 16:18
  • I have not tested it but I think there is a drawback to it. If your command had something like "echo $RANDOM" where $RANDOM is a function to get a random number in bash. Your echo output and real command will be different. – Shubham Rohila Feb 03 '22 at 11:24
  • No matter what I tried, I could not get the double space after the `$` to be a single space using variable substitution. So instead I used `sed`: `run() { echo "$" "$(echo "$@" | sed -e 's/eval //' )"; "$@"; }` – wheeler Feb 09 '23 at 22:21
  • The last output is wrong, should be 'Hello,' Otherwise you should split on comma ',' – Jebiel Mar 21 '23 at 13:35
102

You can also toggle this for select lines in your script by wrapping them in set -x and set +x, for example,

#!/bin/bash
...
if [[ ! -e $OUT_FILE ]];
then
   echo "grabbing $URL"
   set -x
   curl --fail --noproxy $SERV -s -S $URL -o $OUT_FILE
   set +x
fi
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
shuckc
  • 2,766
  • 1
  • 22
  • 17
80

shuckc's answer for echoing select lines has a few downsides: you end up with the following set +x command being echoed as well, and you lose the ability to test the exit code with $? since it gets overwritten by the set +x.

Another option is to run the command in a subshell:

echo "getting URL..."
( set -x ; curl -s --fail $URL -o $OUTFILE )

if [ $? -eq 0 ] ; then
    echo "curl failed"
    exit 1
fi

which will give you output like:

getting URL...
+ curl -s --fail http://example.com/missing -o /tmp/example
curl failed

This does incur the overhead of creating a new subshell for the command, though.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
bhassel
  • 901
  • 6
  • 4
47

According to TLDP's Bash Guide for Beginners: Chapter 2. Writing and debugging scripts:

2.3.1. Debugging on the entire script

$ bash -x script1.sh

...

There is now a full-fledged debugger for Bash, available at SourceForge. These debugging features are available in most modern versions of Bash, starting from 3.x.

2.3.2. Debugging on part(s) of the script

set -x            # Activate debugging from here
w
set +x            # Stop debugging from here

...

Table 2-1. Overview of set debugging options

    Short  | Long notation | Result
    -------+---------------+--------------------------------------------------------------
    set -f | set -o noglob | Disable file name generation using metacharacters (globbing).
    set -v | set -o verbose| Prints shell input lines as they are read.
    set -x | set -o xtrace | Print command traces before executing command.

...

Alternatively, these modes can be specified in the script itself, by adding the desired options to the first line shell declaration. Options can be combined, as is usually the case with UNIX commands:

#!/bin/bash -xv
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Afriza N. Arief
  • 7,696
  • 5
  • 47
  • 74
38

Another option is to put "-x" at the top of your script instead of on the command line:

$ cat ./server
#!/bin/bash -x
ssh user@server

$ ./server
+ ssh user@server
user@server's password: ^C
$
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
nooj
  • 578
  • 5
  • 13
  • Note that this doesn't seem to work exactly the same between `./myScript` and `bash myScript`. Still a good thing to point out, thanks. – altendky Apr 16 '15 at 14:08
37

You can execute a Bash script in debug mode with the -x option.

This will echo all the commands.

bash -x example_script.sh

# Console output
+ cd /home/user
+ mv text.txt mytext.txt

You can also save the -x option in the script. Just specify the -x option in the shebang.

######## example_script.sh ###################
#!/bin/bash -x

cd /home/user
mv text.txt mytext.txt

##############################################

./example_script.sh

# Console output
+ cd /home/user
+ mv text.txt mytext.txt
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
RenRen
  • 10,550
  • 4
  • 37
  • 39
  • 3
    Also `bash -vx` will do the same but without variable interpolation – loa_in_ Jul 25 '18 at 13:09
  • 1
    This is nice, but a bit more hardcore than I wanted. It seems to "descend" into all the commands run by my top-level script. I really just wanted the commands of my top-level script to be echoed, not absolutely everything bash runs. – Ben Farmer Oct 28 '20 at 05:13
22

Type "bash -x" on the command line before the name of the Bash script. For instance, to execute foo.sh, type:

bash -x foo.sh
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Alan
  • 1,889
  • 2
  • 18
  • 30
22

Combining all the answers I found this to be the best, simplest

#!/bin/bash
# https://stackoverflow.com/a/64644990/8608146
exe(){
    set -x
    "$@"
    { set +x; } 2>/dev/null
}
# example
exe go generate ./...

{ set +x; } 2>/dev/null from https://stackoverflow.com/a/19226038/8608146

If the exit status of the command is needed, as mentioned here

Use

{ STATUS=$?; set +x; } 2>/dev/null

And use the $STATUS later like exit $STATUS at the end

A slightly more useful one

#!/bin/bash
# https://stackoverflow.com/a/64644990/8608146
_exe(){
    [ $1 == on  ] && { set -x; return; } 2>/dev/null
    [ $1 == off ] && { set +x; return; } 2>/dev/null
    echo + "$@"
    "$@"
}
exe(){
    { _exe "$@"; } 2>/dev/null
}

# examples
exe on # turn on same as set -x
echo This command prints with +
echo This too prints with +
exe off # same as set +x
echo This does not

# can also be used for individual commands
exe echo what up!
Phani Rithvij
  • 4,030
  • 3
  • 25
  • 60
8

For zsh, echo

setopt VERBOSE

And for debugging,

setopt XTRACE
Gama11
  • 31,714
  • 9
  • 78
  • 100
zzapper
  • 4,743
  • 5
  • 48
  • 45
  • Thanks! [This is interesting.](https://linux.die.net/man/1/zshoptions) `verbose` can be in lowercase. To remove it, use `unsetopt` instead of `setopt`. Example: `unsetopt verbose`. PS: This doesn't work in scripts though. – ADTC Aug 20 '23 at 14:50
2

To allow for compound commands to be echoed, I use eval plus Soth's exe function to echo and run the command. This is useful for piped commands that would otherwise only show none or just the initial part of the piped command.

Without eval:

exe() { echo "\$ $@" ; "$@" ; }
exe ls -F | grep *.txt

Outputs:

$
file.txt

With eval:

exe() { echo "\$ $@" ; "$@" ; }
exe eval 'ls -F | grep *.txt'

Which outputs

$ exe eval 'ls -F | grep *.txt'
file.txt
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
1

For csh and tcsh, you can set verbose or set echo (or you can even set both, but it may result in some duplication most of the time).

The verbose option prints pretty much the exact shell expression that you type.

The echo option is more indicative of what will be executed through spawning.


http://www.tcsh.org/tcsh.html/Special_shell_variables.html#verbose

http://www.tcsh.org/tcsh.html/Special_shell_variables.html#echo


Special shell variables

verbose If set, causes the words of each command to be printed, after history substitution (if any). Set by the -v command line option.

echo If set, each command with its arguments is echoed just before it is executed. For non-builtin commands all expansions occur before echoing. Builtin commands are echoed before command and filename substitution, because these substitutions are then done selectively. Set by the -x command line option.

cnst
  • 25,870
  • 6
  • 90
  • 122
0
$ cat exampleScript.sh
#!/bin/bash
name="karthik";
echo $name;

bash -x exampleScript.sh

Output is as follows:

enter image description here

0

Following the answer of Soth.

It is possible to create a markdown output without highlight (no language given)

set -x
exe() { echo "\`\$\$  ${@/eval/} \`" ; "$@" ; }

script

set -x
exe() { echo "\`\$\$  ${@/eval/} \`" ; "$@" ; }
echo
echo -----------------------------------------------------------
echo # Setup
echo Lets take a random keyframe from in.mp4:
echo
exe eval "kfn=20"
echo
echo "kf=\$(ffprobe -v error -select_streams v -show_frames -print_format csv in.mp4 | grep 'frame,video,0,1' | head -$kfn | tail -1 | perl -pe 's|frame,video,0,1,.*?,(.*?),.*|\1|') "
exe eval "kf=$(ffprobe -v error -select_streams v -show_frames -print_format csv in.mp4 | grep 'frame,video,0,1' | head -$kfn | tail -1 | perl -pe 's|frame,video,0,1,.*?,(.*?),.*|\1|') "
echo
echo Lets select keyframe at $kf. Here are the timestamps of the all the frames from in.mp4 around this keyframe.
echo
exe eval "ffprobe -v error -select_streams v -show_frames -print_format csv in.mp4  | perl -pe 's|frame,video,0,(.*?),.*?,(.*?),.*|\2  \1|' | perl -pe 's|(.*?)  1|\1\tKF|' |  perl -pe 's|(.*?)  0|\1|' |grep -A 5 -B 5 --color $kf"
echo
echo Lets compare 2 methods of split: actual losslesscut 3.53.0 and another one
echo

Output


Lets take a random keyframe from in.mp4:

$$ kfn=20

kf=$(ffprobe -v error -select_streams v -show_frames -print_format csv in.mp4 | grep 'frame,video,0,1' | head -20 | tail -1 | perl -pe 's|frame,video,0,1,.?,(.?),.*|\1|')

$$ kf=3.803792

Lets select keyframe at 3.803792. Here are the timestamps of the all the frames from in.mp4 around this keyframe.

$$ ffprobe -v error -select_streams v -show_frames -print_format csv in.mp4 | perl -pe 's|frame,video,0,(.*?),.*?,(.*?),.*|\2 \1|' | perl -pe 's|(.*?) 1|\1\tKF|' | perl -pe 's|(.*?) 0|\1|' |grep -A 5 -B 5 --color 3.803792

3.720375
3.737083
3.753750
3.770417
3.787125
**3.803792**   KF
3.820500
3.837167
3.853833
3.870542
3.887208
Dorian Grv
  • 421
  • 5
  • 9
0

If you want to log only specific commands, rather than every single line, set -x is somewhat difficult to use. But you can use this function to print any command, prefixed with $ :

function run() {
    echo -n '$'  # Print $ without a newline.
    for arg in "$@"; do
      printf " %q" "$arg"  # Print $arg, properly escaped.
    done
    echo  # Print a newline.
    "$@"
}

Usage:

run ls /full/path/to/some/dir

Using printf with the %q format specifier makes sure that the output is printed in a way suitable for copying and pasting into a shell, even in the presence of spaces or other special characters. It seems to escape spaces using \ rather than adding quotes, but the output does correctly represent what's actually being run.

Thomas
  • 174,939
  • 50
  • 355
  • 478