This question already has answers here:
Execute command containing quotes from shell variable [duplicate]
(2 answers)
Bash - Variable with variables and commands
(1 answer)
Why does shell ignore quoting characters in arguments passed to it through variables? [duplicate]
(3 answers)
Closed 4 months ago.
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
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.
Related
This question already has answers here:
Why can't I specify an environment variable and echo it in the same command line?
(9 answers)
Closed 2 years ago.
I want to assign one or multiple variables at the beginning of a command line in my shell to reuse it in the command invocation. I'm confused of how my shell behaves and want to understand what is happening.
I'm using ZSH but am also interested what the "standard" posix behavior is.
1: % V=/ echo $V # echo is a shell built-in?!?
expected: /. actual: ""
2: % V=/ ls $V # ls is a command
expected: ls /. actual: ls
3: % V=/ ; echo $V
expected: "". actual: /
Here I thought that the semicolon would be equivalent to a new shell line and that I'd need export.
4: % V=/ ; ls $V
expected: ls. actual: ls /
I'm mostly surprised by lines 1 and 2. Is there any ZSH settings that could cause this or do I just start to use a semicolon to use variables in this way?
Variable expansion happens before the command is run, i.e. before the value is assigned to the variable in lines 1 and 2.
export is needed when you need to export the variable to a subshell. A semicolon doesn't introduce a subshell, but causes the assignment to be run before the next command, so the shell now expands the variable by its new value.
Your line 1 would work if you would allow the variable expansion to occur inside the echo and don't force it, before echo gets a chance to run, for instance by
V=/ zsh -c 'echo $V'
or by
V=/ eval 'echo $V'
It doesn't matter that echo is a builtin command. The same idea applies to every command.
Since commands can be separated either by semicolons or by linefeeds, your line 3 is equivalent to
V=/
echo $V
which makes it obvious, why the substitution works in this case.
This question already has answers here:
How can I preserve quotes in printing a bash script's arguments
(7 answers)
Closed 4 years ago.
I have a script that logs the user argument list. This list is later processed by getopt.
If the script is started like:
./script.sh -a 'this is a sentence' -b 1
... and then I save "$#", I get:
-a this is a sentence -b 1
... without the single quotes. I think (because of the way Bash treats quotes) these are removed and are not available to the script.
For logging accuracy, I'd like to include the quotes too.
Can the original argument list be obtained without needing to quote-the-quotes?
No, there is no way to obtain the command line from before the shell performed whitespace tokenization, wildcard expansion, and quote removal on it.
If you want to pass in literal quotes, try
./script.sh '"-a"' '"this is a sentence"' '"-b"' '"1"'
Notice also how your original command line could have been written
'./script.sh' '-a' 'this is a sentence' '-b' '1'
This question already has answers here:
Difference between single and double quotes in Bash
(7 answers)
Closed 2 years ago.
I'm trying to execute a list of commands through the command:
bash -l -c "commands"
However, when I define a variable an then try to use them, the variable is undefined (empty). To be clear:
bash -l -c "var=MyVariable; echo $var"
Bash expansion (as explained here) will expand the variable value inside the double quotes before actually executing the commands. To avoid so you can follow either of these options:
Option 1:
Avoid the expansion using single quotes
bash -l -c 'var=MyVariable; echo $var'
Option 2:
Avoid the expansion inside the double quotes by escaping the desired variables
bash -l -c "var=MyVariable; echo \$var"
The second option allows you to expand some variables and some others not. For example:
expandVar=MyVariable1
bash -l -c "var=MyVariable; echo $expandVar; echo \$var"
Bash expands variables inside double quotes. So in effect in your command $var is replaced by the current value of var before the command is executed. What you want can be accomplished by using single quotes:
bash -l -c 'var=MyVariable; echo $var'
Please note that it is rather unusual to invoke Bash as a login shell (-l) when passing a command string with -c, but then you may have your reasons.
This question already has answers here:
Difference between single and double quotes in Bash
(7 answers)
Closed 6 years ago.
I can do
bash -x mysellh.sh
to see the commands as they are being executed, but I don't see the variables replaced with their values. For instance, if I have in the shell script:
screen -d -m sh -c 'RAILS_ENV="$R_ENV" bundle exec rake sunspot:solr:reindex[500] > "$PROJECT_DIR"/solr/indexing_out.txt'
I will see:
+ screen -d -m sh -c 'RAILS_ENV="$R_ENV" bundle exec rake sunspot:solr:reindex[500] > "$PROJECT_DIR"/solr/indexing_out.txt'
Even though I've already declared earlier:
R_ENV=test
Is there an option to do what I want?
In this case, it is doing what you asked: the command line "word" that $R_ENV is in is what you have enclosed in single quotes, which inhibit expansion. The -x output is showing you the non-expansion. If you want those variables to be expanded, enclose that first "word" in double quotes and use single quotes in the contents, like this:
screen -d -m sh -c "RAILS_ENV='$R_ENV' bundle exec rake sunspot:solr:reindex[500] > '$PROJECT_DIR'/solr/indexing_out.txt"
Then the single quotes are around the expanded text and the double quotes allow the variables to expand.
This question already has answers here:
Why does shell ignore quoting characters in arguments passed to it through variables? [duplicate]
(3 answers)
Closed 4 years ago.
I found an unexpected behavior in a bash script recently and I'd like to understand it before I work around it. Here's a simplified example:
#! /bin/sh
SCRIPT="/tmp/echoscript.sh >> /tmp/log"
/bin/sh ${SCRIPT}
echoscript.sh just does 'echo "abc"'
The unexpected thing is that 'abc' goes to the terminal and not to the /tmp/log file. Why is that?
If I change the third line to:
/bin/sh ${SCRIPT} >> /tmp/log
Then I get the expected result; 'abc' goes to the log file.
You can't put redirections in variables like that. When you do Bash interprets /bin/sh ${SCRIPT} as if you wrote:
/bin/sh "/tmp/echoscript.sh" ">>" "/tmp/log"
Notice how ">>" is quoted, killing the attempted redirection. This happens because Bash parses a command in phases, and parsing redirections happens before variable expansion. Variable values are not checked for redirections.
When variables are expanded, the only further processing on the result is word splitting and wildcard expansion (aka globbing). The result is not scanned for other special operators, like redirection.
If you want full reprocessing of the command line, you have to use eval:
eval "/bin/sh ${SCRIPT}"
This won't do what you expect since the redirect isn't being processed in ${SCRIPT}. You'll get /bin/sh: /tmp/echoscript.sh >> /tmp/log: No such file or directory.
You'll need to tell the shell to evaluate ${SCRIPT} before running it, like this:
eval /bin/sh ${SCRIPT}
$ cat /tmp/log
abc
yet another way :
alias SCRIPT="/bin/sh /tmp/echoscript.sh >> /tmp/log"
SCRIPT