Search code examples
bashquoting

echo the exact command-line passed to a command with visible quotes


So I can quite happily build up a command line into a bash array, and then execute it with quotes and get each argument nicely quoted:

declare -a cmd_args
cmd_args=("-p" "dir path/with spaces")
mkdir "${cmd_args[@]}"
echo dir*/*

But how can I get this echoed to the screen in a way that makes sense - i.e. showing the user a command that they could then type, or that I could keep in a log file for future reference? All these look (basically) the same:

echo runnimg mkdir with arguments ${cmd_args[@]}
echo runnimg mkdir with arguments "${cmd_args[@]}"
echo "runnimg mkdir with arguments '${cmd_args[@]}'"
echo "runnimg mkdir with arguments '${cmd_args[*]}'"

==> runnimg mkdir with arguments '-p dir path/with spaces'

Which is clearly the wrong command. This is not showing the user a command that they could then type, or that I could keep in a log file and reproduce at a future date. I want to see:

runnimg mkdir with arguments '"-p" "dir path/with spaces"'

I thought about using cat<<EOF:

cat<<EOF 
"${cmd_args[@]}" 
EOF

but in fact, that generates one big quote around the whole argument list! What gives here? How could this ever be my intent? If it was, then I have "${cmd_args[*]}".

So that is the challenge. Print the command in a way that the user can say, "yes, that is the correct command".

Sorry to those saying "%p\n", but while that might be OK for a log file, but still a pain to go back and format it "unabiguously" in order to retest the command, it isn't really good enough for the interactive "this is the command feedback to the user".

Perhaps there is an answer already, but if so it is swamped by all the "always quote your arguments" type answers.


Solution

  • Based on BashFAQ/050:

    #!/bin/bash
    
    trap 'printf RUNNING:\ %s\\n "$BASH_COMMAND" >&2' DEBUG
    
    foo () {
        printf '%s\n' "$@" > /dev/null
    }
    
    foo bar baz
    
    foo 'qux bazinga' '1 2' 3 '4 5'
    
    foo "I'm going home"
    

    The DEBUG trap outputs:

    RUNNING: foo bar baz
    RUNNING: foo 'qux bazinga' '1 2' 3 '4 5'
    RUNNING: foo "I'm going home"
    

    Showing the quotes as they appear in the script.

    The function foo is set to output to /dev/null so we can ignore its output. It's just a stand-in for whatever commands your script may actually be running.

    Here is a version that puts the trap code in a function and calls that function to turn the trap on and off. This is useful if you only want the trap to handle certain sections of code:

    #!/bin/bash
    
    dbt () {
        if [[ $1 == on ]]
        then
            trap 'printf RUNNING:\ %s\\n "$BASH_COMMAND" >&2' DEBUG
        elif [[ $1 == off ]]
        then
            trap '' DEBUG
        else
            printf '%s\n' "Invalid action: $1"
        fi
    }
    
    foo () {
        printf '%s\n' "$@" > /dev/null
    }
    
    foo bar baz
    
    dbt on
    
    foo 'qux bazinga' '1 2' 3 '4 5'
    
    foo "I'm going home"
    
    now=$(date)
    
    dbt off
    
    declare -a array_b
    
    dbt on
    
    array_b=(a b 'c d' e)
    

    which outputs:

    RUNNING: foo 'qux bazinga' '1 2' 3 '4 5'
    RUNNING: foo "I'm going home"
    RUNNING: now=$(date)
    RUNNING: dbt off
    RUNNING: array_b=(a b 'c d' e)
    

    Another way to trace execution of a script is to use set -x.

    Print a trace of simple commands, for commands, case commands, select commands, and arithmetic for commands and their arguments or associated word lists after they are expanded and before they are executed. The value of the PS4 variable is expanded and the resultant value is printed before the command and its expanded arguments.