我正在制作一个 python 脚本,通过使用 pexpect 创建子进程来包装对
docker run
的调用(pexpect 对我来说至关重要,因为命令可能是交互式的):
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)
问题是,当将带引号的 bash 命令传递给
command
参数时,Python 会将其解释为字符串值,因此引号将消失:
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
我感兴趣的是,docker内部是如何解决这个问题的?因为
docker run
是类似的 CLI 程序,因此将收到相同的不带引号的 echo hi
。示例
docker run --rm -it ubuntu bash -c 'echo hi'
hi
效果很好。
我尝试查看 docker cli 源代码:
解析功能看起来很有前途:
但我没有找到参数的任何后期处理(例如添加引号)。
pexpect.spawn()
具有接受命令和参数列表的形式。用这个。 切勿通过将字符串连接在一起来构造命令。
docker_args = ['run', '-it', '--rm', 'ubuntu'] + args.command
child = pexpect.spawn('docker', docker_args)
在您的示例中,
bash -c 'echo hi'
是三个shell单词,bash
、-c
和echo hi
;主机 shell 会在程序看到单引号之前删除它(尝试打印出 args.command
)。保持参数列表形式意味着您无需担心可能需要实现哪种引号或转义:您有四个 docker
参数加上三参数命令,并将七个参数传递给 docker
.
此处的字符串操作会让您面临 shell 注入攻击。如果命令包含“特殊”shell 字符或单引号,会发生什么?
./t.py foo \'word\' \; echo bar \> file
pexpect
文档表明它根本不会解释此标点符号或运行 shell,因此您可能会得到令人困惑的结果。值得注意的是 subprocess.call(shell=True)
will 运行 shell,这将导致您的程序在程序退出后运行该 echo
命令。使用列表语法可以避免这种歧义:您有一个包含 ;
的列表项,它作为参数传递给命令,并且永远不会解释为其他任何内容。
还要考虑Docker SDK for Python是否可以满足您的需求。
client.containers.run()
采用与 docker run
类似的 Python 原生关键字参数,但与容器的 stdin/stdout 交互的相应 container.attach_socket()
方法使用起来可能会比较棘手。