Passing variables to docker run from within a bash shell, getting "unterminated quoted string" error
I have a bash shell with multiple variables that form the command options of a docker run command. I suspect a <
character is breaking the config line that I am passing.
This is the command that I am trying to pass:
docker run -it --entrypoint /bin/sh alpine -c \
"apk update && apk add application && application -f < /dir/file.log"
When I run it on the command line it runs fine. If I even run it in the script as that line it runs fine. But when it runs in the script with the variable substitution, it gives the following error:
update: line 1: syntax error: unterminated quoted string
This is my actual script: (it is probably NOT best practice, but I was struggling to pass multiple commands to the entrypoint command, so hacked it so:
DOCKER_SHELL=/bin/sh
DOCKER_IMAGE=alpine
TEMP=/tmp
FILE=file.log
DOCKER_ENTRY="apk update && apk add application && application -f < "$TEMP"/"$FILE
TEST=\"$DOCKER_ENTRY\"
docker run -it --entrypoint $DOCKER_SHELL $DOCKER_IMAGE -c $TEST
I can run the $DOCKER_ENTRY
variable fine if there is no <
character in the string. So I guess I am struggling to "escape" the <
in the line I am passing. I do have to pass the hole line in as a quoted line, so the challenge I have been having is either my --entrypoint
command must change, or I need to do something else to get it to work.
Any thoughts?
NB:
- Whilst I know I can do this using a Dockerfile, the intention is to do it using a single line in a bash file as above.
- I am by no means a Docker expert, so if there is a better way of running the
--entrypoint
command, please point that out too.
General information
The quoting in your code is very wrong. Important information:
- An unquoted variable undergoes word splitting and filename generation.
- Quotes that appear from variable expansion are not special to the shell that expanded the variable.
- There are two shells: the shell that interprets the script and the shell running in a container. Each parses code on its own.
Additionally consider lowercase variable names. If you get used to using uppercase names then sooner or later you will inadvertently change PATH
, IFS
, USER
, TERM
, HOME
or anything like this and something will go wrong.
Specific analysis
In this line:
DOCKER_ENTRY="apk update && apk add application && application -f < "$TEMP"/"$FILE
$TEMP
and $FILE
are not quoted. Their particular values in the script are "safe", so the lack of quoting does not bite you in this case (unless IFS
from the environment is "unfortunate"). But what if you change them to "non-safe" in the future? Will you remember to fix the code? The right thing is to double-quote regardless of the value.
Then here:
TEST=\"$DOCKER_ENTRY\"
the variable is not quoted. It's a case where not-quoting is not a bug (still IMO it's easier to always quote than to remember all edge cases where you can safely omit quotes). The escaped double-quote characters are not special and get stored into the TEST
variable.
And here:
docker run -it --entrypoint $DOCKER_SHELL $DOCKER_IMAGE -c $TEST
no variable is quoted. Similarly as it was with $TEMP
and $FILE
, the values of $DOCKER_SHELL
and $DOCKER_IMAGE
are "safe" (unless IFS
…). What you're doing with $TEST
is nonetheless fatal. After variable expansion the command will be equivalent to:
docker run -it --entrypoint /bin/sh alpine -c \"apk update && apk add application && application -f < /tmp/file.log\"
where double-quotes from the expansion of $TEST
are not special and therefore I showed them as \"
as if I was typing non-special double-quotes in a shell. Word splitting (because of unquoted $TEST
) makes "apk
a separate word, update
a separate word, and so on.
Inside the container /bin/sh
will run with the following arguments: -c
, "apk
, update
, &&
, apk
, add
, application
, &&
, application
, -f
, <
, /tmp/file.log"
.
The option-argument to -c
is "apk
. This is the only code /bin/sh
inside the container will try to run. The code contains unterminated quoted string. update
works like the second sh
in this question: What is the second sh in sh -c 'some shell code' sh
? That's why you got:
update: line 1: syntax error: unterminated quoted string
Solutions
This snippet fixes quoting, introduces lowercase variable names and a shebang:
#!/bin/sh
docker_shell=/bin/sh
docker_image=alpine
temp=/tmp
file=file.log
docker_entry="apk update && apk add application && application -f < $temp/$file"
docker run -it --entrypoint "$docker_shell" "$docker_image" -c "$docker_entry"
It can be improved and we will improve it in a moment. For now the most important thing is the double-quoted $docker_entry
expands to a single word, so after variable expansion the last line will be equivalent to:
docker run -it --entrypoint /bin/sh alpine -c 'apk update && apk add application && application -f < /tmp/file.log'
where I used single-quotes to indicate the single word from the expansion of $docker_entry
, as if I was trying to pass a single word while typing in a shell.
Now inside the container /bin/sh
will run with the following arguments: -c
, apk update && apk add application && application -f < /tmp/file.log
.
The script is still somewhat flawed. If you change file=file.log
to e.g. file=file'.log
or file='file.log; do_unwanted_things'
then from the expansion of $docker_entry
you will get … < /tmp/file'.log
or … < /tmp/file.log; do_unwanted_things
respectively. This will be interpreted as shell code by sh
inside the container. The single quote will be an error, yet relatively harmless; do_unwanted_things
represents arbitrary code you will run by accident and in general it's far from being harmless.
Note file'.log
and file.log; do_unwanted_things
are legitimate names. You may want to use any of them or whatever as the name. To use them in the above script you need additional escaping or quoting in the shell code passed to sh
. Embedding quotes in the code like this:
docker_entry="apk update && apk add application && application -f < '$temp'/'$file'"
(see quotes within quotes) is not a robust way because a single-quote (like in file'.log
) will interfere, break things and open a possibility to inject code (e.g. do_unwanted_things
).
You can manually alter the original variable so it behaves well later. Example:
file='file.log\; do_unwanted_things'
If your $temp
and $file
are known in advance and static then you can always find such quoting/escaping (inside temp
, file
and/or docker_entry
) so it behaves well and unwanted things are not treated as code. It's not that simple if you want to reliably support any value without manually adjusting the code each time (so maybe a value not known in advance).
In general it will be better if you are able to use any name without worrying some part of it will be interpreted as code (e.g. a meaningful quote or actual command to be run).
In Bash you can try with ${temp@Q}
and ${file@Q}
. The Q
operator causes the value to be quoted in a format that can be reused as shell code to be parsed, so after parsing you get the original value back. This is quite helpful if you must reuse a variable inside shell code (e.g. with ssh user@server code
where there is no other simple way to pass variables than inside code
). We will get back to this approach.
In case of sh -c
we don't need to pass $temp
and $file
as code. Shells provide a robust way to pass data as parameters:
#!/bin/sh
docker_shell=/bin/sh
docker_image=alpine
temp=/tmp
file=file.log
docker_entry='apk update && apk add application && application -f < "$1/$2"'
docker run -it --entrypoint "$docker_shell" "$docker_image" -c "$docker_entry" sh "$temp" "$file"
Now the locally expanded value of $temp
will be known to the shell in the container as $1
; and the expanded value of $file
will be known as $2
. docker_entry
will contain "$1/$2"
as a literal string. $1
and $2
will be expanded in the container. $1/$2
is properly double-quoted inside the shell code run in the container. All this ensures the value of $temp
(or $file
) is never treated as shell code.
Instead of concatenating $1
and $2
in the container ($1/$2
) you can concatenate and form a single argument in the script: $temp/$file
; and then refer to it as $1
. Like this:
…
docker_entry='apk update && apk add application && application -f < "$1"'
docker run -it --entrypoint "$docker_shell" "$docker_image" -c "$docker_entry" sh "$temp/$file"
It's a cosmetic change.
Regardless if you choose to pass $temp
and $file
as two arguments or $temp/$file
as one, the code should be totally safe. The only downside I see is docker_entry
contains $1/$2
or $1
which are not descriptive. Where the variable is defined it's not clear what these positional parameters (will) mean. For this reason consider the following example that is somewhat convoluted, yet after you get its point you may actually like it, especially in pure sh
:
#!/bin/sh
docker_shell=/bin/sh
docker_image=alpine
temp=/tmp
file=file.log
docker_entry='apk update && apk add application && application -f < "$temp/$file"'
docker run -it --entrypoint "$docker_shell" "$docker_image" -c \
'temp="$1"; file="$2"; '"$docker_entry" sh "$temp" "$file"
Now docker_entry
contains meaningful variable names (note: it actually contains names, not values). The docker …
line prepends shell code (temp="$1"; file="$2";
) that works in the container and creates variables from the local variables supplied as later arguments. This way when the shell in the container gets to the code from the expansion of $docker_entry
, the variables will be there.
With (local) Bash you can use meaningful names without additional shell code. Use the already mentioned Q
operator:
#!/bin/bash
docker_shell=/bin/sh
docker_image=alpine
temp=/tmp
file=file.log
docker_entry="apk update && apk add application && application -f < ${temp@Q}/${file@Q}"
docker run -it --entrypoint "$docker_shell" "$docker_image" -c "$docker_entry"
Now docker_entry
contains the values of $temp
and $file
, as opposed to literal strings $temp
and $file
in the previous example (however in the script itself you see meaningful names in both cases, not $1
and such). The values are not necessarily the original values; if needed they are modified by the Q
operator, so they are always safe when embedded in shell code like this.
docker_entry
containing the values is similar to what you were trying to do; and to what the first fixed script in this answer does. The improvement is: with @Q
any value will be handled firmly and safely.
Final note
I don't really know Docker. I don't know "if there is a better way of running the --entrypoint
command". My answer does not address this concern at all.