在Python中运行Bash命令


299

在我的本地计算机上,我运行一个包含此行的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)),但是在该错误出现之前,它将在终端中打印命令。然后我只是复制了输出,然后将复制粘贴到终端中,然后按回车,它就可以工作了...

有人知道发生了什么吗?


2
根据您的跑步方式,环境似乎有所不同cwm。也许您有一些配置.bashrc可以设置交互式bash使用环境?
Sven Marnach

在服务器上登录后,是否尝试从命令行运行命令?您的帖子只是说您“已将其粘贴到终端中”。
Sven Marnach

@Sven:是的,我的意思是我直接在服务器的终端中运行了命令
mkn 2010年

根据您的运行方式,PYTHONPATH似乎有所不同cwm。也许PATH有所不同,cwm并且调用了不同的版本。或不同版本的Python。真的很难在没有访问机器的情况下解决这个问题……
Sven Marnach 2010年

Answers:


314

不要使用os.system。不推荐使用它,而推荐使用subprocess。从文档:“此模块旨在取代旧的几个模块和功能:os.systemos.spawn”。

就像您的情况一样:

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

8
当我需要执行cd 'path\to\somewhere'随后需要在该地方运行的另一个bash命令时,这并没有实现我想要的功能。@ user225312
AWrightIV

36
@AWrightIV如果需要在特定的工作目录中运行子进程,则可以使用Popen的cwd参数:subprocess.Popen(..., cwd='path\to\somewhere')
防水

7
对于我的命令,这里需要shell = True;stackoverflow.com/questions/18962785/…–
user984003

4
在这种情况下,最好使用shlex.split()代替string.split()
Alexey Sviridov

4
...(stdout=file在这种情况下,将输出重定向到文件。它实现> file)。传递..., '>', 'file']期望重定向的最后一条命令是错误的(没有外壳程序将无法工作,如果使用外壳程序,则应将命令作为字符串传递)
jfs

186

为了稍微扩展此处的早期答案,通常会忽略许多细节。

  • 身高subprocess.run()subprocess.check_call()和朋友过subprocess.call()subprocess.Popen()os.system()os.popen()
  • 理解并可能使用text=True,又名universal_newlines=True
  • 了解shell=Trueor 的含义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与 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模块文档中就包含了建议优先subprocessos.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()否则通常应使用with 。

实际上,使用check=True或时subprocess.check_*,如果子进程返回非零退出状态,Python将抛出CalledProcessError异常

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

在另一方面,有一个共同的问题check_call()check_output()是谁盲目使用这些功能的用户,当异常发生时,如感到惊讶grep并没有找到匹配。(您可能应该grep仍然用本机Python代码替换,如下所述。)

所有事情都计算在内,您需要了解shell命令如何返回退出代码,以及在什么条件下它们将返回非零(错误)退出代码,并做出有意识的决定,如何精确地处理它。

了解并且可能使用text=Trueakauniversal_newlines=True

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

(如果差异不是立即显而易见的,则建议使用Ned Batchelder的实用Unicode(如果不是必须的话)阅读。如果您愿意,可以在链接后进行36分钟的视频演示,尽管您自己阅读页面的时间可能会大大减少。 )

深入地讲,Python必须获取bytes缓冲区并以某种方式解释它。如果它包含二进制数据的斑点,则不应将其解码为Unicode字符串,因为这是容易出错和引起错误的行为-正是这种讨厌的行为,使许多Python 2脚本充满了麻烦,之后才有办法正确区分编码文本和二进制数据。

使用text=True,您可以告诉Python您实际上希望以系统的默认编码返回文本数据,并且应将其解码为Python(Unicode)字符串,以达到Python的最佳能力(通常,UTF-8在不超过日期系统,也许除了Windows?)

如果您没有要求,Python只会bytesstdoutstderr字符串中提供字符串。也许稍后您确实知道它们是文本字符串,并且知道了它们的编码。然后,您可以解码它们。

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=False您将参数列表传递给操作系统,绕过了外壳程序。

当没有外壳时,您可以保存进程并摆脱相当多的隐藏复杂性,这些复杂性可能会也可能不会包含错误甚至安全问题。

另一方面,当您没有外壳程序时,就没有重定向,通配符扩展,作业控制和大量其他外壳程序功能。

一个常见的错误是使用shell=TruePython,然后仍将令牌列表传递给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)

常见的反驳“但对我有用”不是一个有用的反驳,除非您确切地了解它在什么情况下会停止工作。

重构实例

通常,shell的功能可以用本地Python代码替换。简单的Awk或sed脚本可能应该简单地翻译成Python。

为了部分说明这一点,这是一个典型但有些愚蠢的示例,其中涉及许多外壳功能。

cmd = '''while read -r x;
   do ping -c 3 "$x" | grep 'round-trip 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 'round-trip min/avg/max' in line:
                 print('{}: {}'.format(host, line))

这里要注意一些事情:

  • 随着shell=False你不需要引用的外壳需要大约字符串。无论如何用引号引起来可能是一个错误。
  • 在子流程中运行尽可能少的代码通常是有意义的。这使您可以从Python代码中更好地控制执行。
  • 话虽这么说,复杂的Shell管道非常繁琐,有时很难在Python中重新实现。

重构后的代码还以非常简洁的语法说明了shell实际为您做了多少-更好或更坏。蟒蛇说明确优于隐式,但Python代码相当冗长,可以说是看起来复杂得多,这确实是。另一方面,它提供了许多要点,您可以在其中进行控制,例如通过增强功能可以很容易地说明这一点,我们可以轻松地将主机名与shell命令输出一起包括在内。(这在shell中也绝非挑战性,但是以另一种转移和也许另一种过程为代价的。)

普通壳结构

为了完整起见,这里简要介绍了其中一些外壳程序功能,并提供了一些注释,说明如何用本地Python设施替换它们。

  • Globbing aka通配符扩展可以glob.glob()用python或类似的简单Python字符串比较代替,或者经常用for file in os.listdir('.'): if not file.endswith('.png'): continue。Bash具有各种其他扩展功能,例如.{png,jpg}大括号扩展和{1..100}波浪号扩展(~扩展到您的主目录,并且更广泛~account地扩展到另一个用户的主目录)
  • Shell变量(例如$SHELL$my_exported_var有时可以简单地用Python变量替换)。导出的shell变量可作为例如os.environ['SHELL'](的意思export是使变量提供给子进程-一个变量,它是不可用的子进程显然不会提供给Python的运行作为shell的子进程,反之亦然env=关键字subprocess方法的参数可让您将子流程的环境定义为字典,因此这是使Python变量对子流程可见的一种方法。与shell=False您将需要了解如何删除任何引号;例如,cd "$HOME"相当于os.chdir(os.environ['HOME'])目录名周围不带引号。(常常cd是不是有用的或必要的,无论如何,和很多新手忽略了双引号周围的变量,并摆脱它,直到有一天......
  • 重定向允许您从文件读取作为标准输入,并将标准输出写入文件。grep 'foo' <inputfile >outputfile打开outputfile以进行写入和inputfile阅读,并将其内容作为标准输入传递给grep,然后其标准输出进入outputfile。通常,用本机Python代码替换它并不难。
  • 管道是重定向的一种形式。echo foo | nl运行两个子进程,其中的标准输出echo是的标准输入nl(在OS级别,在类Unix系统中,这是一个文件句柄)。如果您无法用本机Python代码替换管道的一端或两端,则也许可以考虑使用外壳程序,尤其是在管道具有两个或三个以上进程的情况下(尽管请查看pipesPython标准库中的模块或多个更具现代性和多功能的第三方竞争对手)。
  • 作业控制使您可以中断作业,在后台运行它们,将它们返回到前台等。当然,Python也提供了停止和继续一个进程的基本Unix信号。但是作业是外壳程序中的一个更高层次的抽象,涉及流程组等,如果您想从Python中进行类似的工作,则必须理解。
  • 除非您了解所有内容基本上都是字符串,否则在外壳程序中进行报价可能会造成混淆。因此ls -l /等效于,'ls' '-l' '/'但文字的引号是完全可选的。包含外壳元字符的未加引号的字符串将进行参数扩展,空格标记化和通配符扩展;双引号可防止空格标记化和通配符扩展,但允许参数扩展(变量替换,命令替换和反斜杠处理)。从理论上讲这很简单,但是可能会令人困惑,尤其是在存在多层解释时(例如,远程shell命令)。

了解sh和Bash 之间的差异

subprocess/bin/sh除非另有明确要求,否则将使用shell命令运行外壳命令(当然,在Windows上除外,因为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')

A subprocess与它的父项分开,并且不能对其进行更改

一个常见的错误是做类似的事情

subprocess.run('foo=bar', shell=True)
subprocess.run('echo "$foo"', shell=True)  # Doesn't work

除了缺乏优雅之外,这还背叛了人们对“ subprocess”名称中“ sub”部分的根本性了解。

子进程与Python完全独立运行,完成时,Python不知道它做了什么(除了模糊的指示,它可以从子进程的退出状态和输出中推断出来)。孩子通常不能改变父母的环境;它不能设置变量,更改工作目录,也不能在没有上级合作的情况下与其上级通信。

在这种情况下,立即解决的办法是在一个子进程中运行两个命令。

subprocess.run('foo=bar; echo "$foo"', shell=True)

尽管显然,这种特定用例根本不需要外壳。请记住,您可以通过以下方式操纵当前进程的环境(因此也可以操纵其子进程)

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在单个进程中运行多个任务(它更轻巧,可以为您提供更多控制权,但同时也更多地限制了进程中的线程紧密耦合并绑定到单个GIL。)


2
有关如何避免将Python作为子进程调用的更详细说明,请参见与切向相似的问题的答案。
Tripleee 18/09/08

4
令我感到困惑的是,我不得不为这样一个基本问题发布新的答案,以展示如何惯用地从该问题运行命令。您的答案很长,但我看不到这样的例子。无关:避免货物培养。如果check_call()适合您的情况,请使用它。我必须修复run()盲目使用的代码。check=True如果使用check_call,缺失会导致一个可以避免的错误-名称中包含“ check”,您不能丢失它-这是正确的默认值:不要默默地忽略错误。我没有进一步阅读。
jfs

1
@jfs感谢您的反馈,我实际上打算添加有关Bash vs的部分,sh但您击败了我。我正在尝试详细说明细节,以帮助那些陷阱不明显的初学者,因此确实有些麻烦。否则,您应该足够。+1
Tripleee '18

是否stderr/stdout = subprocess.PIPE有比默认设置更高的性能开销?
斯金格斯

1
@Stringers我还没有测试过,但是我不明白为什么要这么做。如果您将这些管道连接到进行某些处理的对象,那么当然需要对处理进行说明;但它不会在管道本身中发生。默认设置是根本不捕获stdout或stderr,即,无论打印什么内容,都超出了Python的可见性和控制范围,就像使用一样os.system()
Tripleee

41

用子流程调用

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

您收到的错误似乎是由于服务器上没有交换模块,您应该在服务器上安装交换,然后再次运行脚本


3
swap模块显然在那里,因为从外壳运行命令即可。
Sven Marnach

2
不在服务器上,当他在服务器上运行该服务器时,出现导入错误。
雅各布·鲍耶

@mkn:“然后我只是复制了输出,然后将副本粘贴到终端中,然后按Enter键,它就可以工作了……”-您是在服务器上还是在计算机上尝试过?
Sven Marnach

您是否正在独立的计算机上运行此程序,但在服务器上运行它却无法正常工作?还是您可以在服务器终端上运行它,而不是在服务器本身上运行
Jakob Bowyer

1
这是错误的,如果您不使用shell=True,则应使用列表来传递多个参数,即使用['a', 'b', 'c']而不是'a b c'。尽管由于> file命令中的(shell重定向),幼稚的拆分将不起作用。更多详细信息
jfs

18

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

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

2
subprocess.check_output(bashCommand, shell=True)做同样的事情。如果您的命令是静态字符串,请尝试自己将其解析为列表,并避免使用shell=True;。尽管在这种情况下,你需要重定向壳反正,否则你需要将它重构为纯Python -with open('test.nt', 'w') as dest: output = subprocess.check_output(['cwm' ,'--rdf', 'test.rdf', '--ntriples'], stdout=dest, shell=False)
tripleee

@tripleee注意:(/bin/sh由子进程使用)不一定bash(您不能使用bashisms)。虽然可以executable='/bin/bash根据需要使用。这是一个代码示例
jfs

2
它是第一个答案在命令应该成功(接受和第二流行的答案是错的只是一个小问题开始:check_output()在这里没用(输出总是空的,由于> file重定向; 使用check_call()。而不是
JFS

16

您可以使用subprocess,但是我始终觉得这不是一种“ Pythonic”方式。因此,我创建了Sultan(无耻插件),使运行命令行功能变得容易。

https://github.com/aeroxis/sultan


3
做得好!比子流程更干净,更直观。
mjd2

非常感谢!我很高兴听到这个消息!
大卫·丹尼尔

2
应当诚实地将其纳入标准库。
Joshua Detwiler

1
有没有办法使用Sultan捕获终端的输出?
alvas

是的,你可以@alvas ......这里是如何做到这一点的文档:sultan.readthedocs.io/en/latest/...
大卫·丹尼尔·

7

根据错误,您在服务器上缺少名为swap的软件包。这/usr/bin/cwm需要它。如果您使用的是Ubuntu / Debian,请python-swap使用aptitude 安装。


但是当我直接在终端中运行它时它可以工作...所以交换必须在那里,不是吗?
mkn 2010年

有两种选择。它要么找不到,swap要么不该首先导入。你可以import swap手动吗?它有效吗?
kichik

嗯,我不能。如果我在终端中键入python来启动python,然后键入import swap,则会收到错误“ ImportError:No module named swap”。奇怪的是,当我直接在服务器终端中运行cwm命令时,它仍然可以工作
mkn 2010年

尝试在可行的sys.path地方打印,不在可行的地方打印。然后尝试在打印的文件夹中查找交换文件夹或swap.py。正如Sven所说,这些路径可能存在问题,这将帮助您解决问题。
kichik

4

您也可以使用“ os.popen”。例:

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

1
文档包含一个红色的大框:从2.6版开始不推荐使用:此功能已作废。请使用该subprocess模块。”
三人房

1
公平地讲,os.popen不再有此警告,现在只是一个薄薄的包装subprocess.Popen()
tripleee

4

要在没有外壳的情况下运行命令,请将命令作为列表传递,并使用[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)

这是外壳程序,负责输出重定向(> test.nt在命令中)。


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

#!/usr/bin/env python
import subprocess

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

也许提到.split()当用引号引起来的字符串等时,这是不够的。有一个单独的例程shlex.split()可以应对任意复杂的shell语法。
Tripleee '18

@tripleee .split()在这种情况下的作品。shlex.split()有时可能有用,但在某些情况下也可能失败。有很多事情可以提及。您可以从上面提供的子流程标签说明的链接开始。
jfs

0

执行此操作的pythonic方法是使用 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

不,最后一个示例echo -v '"Hello Again!"'与使用双引号引起来的单引号相同。
三人房

另外,要正确使用subprocesss.Popen,您必须管理生成的过程对象(至少执行一次操作wait()以防止它变成僵尸过程)。
三胞胎
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.