在Python中运行Bash命令

2024-04-19 06:30:25 发布

您现在位置:Python中文网/ 问答频道 /正文

在本地计算机上,我运行一个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)引起),但在错误之前,它会在终端中打印命令。然后我复制了输出并复制粘贴到终端,点击回车键,它就工作了。。。

有人知道发生了什么事吗?


Tags: testimport命令脚本终端os计算机错误
3条回答

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

  • 喜欢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()函数是一个低级的工作程序,但是要正确使用它是很困难的,并且最终会复制/粘贴多行代码。。。它已经作为一组用于各种目的的高级包装函数方便地存在于标准库中,下面将详细介绍这些函数。

这是documentation中的一段:

The recommended approach to invoking subprocesses is to use the run() function for all use cases it can handle. For more advanced use cases, the underlying Popen interface can be used directly.

不幸的是,这些包装函数的可用性在不同的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 vssubprocess.Popen()

重构和扩展的subprocess.run()比它替换的旧的遗留函数更具逻辑性和通用性。它返回一个^{}对象,该对象具有各种方法,允许您从完成的子流程中检索退出状态、标准输出以及其他一些结果和状态指示器。

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

或许应该强调的是,仅仅subprocess.Popen()只是创造了一个过程。如果不这样做,就会有一个子进程与Python同时运行,因此是一个“后台”进程。如果它不需要输入或输出,也不需要与您协调,那么它可以与您的Python程序并行地执行有用的工作。

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

从TimeEternal(嗯,从Python2.5开始)以来,^{} module documentation包含了一个建议,建议您更喜欢subprocess,而不是os.system()

The subprocess module provides more powerful facilities for spawning new processes and retrieving their results; using that module is preferable to using this function.

system()的问题在于它明显依赖于系统,并且不提供与子流程交互的方式。它只是简单地运行,标准输出和标准错误超出了Python的能力范围。Python接收到的唯一信息是命令的退出状态(零表示成功,但非零值的含义也有些依赖于系统)。

PEP-324(上面已经提到过)包含了一个更详细的理由,解释了为什么os.system有问题,以及subprocess如何试图解决这些问题。

os.popen()过去更是strongly discouraged

Deprecated since version 2.6: This function is obsolete. Use the subprocess module.

但是,自Python 3中的某个时候起,它被重新实现为只使用subprocess,并重定向到subprocess.Popen()文档以获取详细信息。

了解并经常使用check=True

您还将注意到subprocess.call()有许多相同的limi命名为os.system()。在常规使用中,通常应该检查进程是否成功完成,这两个进程分别是subprocess.check_call()subprocess.check_output()(后者还返回已完成子进程的标准输出)。类似地,除非特别需要允许子进程返回错误状态,否则通常应该将check=Truesubprocess.run()一起使用。

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

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

另一方面,check_call()check_output()的一个常见问题是,当出现异常时(例如,grep找不到匹配时),盲目使用这些函数的用户会感到惊讶。(无论如何,您可能应该用本机Python代码替换grep,如下所述。)

总而言之,您需要了解shell命令如何返回退出代码,以及在什么情况下它们将返回非零(错误)退出代码,并有意识地决定应该如何处理它。

理解并可能使用text=True又名universal_newlines=True

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

(如果差异不明显,建议阅读Ned Batchelder的Pragmatic 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,则保存一个进程并除去一个fairly substantial amount of hidden complexity, which may or may not harbor bugs or even security problems.

另一方面,如果没有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)

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

重构示例

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

为了部分地说明这一点,这里有一个典型但有点傻的例子,它涉及许多shell特性。

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您不需要shell在字符串周围需要的引号。不管怎样,引用可能是个错误。
  • 在子流程中运行尽可能少的代码通常是有意义的。这使您可以从Python代码中对执行进行更多的控制。
  • 尽管如此,复杂的shell管道是乏味的,有时在Python中重新实现是一项挑战。

重构后的代码还用非常简洁的语法说明了shell到底能为您带来多少好处——不管好坏。Python说显式优于隐式,但是Python代码相当冗长,而且可以说看起来比实际情况更复杂。另一方面,它提供了许多点,您可以在其他内容的中间获取控制权,我们可以轻松地将主机名与shell命令输出一起包括在内的增强就是一个很好的例子。(这也绝非空穴来风,而是以另一种转移注意力或另一种过程为代价。)

普通外壳结构

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

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

了解sh和Bash之间的区别

subprocess使用/bin/sh运行shell命令,除非您特别请求(当然,在Windows上除外,它使用COMSPEC变量的值)。这意味着various Bash-only features like arrays, ^{} etc不可用。

如果只需要使用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('foo=bar', shell=True)
subprocess.run('echo "$foo"', shell=True)  # Doesn't work

这除了缺乏优雅之外,还暴露出对“子流程”这个名称的“子”部分缺乏基本的理解。

子进程完全独立于Python运行,当它完成时,Python不知道它做了什么(除了它可以从子进程的退出状态和输出推断出的模糊指标)。子级通常不能更改父级的环境;它不能设置变量、更改工作目录,或者用这么多的话来说,如果没有父级的合作,就不能与父级通信。

在这种特殊情况下,最直接的解决方法是在单个子进程中运行这两个命令

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

尽管很明显,这个特殊的用例根本不需要shell。记住,您可以通过

os.environ['foo'] = 'bar'

或将环境设置传递给

subprocess.run('echo "$foo"', shell=True, env={'foo': 'bar'})

(更不用说明显的重构subprocess.run(['echo', 'bar']);但是echo是一个很糟糕的例子,当然,首先要在子流程中运行某些东西)。

不要从Python运行Python

这是一个有点可疑的建议;在某些情况下,将Python解释器作为Python脚本的子进程运行确实有意义,甚至是绝对必要的。但通常情况下,正确的方法只是将另一个Python模块放入调用脚本并直接调用其函数。

如果另一个Python脚本在您的控制之下,并且它不是一个模块,请考虑turning it into one。(这个答案已经太长了,所以我不会在这里详细讨论。)

如果需要并行,可以在子进程中使用^{} module.运行Python函数,也可以使用^{}在单个进程中运行多个任务(这样更轻量级,可以提供更多控制,但也更受限制,因为进程中的线程是紧密耦合的,并且绑定到单个GIL

不要使用os.system。它已被弃用,取而代之的是subprocess。从docs:“这个模块打算替换几个旧的模块和函数: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()

用子流程调用它

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

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

相关问题 更多 >