Python子进程命令以列表而非字符串形式使用
我需要在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 个回答
根据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中会成功。
这是因为这个参数被当作可执行文件的名字来理解。如果你在命令行里输入 "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
模式过渡过来。
如果你想让你的字符串像在命令行中那样被分开,可以使用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
这是因为调用的方式不同:
当你使用 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)
它只是接受一个列表并用它来进行调用。