In my bash scripts, I often execute commands stored in an array to handle arguments with spaces correctly:
declare -a mycmd=( "git" "commit" "-m" "A commit message with spaces" )
"${mycmd[@]}"
When a command fails, I want to print the exact, copy-paste-runnable command that was attempted for easy debugging.
My problem is that when I try to print the array, the quoting is lost, and the resulting string is not runnable if it contains arguments with spaces. Minimal Reproducible Example:
Here is a simple script that demonstrates the problem: Bash
#!/bin/bash
# 1. Define an array with a spaced argument
declare -a myargs=("touch" "file1" "file2" "a file with spaces")
# 2. Execute the command - this works perfectly
"${myargs[@]}" || echo "Execution failed"
# 3. Now, try to print the command that was run
echo "The command was: ${myargs[@]}"
Actual vs. Desired Output:
When I run ls after the script, I can see the files were created correctly, so the "${myargs[@]}" expansion worked. Shell
$ ls
'a file with spaces' file1 file2
However, the output from the echo command is not what I need.
Actual output from echo:
The command was: touch file1 file2 a file with spaces
If I copy and paste this output, it will fail because a file with spaces is treated as four separate arguments.
Desired output from echo (either of these would be acceptable):
The command was: touch file1 file2 "a file with spaces"The command was: touch 'file1' 'file2' 'a file with spaces'What is a compact and reliable way to transform the myargs array back into a correctly quoted/escaped string that can be run directly from the shell?
Your problem is with echo. It is getting the correct number of parameters, with some parameters containing spaces, but it's output loses the distinction of spaces between parameters and spaces within parameters.
Instead, you can use printf(1) to output the parameters and always include quotes, making use of printf's feature that applies the format string successively to parameters when there are more parameters than format specifiers in the format string:
echo "Failed: foo:" $(printf "'%s' " "${mycmd[@]}")
That will put single quotes around each argument, even if it is not needed:
Failed: foo: 'command.ext' 'arg1 with space' 'arg2' 'thing' 'etc'
I've used single quotes to ensure that other shell metacharacters are not mishandled. This will work for all characters except single quote itself - i.e. if you have a parameter containing a single quote, the output from the above command will not cut and paste correctly. This is likely the closest you will get without getting messy.
Edit: Almost 5 years later and since I answered this question, bash 4.4 has been released. This has the "${var@Q}" expansion which quotes the variable such that it can be parsed back by bash.
This simplifies this answer to:
echo "Failed: foo: " "${mycmd[@]@Q}"
This will correctly handle single quotes in an argument, which my earlier version did not.