23

Is there some way to get bash into a sort of verbose mode where, such that, when it's running a shell script, it echoes out the command it's going to run before running it? That is, so that it's possible to see the commands that were run (as well as their output), similar to the output of make?

That is, if running a shell script like

echo "Hello, World"

I would like the following output

echo "Hello, World"
Hello, World

Alternatively, is it possible to write a bash function called echo_and_run that will output a command and then run it?

$ echo_and_run echo "Hello, World"
echo "Hello, World"
Hello, World
mjs
  • 63,493
  • 27
  • 91
  • 122

6 Answers6

38

You could make your own function to echo commands before calling eval.

Bash also has a debugging feature. Once you set -x bash will display each command before executing it.

cnicutar@shell:~/dir$ set -x
cnicutar@shell:~/dir$ ls
+ ls --color=auto
a  b  c  d  e  f
cnicutar
  • 178,505
  • 25
  • 365
  • 392
  • 1
    +1 and also, if it's not obvious, `set -x` can also go in shell scripts and will last the length of the script. – jedwards Sep 02 '12 at 03:13
  • This does seem like the best, most reliable approach. (And also works with redirection, which I was prepared to not worry about, though it's nice to have.) The only problem is `set +x` commands appear in the output as well... Also discovered this: [I want a log of my script's actions](http://mywiki.wooledge.org/BashFAQ/050). – mjs Sep 02 '12 at 07:35
  • 3
    @mjs: If you want the command echo-ing for the whole runtime of your script, you can run it via `bash -x` (which enables `set -x` implicitly and does not output it). – Michał Górny Sep 02 '12 at 21:24
  • @MichałGórny Thanks, "bash -cx ..." works almost perfectly; it only fails to solve the quoted argument problem. But, close enough! – mjs Sep 12 '13 at 10:55
  • 4
    You can also do `( set -x ; ls ; echo blah )` which might be helpful in some situations. (The `set -x` is reverted when the subshell exits.) – mjs Jun 03 '14 at 15:24
  • See `set -v`trace each operation but not inside loop. – F. Hauri - Give Up GitHub Mar 23 '19 at 22:44
25

To answer the second part of your question, here's a shell function that does what you want:

echo_and_run() { echo "$*" ; "$@" ; }

I use something similar to this:

echo_and_run() { echo "\$ $*" ; "$@" ; }

which prints $ in front of the command (it looks like a shell prompt and makes it clearer that it's a command). I sometimes use this in scripts when I want to show some (but not all) of the commands it's executing.

As others have mentioned, it does lose quotation marks:

$ echo_and_run echo "Hello, world"
$ echo Hello, world
Hello, world
$ 

but I don't think there's any good way to avoid that; the shell strips quotation marks before echo_and_run gets a chance to see them. You could write a script that would check for arguments containing spaces and other shell metacharacters and add quotation marks as needed (which still wouldn't necessarily match the quotation marks you actually typed).

Keith Thompson
  • 254,901
  • 44
  • 429
  • 631
  • Is there a way to make this handle multiple commands on a single line? I.e `echo_and_run cd .. && ls` runs both commands, but only outputs `$ cd ..` – MrTheWalrus May 01 '15 at 17:26
  • 1
    @MrTheWalrus: That's because only `cd ..` is passed as arguments to the `echo_and_run()` function. It's like typing `echo_and_run cd ..` followed by `ls`. The trick is to feed it to `echo_and_run` *as a single command*: `echo_and_run sh -c 'cd .. && ls'`. Replace `/bin/sh` by `/bin/bash` if you need bash-specific features (or ksh, or zsh, or ...) – Keith Thompson May 01 '15 at 17:31
  • Thanks. I could tell what the problem was, but had no idea how to make it treat the whole line as a single command. – MrTheWalrus May 01 '15 at 17:38
  • It does seem that this doesn't allow overriding variables, such as LD_LIBRARY_PATH. It evaluates everything like a command – Txangel Jun 09 '21 at 19:09
  • 1
    @Txangel Yes, and you can set environment variables in a command. For example, `sh -c 'LD_LIBRARY_PATH=/some/path some_command'` – Keith Thompson Jun 09 '21 at 20:03
10

It's possible to use bash's printf in conjunction with the %q format specifier to escape the arguments so that spaces are preserved:

function echo_and_run {
  echo "$" "$@"
  eval $(printf '%q ' "$@") < /dev/tty
}
mjs
  • 63,493
  • 27
  • 91
  • 122
2

To add to others' implementations, this is my basic script boilerplate, including argument parsing (which is important if you're toggling verbosity levels).

#!/bin/sh

# Control verbosity
VERBOSE=0

# For use in usage() and in log messages
SCRIPT_NAME="$(basename $0)"

ARGS=()

# Usage function: tells the user what's up, then exits.  ALWAYS implement this.
# Optionally, prints an error message
# usage [{errorLevel} {message...}
function usage() {
    local RET=0
    if [ $# -gt 0 ]; then
        RET=$1; shift;
    fi
    if [ $# -gt 0 ]; then
        log "[$SCRIPT_NAME] ${@}"
    fi
    log "Describe this script"
    log "Usage: $SCRIPT_NAME [-v|-q]" # List further options here
    log "   -v|--verbose    Be more verbose"
    log "   -q|--quiet      Be less verbose"
    exit $RET
}

# Write a message to stderr
# log {message...}
function log() {
    echo "${@}" >&2
}

# Write an informative message with decoration
# info {message...}
function info() {
    if [ $VERBOSE -gt 0 ]; then
        log "[$SCRIPT_NAME] ${@}"
    fi
}

# Write an warning message with decoration
# warn {message...}
function warn() {
    if [ $VERBOSE -gt 0 ]; then
        log "[$SCRIPT_NAME] Warning: ${@}"
    fi
}

# Write an error and exit
# error {errorLevel} {message...}
function error() {
    local LEVEL=$1; shift
    if [ $VERBOSE -gt -1 ]; then
        log "[$SCRIPT_NAME] Error: ${@}"
    fi
    exit $LEVEL
}

# Write out a command and run it
# vexec {minVerbosity} {prefixMessage} {command...}
function vexec() {
    local LEVEL=$1; shift
    local MSG="$1"; shift
    if [ $VERBOSE -ge $LEVEL ]; then
        echo -n "$MSG: "
        local CMD=( )
        for i in "${@}"; do
            # Replace argument's spaces with ''; if different, quote the string
            if [ "$i" != "${i/ /}" ]; then
                CMD=( ${CMD[@]} "'${i}'" )
            else
                CMD=( ${CMD[@]} $i )
            fi
        done
        echo "${CMD[@]}"
    fi
    ${@}
}

# Loop over arguments; we'll be shifting the list as we go,
# so we keep going until $1 is empty
while [ -n "$1" ]; do
    # Capture and shift the argument.
    ARG="$1"
    shift
    case "$ARG" in
        # User requested help; sometimes they do this at the end of a command
        # while they're building it.  By capturing and exiting, we avoid doing
        # work before it's intended.
        -h|-\?|-help|--help)
            usage 0
            ;;
        # Make the script more verbose
        -v|--verbose)
            VERBOSE=$((VERBOSE + 1))
            ;;
        # Make the script quieter
        -q|--quiet)
            VERBOSE=$((VERBOSE - 1))
            ;;
        # All arguments that follow are non-flags
        # This should be in all of your scripts, to more easily support filenames
        # that start with hyphens.  Break will bail from the `for` loop above.
        --)
            break
            ;;
        # Something that looks like a flag, but is not; report an error and die
        -?*)
            usage 1 "Unknown option: '$ARG'" >&2
            ;;
        #
        # All other arguments are added to the ARGS array.
        *)
            ARGS=(${ARGS[@]} "$ARG")
            ;;
    esac
done
# If the above script found a '--' argument, there will still be items in $*;
# move them into ARGS
while [ -n "$1" ]; do
    ARGS=(${ARGS[@]} "$1")
    shift
done

# Main script goes here.

Later...

vexec 1 "Building myapp.c" \
    gcc -c myapp.c -o build/myapp.o ${CFLAGS}

Note: This will not cover piped commands; you need to bash -c those sorts of things, or break them up into intermediate variables or files.

Fordi
  • 2,798
  • 25
  • 20
2

For extra timestamps and I/O info, consider the annotate-output command from Debian's devscripts package:

annotate-output echo hello

Output:

13:19:08 I: Started echo hello
13:19:08 O: hello
13:19:08 I: Finished with exitcode 0

Now look for a file that doesn't exist, and note the E: for STDERR output:

annotate-output ls nosuchfile

Output:

13:19:48 I: Started ls nosuchfile
13:19:48 E: ls: cannot access 'nosuchfile': No such file or directory
13:19:48 I: Finished with exitcode 2
agc
  • 7,973
  • 2
  • 29
  • 50
1

Two useful shell options that can be added to the bash command line or via the set command in a script or interactive session:

  • -v Print shell input lines as they are read.
  • -x After expanding each simple command, for command, case command, select command, or arithmetic for command, display the expanded value of PS4, followed by the command and its expanded arguments or associated word list.
Toby Speight
  • 27,591
  • 48
  • 66
  • 103