Echo command, and then run it? (Like make)

2019-03-18 07:24发布

问题:

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

回答1:

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


回答2:

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).



回答3:

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
}


回答4:

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.



回答5:

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.


回答6:

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


回答7:

Create executable(+x) base script named as "echo_and_run" with below mentioned simple shell script!

#!/bin/bash
echo "$1"
$1

$ ./echo_and_run "echo Hello, World"

echo Hello, World
Hello, World

However, cnicutar's approch to set -x is reliable and strongly recommended.