I’m making a python script, that wraps call to docker run
by creating a child process with pexpect (pexpect is essential to me, because command might be interactive):
import argparse
import pexpect
parser = argparse.ArgumentParser()
parser.add_argument("some_positional_argument")
parser.add_argument("command", nargs=argparse.REMAINDER)
args = parser.parse_args()
command = " ".join(args.command)
docker_run = f"docker run -it --rm ubuntu {command}"
print(f"docker_run: {docker_run}")
child = pexpect.spawn(docker_run)
child.interact()
child.close()
exit(child.exitstatus)
Problem is, when passing bash command with quotes to the command
argument, python will interpret it as just a string value, so quotes will be gone:
python3 t.py foo bash -c 'echo hi'
docker_run: docker run -it --rm ubuntu bash -c echo hi
# empty line here, because 'bash -c echo' was actually called inside the container
What is interesting to me, how docker solves this issue internally? Because docker run
is similar CLI program, that will receive same unquoted echo hi
. Example
docker run --rm -it ubuntu bash -c 'echo hi'
hi
works just fine.
I tried to look into docker cli source code:
- https://github.com/docker/cli/blob/06de1f8d2c56737ea06a547197a5ae3701aa5397/cli/command/container/run.go#L44
- https://github.com/docker/cli/blob/06de1f8d2c56737ea06a547197a5ae3701aa5397/cli/command/container/run.go#L108
parse function seemed promising:
- https://github.com/docker/cli/blob/06de1f8d2c56737ea06a547197a5ae3701aa5397/cli/command/container/opts.go#L324
- https://github.com/docker/cli/blob/06de1f8d2c56737ea06a547197a5ae3701aa5397/cli/command/container/opts.go#L402
- https://github.com/docker/cli/blob/06de1f8d2c56737ea06a547197a5ae3701aa5397/cli/command/container/opts.go#L632
- https://github.com/docker/cli/blob/06de1f8d2c56737ea06a547197a5ae3701aa5397/cli/command/container/opts.go#L716
but i didn’t find any post processing (like adding quotes back) of the arguments.
2
Answers
pexpect.spawn()
has a form where it takes a command and a list of arguments. Use this. Never construct a command by concatenating strings together.In your example,
bash -c 'echo hi'
is three shell words,bash
,-c
, andecho hi
; the host shell will remove the single quotes before your program sees it (try printing outargs.command
). Staying in the list-of-arguments form means you don’t need to worry about what kind of quotes or escaping you might need to implement: you have fourdocker
arguments plus the three-argument command and pass seven arguments todocker
.String manipulation here opens you up to a shell injection attack. What happens if the command includes "special" shell characters, or single quotes?
pexpect
documents that it won’t interpret this punctuation or run a shell at all, so you’ll get potentially confusing results. Of notesubprocess.call(shell=True)
will run a shell, and this would cause your program to run thatecho
command after the program exits. Using the list syntax avoids this ambiguity: you have a list item containing;
that is passed as an argument to the command and never interpreted as anything else.Also consider whether the Docker SDK for Python can meet your needs.
client.containers.run()
takes Python-native keyword arguments similar todocker run
, but the correspondingcontainer.attach_socket()
method to interact with the container’s stdin/stdout can be trickier to use.Python has a utility to give shell-like quote handling, shlex, https://pymotw.com/2/shlex/ (or officil python docs)
So replace
with
to get properly quoted text.