使用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 <module>
from swap import  diag
ImportError: No module named swap

因此,我所做的就是在终端中插入一个print bashCommand,它在终端中使用os.system()运行命令之前将我打印出来。

当然,我再次得到错误(由os.system(bashCommand)引起),但在该错误之前,它在终端中打印命令。然后我复制输出并复制粘贴到终端,然后按enter,它工作了…

有人知道这是怎么回事吗?

885288 次浏览

用subprocess调用它

import subprocess
subprocess.Popen("cwm --rdf test.rdf --ntriples > test.nt")

你得到的错误似乎是因为服务器上没有交换模块,你应该在服务器上安装交换模块,然后再次运行脚本

不要使用os.system。它已被弃用,而支持子流程。该模块打算取代几个较旧的模块和函数:os.systemos.spawn"

比如你的例子:

import subprocess


bashCommand = "cwm --rdf test.rdf --ntriples > test.nt"
process = subprocess.Popen(bashCommand.split(), stdout=subprocess.PIPE)
output, error = process.communicate()

根据错误,您在服务器上丢失了一个名为交换的包。/usr/bin/cwm需要它。如果你使用的是Ubuntu/Debian,使用aptitude安装python-swap

你可以使用bash程序,带参数-c来执行命令:

bashCommand = "cwm --rdf test.rdf --ntriples > test.nt"
output = subprocess.check_output(['bash','-c', bashCommand])

你可以使用subprocess,但我总是觉得这不是一种“python”的方式。所以我创建了Sultan(无耻的插头),它可以很容易地运行命令行函数。

https://github.com/aeroxis/sultan

python的方法是使用subprocess.Popen

subprocess.Popen接受一个列表,其中第一个元素是要运行的命令,后面跟着任何命令行参数。

举个例子:

import subprocess


args = ['echo', 'Hello!']
subprocess.Popen(args) // same as running `echo Hello!` on cmd line


args2 = ['echo', '-v', '"Hello Again"']
subprocess.Popen(args2) // same as running 'echo -v "Hello Again!"` on cmd line

也可以使用'os.popen'。 例子:< / p >

import os


command = os.popen('ls -al')
print(command.read())
print(command.close())

输出:

total 16
drwxr-xr-x 2 root root 4096 ago 13 21:53 .
drwxr-xr-x 4 root root 4096 ago 13 01:50 ..
-rw-r--r-- 1 root root 1278 ago 13 21:12 bot.py
-rw-r--r-- 1 root root   77 ago 13 21:53 test.py


None

为了进一步扩展前面的答案,这里有一些通常被忽略的细节。

  • 喜欢subprocess.run()而不是subprocess.check_call(),喜欢朋友而不是subprocess.call()而不是subprocess.Popen()而不是os.system()而不是os.popen()
  • 理解并可能使用text=True,也就是universal_newlines=True
  • 理解shell=Trueshell=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对象,该对象具有各种方法,允许您从已完成的子进程检索退出状态、标准输出和其他一些结果和状态指示器。

如果你只是需要一个程序来运行并将控制权返回给Python, subprocess.run()是一种方法。对于更复杂的场景(后台进程,可能与Python父程序进行交互式I/O),您仍然需要使用subprocess.Popen()并自己处理所有管道。这需要对所有活动的部分有相当复杂的理解,不应该轻率地进行。更简单的Popen对象表示(可能仍在运行)需要在子进程生命周期的剩余时间内从代码中管理的进程。

应该强调的是,subprocess.Popen()仅仅创建了一个进程。如果你不去管它,你就有一个子进程与Python同时运行,所以“后台”;的过程。如果它不需要输入或输出,或以其他方式与你协调,它可以与你的Python程序并行地做有用的工作。

避免os.system()os.popen()

从time eternal(好吧,从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=Truesubprocess.run(),除非你特别需要允许子进程返回错误状态。

实际上,使用check=Truesubprocess.check_*,如果子进程返回非零退出状态,Python将抛出CalledProcessError例外

subprocess.run()的一个常见错误是省略check=True,如果子进程失败,当下游代码失败时,会感到惊讶。

另一方面,check_call()check_output()的一个常见问题是,盲目使用这些函数的用户在引发异常时感到惊讶,例如当grep没有找到匹配时。(无论如何,你都应该用本地Python代码替换grep,如下所述。)

考虑到所有因素,您需要了解shell命令如何返回退出码,以及在什么条件下它们将返回非零(错误)退出码,并有意识地决定应该如何处理它。

理解并可能使用text=Trueuniversal_newlines=True

从Python 3开始,Python内部的字符串都是Unicode字符串。但是不能保证子进程会生成Unicode输出或字符串。

(如果差异不是立即明显,建议阅读Ned Batchelder的务实的Unicode,如果不是完全必须的,阅读。如果你愿意,链接后面有一个36分钟的视频演示,不过自己阅读页面可能会花更少的时间。)

在深层,Python必须获取bytes缓冲区并以某种方式解释它。如果它包含一团二进制数据,它不应该将被解码为Unicode字符串,因为这是容易出错和导致错误的行为——正是这种令人讨厌的行为,困扰了许多Python 2脚本,在有办法正确区分编码文本和二进制数据之前。

使用text=True,你告诉Python,你实际上希望返回系统默认编码的文本数据,并且应该尽Python的最大能力将其解码为Python (Unicode)字符串(在任何最新的系统上通常是UTF-8,除了Windows?)

如果你请求返回的是, Python只会在stdoutstderr字符串中给你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=Trueshell=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 <hosts.txt'''


# Trivial but horrible
results = subprocess.run(
cmd, shell=True, universal_newlines=True, check=True)
print(results.stdout)


# Reimplement with shell=False
with open('hosts.txt') as hosts:
for host in hosts:
host = host.rstrip('\n')  # drop newline
ping = subprocess.run(
['ping', '-c', '3', host],
text=True,
stdout=subprocess.PIPE,
check=True)
for line in ping.stdout.split('\n'):
if 'min/avg/max' in line:
print('{}: {}'.format(host, line))

这里有几点需要注意:

  • 使用shell=False,你不需要shell在字符串周围要求的引号。无论如何加引号都可能是错误的。
  • 在子进程中运行尽可能少的代码通常是有意义的。这使您可以从Python代码中更好地控制执行。
  • 话虽如此,复杂的shell管道是乏味的,有时在Python中重新实现具有挑战性。

重构的代码还用非常简洁的语法说明了shell到底为您做了多少事情——不管是好是坏。Python说显性比隐性好,但Python代码相当冗长,可以说看起来比实际更复杂。另一方面,它提供了许多点,在这些点上您可以在其他事情中间获取控制权,例如我们可以轻松地在shell命令输出中包含主机名的增强。(这在shell中也绝不具有挑战性,但代价是另一种转移,也许是另一个过程。)

通用Shell结构

为了完整起见,这里简要说明了其中一些shell特性,并说明了如何用本地Python工具替换它们。

  • 通配符扩展可以用glob.glob()代替,也可以用简单的Python字符串比较替换,比如for file in os.listdir('.'): if not file.endswith('.png'): continue。Bash还有其他各种扩展工具,比如.{png,jpg}大括号展开和{1..100}展开以及波浪号展开(~展开到你的主目录,更普遍的是~account展开到另一个用户的主目录)。
  • $SHELL$my_exported_var这样的Shell变量有时可以简单地用Python变量替换。导出的shell变量是可用的,例如os.environ['SHELL'] (export的意思是使变量对子进程可用——一个对子进程不可用的变量显然对作为shell子进程运行的Python不可用,反之亦然。subprocess方法的env=关键字参数允许你将子进程的环境定义为字典,因此这是使Python变量对子进程可见的一种方法)。使用shell=False,你需要了解如何删除任何引号;例如,cd "$HOME"相当于不带引号的os.chdir(os.environ['HOME'])。(通常cd是没有用的,也没有必要,许多初学者省略了变量周围的双引号,而使用$my_exported_var0)
  • 重定向允许您从文件中读取作为标准输入的内容,并将标准输出写入文件。grep 'foo' <inputfile >outputfile打开outputfile用于写入,打开inputfile用于读取,并将其内容作为标准输入传递给grep,后者的标准输出随后位于outputfile。这通常不难用本地Python代码替换。
  • 管道是重定向的一种形式。echo foo | nl运行两个子进程,其中echo的标准输出是nl的标准输入(在操作系统级别,在类unix系统中,这是一个单一的文件句柄)。如果你不能用本地Python代码替换管道的一端或两端,也许还是考虑使用shell吧,特别是如果管道有两个或三个以上的进程(尽管可以参考Python标准库中的pipes模块或许多更现代、更通用的第三方竞争对手)。
  • 作业控制允许您中断作业、在后台运行它们、将它们返回到前台等等。当然,用于停止和继续进程的基本Unix信号也可以从Python中获得。但是作业是shell中更高层次的抽象,涉及到进程组等,如果你想用Python做类似的事情,你必须理解这些。
  • 在shell中引用可能会令人困惑,直到你明白一切基本上是一个字符串。所以ls -l /等价于'ls' '-l' '/',但是字面量周围的引号是完全可选的。包含shell元字符的非引号字符串进行参数展开、空白标记化和通配符展开;双引号阻止空格标记化和通配符展开,但允许参数展开(变量替换、命令替换和反斜杠处理)。这在理论上很简单,但可能会令人困惑,特别是当有多个解释层时(例如,一个远程shell命令)。

理解sh和Bash之间的区别

subprocess使用/bin/sh运行你的shell命令,除非你特别要求(当然在Windows上除外,它使用COMSPEC变量的值)。这意味着各种bash独有的特性,如数组,[[不可用。

如果你需要使用Bash-only语法,你可以 将路径作为executable='/bin/bash'传递给shell(当然,如果您的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脚本的子进程运行是有意义的,甚至是绝对必要的。但通常情况下,正确的方法是简单地将import另一个Python模块放入调用脚本中,并直接调用它的函数。

如果另一个Python脚本在你的控制之下,并且它不是一个模块,考虑把它变成一个。(这个答案已经太长了,所以我不会在这里深入研究细节。)

如果你需要并行性,你可以使用multiprocessing模块。在子进程中运行Python函数。还有threading可以在单个进程中运行多个任务(它更轻量级,给你更多控制,但也有更多约束,因为进程中的线程是紧密耦合的,并绑定到单个吉尔。)

要在没有shell的情况下运行该命令,将该命令作为列表传递,并使用[subprocess]在Python中实现重定向:

#!/usr/bin/env python
import subprocess


with open('test.nt', 'wb', 0) as file:
subprocess.check_call("cwm --rdf test.rdf --ntriples".split(),
stdout=file)

注意:结尾没有> test.ntstdout=file实现了重定向。


要在Python中使用shell运行该命令,将该命令作为字符串传递并启用shell=True:

#!/usr/bin/env python
import subprocess


subprocess.check_call("cwm --rdf test.rdf --ntriples > test.nt",
shell=True)

下面是shell负责输出重定向(> test.nt在命令中)。


要运行使用bashisms的bash命令,需要显式指定bash可执行文件,例如模拟bash进程替换:

#!/usr/bin/env python
import subprocess


subprocess.check_call('program <(command) <(another-command)',
shell=True, executable='/bin/bash')

subprocess.Popen()优先于os.system(),因为它提供了更多的控制和可见性。然而,如果你觉得subprocess.Popen()太冗长或复杂,peasyshell是我在上面写的一个小包装器,这使得它很容易从Python中与bash交互。

https://github.com/davidohana/peasyshell

复制粘贴:

def run_bash_command(cmd: str) -> Any:
import subprocess


process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
output, error = process.communicate()
if error:
raise Exception(error)
else:
return output