Python子进程命令以列表而非字符串形式使用

2 投票
4 回答
3660 浏览
提问于 2025-04-18 05:41

我需要在Python中使用subprocess模块,通过重定向标准输出(stdout)来创建一些新文件。我不想使用shell=True,因为那样会有安全隐患。

我写了一些测试命令来搞清楚这个问题,发现这样做是有效的:

import subprocess as sp
filer = open("testFile.txt", 'w')
sp.call(["ls", "-lh"], stdout=filer)
filer.close()

但是,当我把命令作为一个长字符串传递,而不是作为一个列表时,它找不到文件。所以当我这样写的时候:

import subprocess as sp
filer = open("testFile.txt", 'w')
sp.call("ls -lh", stdout=filer)
filer.close()

我收到了这个错误:

Traceback (most recent call last):
  File "./testSubprocess.py", line 16, in <module>
    sp.call(command2, stdout=filer)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 524, in call
    return Popen(*popenargs, **kwargs).wait()
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 711, in __init__
    errread, errwrite)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 1308, in _execute_child
    raise child_exception
OSError: [Errno 2] No such file or directory

为什么把参数作为字符串传递和作为列表传递会有区别呢?

4 个回答

2

根据subprocess.py中的注释:

在UNIX系统中,当shell=False(默认设置)时:在这种情况下,Popen类会使用os.execvp()来执行子程序。args通常应该是一个序列。如果是字符串,它会被当作一个只有这个字符串的序列(也就是要执行的程序)。

在UNIX系统中,当shell=True时:如果args是一个字符串,它就指定了通过shell执行的命令字符串。如果args是一个序列,第一个项目指定命令字符串,后面的项目会被当作额外的shell参数。

在Windows系统中:Popen类使用CreateProcess()来执行子程序,这个方法处理的是字符串。如果args是一个序列,它会通过list2cmdline方法转换成字符串。需要注意的是,并不是所有的Windows应用程序都以相同的方式解释命令行:list2cmdline是为那些遵循与MS C运行时相同规则的应用程序设计的。

在UNIX中,subprocess.call('ls -l')会失败,而在Windows中则会成功。问题出在os.execvp()上,因为整个字符串作为一个参数传递。如果你执行subprocess.call('free'),在UNIX中会成功。

3

这是因为这个参数被当作可执行文件的名字来理解。如果你在命令行里输入 "ls -lh",也是一样的道理。

luk32:~/projects/tests$ "ls -lh"
bash: ls -lh: command not found

有一个工具可以做到这一点,叫做 shlex.split

>>> import shlex, subprocess
>>> command_line = raw_input()
/bin/vikings -input eggs.txt -output "spam spam.txt" -cmd "echo '$MONEY'"
>>> args = shlex.split(command_line)
>>> print args
['/bin/vikings', '-input', 'eggs.txt', '-output', 'spam spam.txt', '-cmd', "echo '$MONEY'"]
>>> p = subprocess.Popen(args)

不过我觉得你不需要这个工具。只要记住,使用列表的方式是正确的,而这个工具是为了帮助你从不推荐的 shell=True 模式过渡过来。

5

如果你想让你的字符串像在命令行中那样被分开,可以使用shlex

import subprocess as sp
import shlex
with open("testFile.txt", 'w') as filer:
    sp.call(shlex.split("ls -lh"), stdout=filer)

顺便说一下,我想推荐一下check_call。如果不使用它,当你添加了一个无效的参数时,你会得到空的输出。例如,你可能会搞不清楚为什么filer的输出是空的。

with open("testFile.txt", 'w') as filer:
    sp.check_call(shlex.split("ls -lh0"), stdout=filer)

使用check_call时,你会得到一个错误提示,这样可以帮助你找到问题所在,并且可以阻止后面的代码继续执行:

Traceback (most recent call last):
  File "go.py", line 6, in <module>
    sp.check_call(shlex.split("ls -lh0"), stdout=filer)
  File "/usr/lib/python2.7/subprocess.py", line 540, in check_call
    raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['ls', '-lh0']' returned non-zero exit status 2
6

这是因为调用的方式不同:

当你使用 shell=True 时,调用是通过命令行的方式进行的,命令会作为一个完整的字符串传给命令行。

而当你使用 shell=False 时,调用是直接进行的,使用的是 execv() 以及相关的函数。这些函数需要一个参数数组。

如果你只传递一个字符串,它会被当作只有可执行文件名称的简写,没有其他参数。但你的系统上可能并没有叫 ls -lh 的可执行文件。

更准确地说,在 subprocess.py 的某个深处,会发生以下情况:

        if isinstance(args, types.StringTypes):
            args = [args]
        else:
            args = list(args)

所以每个传入的字符串都会变成一个只有一个元素的列表。

        if shell:
            args = ["/bin/sh", "-c"] + args

这一点我之前不知道:显然,这样可以传递额外的参数给调用的命令行。虽然文档上是这么写的,但最好不要使用它,因为这样会造成很多混淆。

如果 shell=False,那么下面的代码是:

if env is None:
    os.execvp(executable, args)
else:
    os.execvpe(executable, args, env)

它只是接受一个列表并用它来进行调用。

撰写回答