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? T
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
}
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.
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
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 arithmeticfor
command, display the expanded value ofPS4
, followed by the command and its expanded arguments or associated word list.
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
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).