Why
Quoting bash manual
Expansion is performed on the command line after it has been split into tokens. There are seven kinds of expansion performed:
- brace expansion
- tilde expansion
- parameter and variable expansion
- command substitution
- arithmetic expansion
- word splitting
- filename expansion
So typing this
command="docker exec f0a57e0592e5 sh -c 'consul kv export > /consul/data/backup.json'"
does what you think it does. Double-quotes prevent spiting. So it is just command=something
, with no other tokens. So far so good.
Now, when you type
$command
$command
is expanded into
docker exec f0a57e0592e5 sh -c 'consul kv export > /consul/data/backup.json'
And this line is processed with the remaining of the expansion chain. But not restarting the whole evaluation chain.
So, after variable substitution comes command substitution ($(...)
or `...`
). There is none of that.
Then arithmetic expansion $((...))
. None neither.
Then word slitting.
So we spit into words, that are docker
exec
f0a57e0592e5
sh
-c
'consul
kv
export
>
/consul/data/backup.json'
The quotes here are not interpreted as preventing splitting. It is too late for finding the quotes. That happens at "tokens" phase, the very first one. That was done before those quotes were there. So those quotes are just chars here.
And so docker
is executed with the said arguments. Including one 'consul
argument. That does not what you expect.
Experiments
If you had
command="printf (%s)"
Then
$command 1 2 3
gives (1)(2)(3)
While
$command "1 2 3"
gives (1 2 3)
As expected, I think. Because "
were there at the first stage, token splitting, and interpreted to prevent, later, word splittng.
But try this variant
command="printf (%s) 1 2"
$command 3 4 5
Still nothing strange → (1)(2)(3)(4)(5)
command="printf (%s) '1 2'"
$command '3 4 5'
→ ('1)(2')(3 4 5)
Which may be strange. But it's normal. See what happens:
$command '3 4 5'
tokens $command
(variable reference) 3\ 4\ 5
(string — spaces in it are found to be literal spaces by tokenisation, because surrounded by quotes. I represent that with \
) are found
- no brace, no tilde to expand
- we substitute variable, so now line to evaluate is
printf (%s) '1 2' 3\ 4\ 5
- no
$(...)
no $((...)
to expand
- split into words:
printf
(%s)
'1
2'
3 4 5
. Note that '
at this stages are just characters. It is too late for them to influence tokenization, as the ones around 3 4 5
did. So space between 1
and 2
does split words, contrarily to the ones between 3
4
and 5
that were transformed into literal spaces by tokenization, because of the presence of the '
.
- exec
- →
('1)(2')(3 4 5)
Exactly what we got. Even the strangest result are completely predictable when we understand what exactly occurs to our command.
Solution
You could use the infamous eval
command="eval docker exec f0a57e0592e5 sh -c 'consul kv export > /consul/data/backup.json'"
Which forces the expansion chain to start over from the beginning with the the rest. Including tokenization.
But, as always, eval must be handled with care, especially if there are user input part in this. Which seems not to be the case here. Because if anything in the chain expands to ; rm -fr /
you know what happens...
Or you could try to create a script /path/bin/myscript
#!/bin/bash
docker exec f0a57e0592e5 sh -c 'consul kv export > /consul/data/backup.json'
and then have command=/path/bin/myscript
so $command
just execute that script (this is a controlled eval
. Since launching a script is forcing to evaluate its content).
Or, depending on the context, same with functions or alias.