One really simple way to do it is by using an array or positional parameters.
Array-based solution :
# Build command
declare -a CMD_AND_ARGS=(command and args with normal quoting)
# Append arguments
CMD_AND_ARGS+=(more arguments quoted the normal way)
# Execute command
"${CMD_AND_ARGS[@]}"
Positional parameter-based solution:
# Create command
set -- command and args with normal quoting
# Append arguments
set -- "$@" more arguments quoted the normal way
# Execute command
"$@"
The nice thing about both solutions is you do not need to put quotes inside quotes, because expanding positional parameters or an array surrounded by double quotes does not cause word-splitting and expansion to be performed again.
Examples:
declare -a CMD=()
CMD=(ls "/dir/with spaces/in its name")
"$CMD"
set -- ls "/dir/with spaces/in its name"
"$@"
Note that in both cases, you get to build your command incrementally, for instance having conditional expressions (e.g. if/case) choosing to add different arguments depending on the flow of your script.
If you want to pipe a command to another, you will have to build each command separately (e.g. two arrays), as the |
symbol cannot be used inside an array declaration unquoted, and once quoted will be treated as a string argument and will not cause piping to occur.