在Python中运行Bash命令

8 浏览
0 Comments

在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) 引起)。然后我只需要复制该输出,并将其粘贴到终端中并敲击回车键即可工作……

有谁知道究竟发生了什么吗?

admin 更改状态以发布 2023年5月22日
0
0 Comments

稍微扩展一下先前的答案,有几个常常被忽视的细节。

  • 优先使用subprocess.run(),接着是subprocess.check_call()subprocess.call(),然后是subprocess.Popen(),然后是os.system(),最后是os.popen()
  • 了解并且使用text=Trueuniversal_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=Truesubprocess.run()

在实践中,使用check=Truesubprocess.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只会在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 

这里需要注意的一些事情:

  • shell=False 时,你不需要像shell一样对字符串进行引用,多余的引号可能是错误的。
  • 在子进程中尽可能少地运行代码通常是有意义的。这样可以更好地从Python代码中控制执行。
  • 话虽如此,复杂的shell管道有时候很繁琐,重新在Python中实现也有些不易。

重构的代码也展示了shell为你提供了多少简洁语法 -- 好的或坏的,它说明了“明确要优于隐含”,然而Python代码相当冗长,可以说看起来比实际更复杂。然而,它提供了许多从其他地方获取控制权的点,就像我们通过增强可以轻松地将主机名和shell命令输出一起包含在内一样。(这在shell中并不是难做到的,但代价是增加另一个分支和另一个进程。)

常见的Shell结构

为了完整起见,下面是一些shell功能的简要解释以及如何使用本地Python库来代替它们的一些说明。

要了解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)。

0
0 Comments

不要使用 os.system。它已经被废弃了,而 subprocess 更好。来自 文档: "这个模块旨在取代几个旧模块和函数: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()

0