在Python中运行Bash命令
在Python中运行Bash命令
在我的本地机器上,我运行了一个包含这行代码的 Python 脚本:
bashCommand = "cwm --rdf test.rdf --ntriples > test.nt" os.system(bashCommand)
这个工作得很好。
然后我在服务器上运行了相同的代码,结果我收到了以下错误信息:
'import site' failed; use -v for traceback Traceback (most recent call last): File "/usr/bin/cwm", line 48, in from swap import diag ImportError: No module named swap
于是我插入了一个 print bashCommand
来在终端中输出命令,然后再使用 os.system()
运行该命令。
当然,在那个错误之前它会再次给我打印出错误(由 os.system(bashCommand)
引起)。然后我只需要复制该输出,并将其粘贴到终端中并敲击回车键即可工作……
有谁知道究竟发生了什么吗?
稍微扩展一下先前的答案,有几个常常被忽视的细节。
- 优先使用
subprocess.run()
,接着是subprocess.check_call()
和subprocess.call()
,然后是subprocess.Popen()
,然后是os.system()
,最后是os.popen()
- 了解并且使用
text=True
或universal_newlines=True
- 理解
shell=True
或者shell=False
的意义,以及它对引号和shell简便选项的可用性的影响 - 了解
sh
和Bash之间的区别 - 了解子进程与其父进程是分离的,通常无法改变父进程
- 避免将Python解释器作为Python的子进程运行
以下章节将对这些主题进行详细阐述。
优先使用subprocess.run()
或subprocess.check_call()
subprocess.Popen()
函数是一个低级别的工具,但使用时很棘手,会导致需要复制/粘贴多行代码...幸运的是,这些高级别的包装程序函数已经存在于标准库中,用于多种不同的目的,下面将提供更详细的解释。
下面是文档中的一段话:
调用子进程的推荐方法是将所有可处理的用例都使用
run()
函数。对于更高级别的用例,可以直接使用底层的Popen
接口。
不幸的是,这些包装函数的可用性在Python的不同版本中是不同的。
subprocess.run()
是在Python 3.5中正式引入的。它的目的是替代以下所有函数。subprocess.check_output()
在Python 2.7 / 3.1中引入。它基本上等价于subprocess.run(..., check=True, stdout=subprocess.PIPE).stdout
subprocess.check_call()
在Python 2.5中引入。它基本上等价于subprocess.run(..., check=True)
subprocess.call()
是在Python 2.4中在原始的subprocess
模块中引入的(PEP-324)。它基本上等价于subprocess.run(...).returncode
高级别API vs subprocess.Popen()
重构和扩展subprocess.run()
比它替代的旧的遗留函数更合乎逻辑,更为灵活。它返回一个CompletedProcess
对象,该对象有多种方法,允许您检索完成的子进程的退出状态、标准输出以及一些其他的结果和状态指标。
subprocess.run()
是如果你只是需要运行一个程序并将控制返回给Python,则应采用的方法。对于更复杂的情况(后台进程,可能与Python父程序进行交互式I/O),您仍需要使用subprocess.Popen()
并自行处理所有管道。这需要对所有移动部件有相当复杂的理解,不应轻率地进行。简单的Popen
对象表示需要在子进程的剩余生命周期内从您的代码中管理的(可能仍在运行的)进程。
应该强调的是,只有subprocess.Popen()
仅仅是创建一个进程。如果您只是让其运行,那么您将拥有一个与Python并行运行的子进程,因此是一个“后台”进程。如果它不需要进行输入或输出或以其他方式与您协调,则可以与您的Python程序并行进行有用的工作。
避免使用os.system()
和os.popen()
从时代的长河(自Python 2.5以来),os模块文档一直建议优先使用subprocess
而非os.system()
:
subprocess
模块提供了更强大的生成新进程并检索结果的功能;使用该模块优于使用此功能。
system()
的问题在于,它显然是依赖于系统的,并且不提供与子进程交互的方式。它只是运行,而标准输出和标准错误超出了Python的范围。Python收到的唯一信息是命令的退出状态(零代表成功,尽管非零值的含义也有些依赖于系统)。
PEP-324(如上所述)包含了更详细的理由,介绍了os.system
存在的问题以及subprocess
试图解决这些问题的方法。
os.popen()
以前甚至被强烈建议不再使用:
从版本2.6开始弃用:此函数已过时,请使用
subprocess
模块。
然而,自Python 3的某个时候以来,它已被重新实现为仅使用subprocess
,并将重定向到subprocess.Popen()
文档以获取详细信息。
理解并通常使用check=True
您还会注意到,subprocess.call()
具有与os.system()
相同的许多限制。在常规使用中,您通常应该检查进程是否成功完成,此时subprocess.check_call()
和subprocess.check_output()
会执行此操作(后者还会返回已完成子进程的标准输出)。类似地,除非有特定需要允许子进程返回错误状态,通常应使用check=True
和subprocess.run()
。
在实践中,使用check=True
或subprocess.check_*
,如果子进程返回非零退出状态,Python将抛出CalledProcessError
异常。
使用subprocess.run()
时常见的错误是省略了check=True
,如果子进程失败,下游代码将失败,而你会感到惊讶。
另一方面,check_call()
和check_output()
的常见问题是,用户在盲目使用这些函数时,当抛出异常时被惊讶了。(你应该替换grep
为下面所述的本地Python代码。)
总的来说,你需要理解shell命令返回退出码的方式,以及它们在什么条件下将返回非零(错误)退出码,并做出明智的决定如何处理它们。
了解并可能使用text=True
,即universal_newlines=True
自Python 3以后,Python内部的字符串是Unicode字符串。但是不能保证subprocess生成Unicode输出,或者根本不输出字符串。
(如果差异不是立即明显的,建议阅读Ned Batchelder的实用Unicode,如果不是强制性的。链接后面有一个36分钟的视频演示,如果你喜欢的话,看这个页面自己读可能要花费更少的时间。)
在深处,Python必须获取一些bytes
缓冲区并以某种方式进行解释。如果它包含一个二进制数据块,那么不应将其解码为Unicode字符串,因为这是错误且易导致错误的行为——这正是许多Python 2脚本在正确区分编码文本和二进制数据之前遭遇的讨厌行为。
使用text=True
,你告诉Python,你实际上期望以系统默认编码返回文本数据,并且应该根据Python的最佳能力(通常在任何适度更新的系统上为UTF-8,除了可能是Windows?)将其解码为Python(Unicode)字符串
如果你请求的不是这个,Python只会在stdout
和stderr
字符串中给你bytes
字符串。也许在以后的某个时候,你确实知道它们实际上是文本字符串,你知道它们的编码。那么,你可以对它们进行解码。
normal = subprocess.run([external, arg], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, text=True) print(normal.stdout) convoluted = subprocess.run([external, arg], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) # You have to know (or guess) the encoding print(convoluted.stdout.decode('utf-8'))
Python 3.7引入了更短、更具描述性和更易理解的别名text
,以替换之前有些具有误导性的关键字参数universal_newlines
。
了解shell=True
与shell=False
之间的区别
使用shell=True
,你将一个字符串传递给你的shell程序,然后shell程序接手后处理。
当你将 shell=False
作为参数传递给操作系统时,你可以绕过shell,直接传递一个参数列表。
当你没有shell时,可以省去一些隐藏的复杂性,这些复杂性可能会存在错误或安全问题。
另一方面,当你没有shell时,你也无法使用重定向、通配符扩展、作业控制以及许多其他shell功能。
一个常见的错误是使用 shell=True
,然后仍然向Python传递一个标记列表,或者反之亦然。这在某些情况下可能有效,但定义是含糊的,可能会出现有趣的问题。
# XXX AVOID THIS BUG buggy = subprocess.run('dig +short stackoverflow.com') # XXX AVOID THIS BUG TOO broken = subprocess.run(['dig', '+short', 'stackoverflow.com'], shell=True) # XXX DEFINITELY AVOID THIS pathological = subprocess.run(['dig +short stackoverflow.com'], shell=True) correct = subprocess.run(['dig', '+short', 'stackoverflow.com'], # Probably don't forget these, too check=True, text=True) # XXX Probably better avoid shell=True # but this is nominally correct fixed_but_fugly = subprocess.run('dig +short stackoverflow.com', shell=True, # Probably don't forget these, too check=True, text=True)
常见的反驳“但是在我的电脑上可以运行”并不是一个有用的反驳,除非你明确了解在何种情况下它可能停止工作。
简要概括一下,正确的用法如下:
subprocess.run("string for 'the shell' to parse", shell=True) # or subprocess.run(["list", "of", "tokenized strings"]) # shell=False
如果你想避免使用shell,但是懒得或者不确定如何将字符串解析为标记列表,可以注意 shlex.split()
,它可以操作。
subprocess.run(shlex.split("no string for 'the shell' to parse")) # shell=False # equivalent to # subprocess.run(["no", "string", "for", "the shell", "to", "parse"])
常规的 split()
在这里不起作用,因为它不能保留引用。在上面的例子中,注意 “the shell”
是单个字符串。
重构示例
通常,shell的某些功能可以用本地的Python代码替换。简单的Awk或者 sed
脚本应该被转换为Python脚本。
下面是一个典型但稍微有点儿愚蠢的示例,它涉及到许多shell功能。
cmd = '''while read -r x; do ping -c 3 "$x" | grep 'min/avg/max' done
这里需要注意的一些事情:
- 在
shell=False
时,你不需要像shell一样对字符串进行引用,多余的引号可能是错误的。- 在子进程中尽可能少地运行代码通常是有意义的。这样可以更好地从Python代码中控制执行。
- 话虽如此,复杂的shell管道有时候很繁琐,重新在Python中实现也有些不易。
重构的代码也展示了shell为你提供了多少简洁语法 -- 好的或坏的,它说明了“明确要优于隐含”,然而Python代码相当冗长,可以说看起来比实际更复杂。然而,它提供了许多从其他地方获取控制权的点,就像我们通过增强可以轻松地将主机名和shell命令输出一起包含在内一样。(这在shell中并不是难做到的,但代价是增加另一个分支和另一个进程。)
常见的Shell结构
为了完整起见,下面是一些shell功能的简要解释以及如何使用本地Python库来代替它们的一些说明。
- Globbing(也称通配符扩展)可以用
glob.glob()
或者像for file in os.listdir('.'): if not file.endswith('.png'): continue
这样的Python简单字符串比较来代替。Bash具有其他扩展功能,如.{png,jpg}
花括号扩展和{1.. 100}
,以及波浪线扩展(~
扩展为您的主目录,而更一般地,~account
对应于另一个用户的主目录)- Shell变量(如
$ SHELL
或$my_exported_var
)有时可以直接用Python变量替换。导出的Shell变量可通过os.environ['SHELL']
等方式使用(export
的含义是使变量可用于子进程--一个对子进程不可用的变量将显然不可用于作为Shell的子进程运行的Python,反之亦然。subprocess
方法的env =
关键字参数允许您将子进程的环境定义为字典,因此这是一种将Python变量显示为子进程可见的方法)。如果shell=False
,您需要了解如何删除任何引号。例如,cd "$HOME"
等同于没有在目录名称周围添加引号的os.chdir(os.environ['HOME'])
。(很多时候,cd
无用或不必要,许多初学者省略了Shell变量周围的双引号,这样得以继续,直到某天...)- 重定向允许您将一个文件作为标准输入读取,并将标准输出写入文件。例如:
grep 'foo'
打开outputfile outputfile
进行读写, 以及打开inputfile
进行读取,并将其内容作为标准输入传递给grep
,其标准输出然后出现在outputfile
中。这通常很容易用本机Python代码替换。- 管道是一种重定向形式。
echo foo | nl
运行两个子进程,其中echo
的标准输出是nl
的标准输入(在Unix系统中,在操作系统级别上,这是一个单个的文件句柄)。如果您无法将管道的一个或两个端点替换为本机Python代码,那么也许还是需要考虑使用Shell,特别是管道拥有两个或三个以上的过程(尽管可以查看Python标准库中的pipes
模块或众多更现代和多功能的第三方竞争者)。- 作业控制可让您中断作业、在后台运行作业、返回到前台等等。当然,Unix的基本信号以停止和继续进程当然也可在Python中使用。但如果您要从Python执行此类操作,则必须理解作业是Shell中的高级抽象,它涉及进程组等内容。
- Shell中的引用可能会令人困惑,直到您理解所有内容都基本上是字符串。因此,
ls -l /
等同于'ls' '-l' '/'
,但对文字的引用是完全可选的。未引用字符串包含Shell元字符,将进行参数扩展、空格标记和通配符扩展;双引号可防止空格标记和通配符扩展,但允许参数扩展(变量替换、命令替换和反斜杠处理)。理论上这很简单,但可能会变得令人困惑,特别是当有几层解释时(例如,远程Shell命令)。要了解
sh
和Bash之间的差异
subprocess
使用/bin/sh
运行您的Shell命令,除非您专门要求不然(当然在Windows上,它使用COMSPEC
变量的值)。这意味着一些只在Bash中可用的功能,例如数组、[[
等,不可用。如果需要使用Bash专有语法,可以将shell的路径作为
executable='/bin/bash'
传入(当然如果您的Bash安装在其他位置,则需要调整路径)。subprocess.run(''' # This for loop syntax is Bash only for((i=1;i<=$#;i++)); do # Arrays are Bash-only array[i]+=123 done''', shell=True, check=True, executable='/bin/bash')
subprocess
进程与其父进程分离,无法更改其父进程有一个常见的错误是做类似于下面的事情
subprocess.run('cd /tmp', shell=True) subprocess.run('pwd', shell=True) # Oops, doesn't print /tmp
如果第一个子进程试图设置一个环境变量,当您运行另一个子进程时,该变量自然会消失,等等。
子进程与Python完全分开运行,当它完成时,Python不知道它做了什么(除了可以从子进程的退出状态和输出推断出的模糊指示)。 子进程通常无法更改父进程的环境;它无法设置变量,更改工作目录,或者换句话说,不经父进程的合作就无法与其父进程通信。
在这种特殊情况下的直接解决方法是在单个子进程中运行两个命令;
subprocess.run('cd /tmp; pwd', shell=True)
显然,这种特定用法并不是非常有用;而是使用
cwd
关键字参数,或者在运行子进程之前简单地使用os.chdir()
。 同样,对于设置变量,您可以通过os.environ['foo'] = 'bar'
或使用
subprocess.run('echo "$foo"', shell=True, env={'foo': 'bar'})
将环境设置传递给子进程(更不用提明显的重构
subprocess.run(['echo', 'bar'])
了; 但是,当然,首先在子进程中运行echo
是一个糟糕的例子)。不要从Python中运行Python
这是稍微有些可疑的建议;当然有些情况下确实有意义,甚至是绝对需要从Python脚本中作为子进程运行Python解释器。但很多时候,正确的方法是直接将其他Python模块导入您的调用脚本,并直接调用其函数。
如果另一个Python脚本在您的控制之下,并且不是模块,请考虑将其转换为模块。 (这个答案已经太长了,所以我不会在这里深入探讨细节。)
如果需要并行处理,可以使用
multiprocessing
模块在子进程中运行Python函数。 还有threading
,它在单个进程中运行多个任务(更轻量级,可以提供更多的控制,但也更受限制,因为进程内的线程紧密耦合,并绑定到单个GIL)。
不要使用 os.system
。它已经被废弃了,而 subprocess 更好。来自 文档: "这个模块旨在取代几个旧模块和函数:os.system
、os.spawn
"。
就像在你的情况下:
import subprocess bashCommand = "cwm --rdf test.rdf --ntriples > test.nt" process = subprocess.Popen(bashCommand.split(), stdout=subprocess.PIPE) output, error = process.communicate()