1

I'm trying to running a command inside a container with a redirect that should work inside the container, the command should be stored in a variable. This is the command that I run inside the container

consul kv export > /consul/data/backup.json

And this is the command that I tried from the docker host

docker exec f0a57e0592e5 consul kv export > /consul/data/backup.json

This is not working because the redirect happens on the host and not inside the container

For working I had to use shell for executing the redirect inside the container

docker exec f0a57e0592e5 sh -c 'consul kv export > /consul/data/backup.json'

Until here is working, what I need is to be able to pass all this command as a variable

command="docker exec f0a57e0592e5 sh -c 'consul kv export > /consul/data/backup.json'"

But when executing $command I receive kv: 1: Syntax error: Unterminated quoted string

lettore
  • 13
  • 2
  • See [BashFAQ #50](https://mywiki.wooledge.org/BashFAQ/050): _I'm trying to put a command in a variable, but the complex cases always fail!_ – Charles Duffy Nov 04 '22 at 01:41
  • Note that the same thing applies to `>` as to quotes, pipes, and other syntax; so the set of applicable duplicates is broader than a narrow read may make it look. – Charles Duffy Nov 04 '22 at 01:45

1 Answers1

0

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.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
chrslg
  • 9,023
  • 5
  • 17
  • 31