在 Python 中运行 Bash 命令

2024-11-20 08:44:00
admin
原创
8
摘要:问题描述:在我的本地机器上,我运行一个包含此行的 Python 脚本bashCommand = "cwm --rdf test.rdf --ntriples > test.nt" os.system(bashCommand) 这很好用。然后我在服务器上运行相同的代码并收到以下错误消息...

问题描述:

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

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


解决方案 1:

为了稍微扩展一下之前的答案,这里有许多经常被忽视的细节。

  • 喜欢subprocess.run()subprocess.check_call(),朋友过subprocess.call(),过subprocess.Popen()os.system()os.popen()

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

  • shell=True理解or的含义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).returncode(返回码总是 0)

  • subprocess.call()`subprocess在 Python 2.4 的原始模块 ( 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模块文档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()。在常规使用中,您通常应该检查进程是否成功完成, whichsubprocess.check_call()subprocess.check_output()do (后者还返回已完成子进程的标准输出)。同样,您通常应该使用check=Truewith ,subprocess.run()除非您特别需要允许子进程返回错误状态。

实际上,使用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 的能力将其解码为 Python(Unicode)字符串(在任何适度更新的系统上通常为 UTF-8,也许 Windows 除外?)

如果这不是您请求的返回内容,Python 只会在和字符串bytes中为您提供字符串。也许在稍后的某个时候,您确实知道它们毕竟是文本字符串,并且您知道它们的编码。然后,您可以对它们进行解码。stdout`stderr`

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'))

textPython 3.7为关键字参数引入了更简短、更具描述性和更易于理解的别名,之前该名称有些具有误导性universal_newlines

理解shell=Truevsshell=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 中如何使用单引号来生成该字符串值,其中单引号不是值本身的一部分。

重构示例

通常,shell 的功能可以用原生 Python 代码替换。普通的管道(greptrcut等)和简单的 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, text=True, capture_output=True, check=True)
print(results.stdout)

# Reimplement with shell=False
with open('hosts.txt') as hosts:
    for host in hosts:
        host = host.rstrip('
')  # drop newline
        ping = subprocess.run(
             ['ping', '-c', '3', host],
             text=True,
             stdout=subprocess.PIPE,
             check=True)
        for line in ping.stdout.split('
'):
             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

  • 有时,可以简单地用 Python 变量替换诸如$SHELL或 之类的Shell 变量。导出的 Shell 变量可用作例如( 的含义是使变量可供子进程使用——子进程不可用的变量显然也不会供作为 Shell 子进程运行的 Python 使用,反之亦然。方法的关键字参数允许您将子进程的环境定义为字典,因此这是一种使 Python 变量对子进程可见的方法)。您需要了解如何删除任何引号;例如,相当于目录名称周围没有引号。(通常没有用处或必要,许多初学者省略变量周围的双引号并侥幸逃脱,直到有一天……)$my_exported_var`os.environ['SHELL']exportenv=subprocessshell=Falsecd "$HOME"os.chdir(os.environ['HOME'])`cd

  • 重定向允许您从文件读取数据作为标准输入,并将标准输出写入文件。grep 'foo' <inputfile >outputfile打开outputfile进行写入和inputfile读取,并将其内容作为标准输入传递给grep,然后其标准输出进入outputfile。 这通常不难用本机 Python 代码替换。

  • 管道是一种重定向形式。echo foo | nl运行两个子进程,其中的标准输出echo是的标准输入nl(在操作系统级别,在类 Unix 系统中,这是一个文件句柄)。如果您无法用本机 Python 代码替换管道的一端或两端,也许还是考虑使用 shell,特别是如果管道有两个或三个以上的进程(尽管请查看pipesPython 标准库中的模块或许多更现代、更通用的第三方竞争对手)。

  • 作业控制允许您中断作业、在后台运行它们、将它们返回到前台等。停止和继续进程的基本 Unix 信号当然也可以从 Python 获得。但是作业是 shell 中涉及进程组等的更高级别的抽象,如果您想从 Python 执行此类操作,您必须了解它们。

  • 在 shell 中使用引号可能会让人感到困惑,除非您明白所有内容基本上都是字符串。因此,ls -l /这相当于,'ls' '-l' '/'但文字周围的引号完全是可选的。包含 shell 元字符的未加引号的字符串会进行参数扩展、空格标记和通配符扩展;双引号可防止空格标记和通配符扩展,但允许参数扩展(变量替换、命令替换和反斜杠处理)。这在理论上很简单,但可能会让人感到困惑,尤其是当存在多层解释时(例如,远程 shell 命令)。

sh了解和 Bash之间的区别

subprocess运行 shell 命令,/bin/sh除非您另有特别要求(当然,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')

Asubprocess与其父级是分开的,并且无法改变它

一个常见的错误是这样做

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在单个进程中运行多个任务的模块(它更轻量,可以让你更好地控制,但也受到更多限制,因为进程内的线程紧密耦合,并绑定到单个GIL。)

解决方案 2:

不要使用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()

解决方案 3:

使用子进程调用它

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

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

解决方案 4:

您可以使用 bash 程序,并使用参数 -c 来执行以下命令:

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

解决方案 5:

您可以使用subprocess,但我总是觉得这不是一种“Pythonic”的做法。所以我创建了 Sultan(无耻的插件),它可以轻松运行命令行函数。

https://github.com/aeroxis/sultan

解决方案 6:

您也可以使用 '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

解决方案 7:

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

解决方案 8:

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

#!/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.nt末尾没有。stdout=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')

解决方案 9:

复制粘贴此内容:

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

解决方案 10:

实现这个目的的 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

解决方案 11:

subprocess.Popen()更受欢迎,os.system()因为它提供了更多的控制和可见性。但是,如果你觉得它subprocess.Popen()太冗长或复杂,peasyshell我在上面写了一个小包装器,它可以轻松地从 Python 与 bash 交互。

https://github.com/davidohana/peasyshell

解决方案 12:

我非常喜欢https://github.com/amoffat/sh,通常我会指出这一点。但这次我想使用一个更简单的 shell 嵌入版本,名为pshlib https://gitlab.com/ewiger/pshlib

免责声明:我只是编写了一个稍微简单的库,现在我将通过提供另一种答案来说明它。

它允许将 shell 命令按照因果关系嵌入到你的 python 代码中。

您可以将单个较长的 bash 行拆分为嵌套的 python 类似多行语句:

res = psh(
    'VAR=world',
    """
        echo This is
            a multiline hello
            $VAR!
""").output
print(res)
excepted = 'This is a multiline hello world!
'
assert excepted == res

因此,您的答案将为:

def cwm(rdf_file="test.rdf", with_ntriples="--ntriples", output_file="test.nt"):
    res = psh(f"""
        cwm 
            --rdf {rdf_file}
            {with_ntriples}
            > {output_file}
    """).output
    print(res)

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用