在 Python 中运行 Bash 命令
- 2024-11-20 08:44:00
- admin 原创
- 33
问题描述:
在我的本地机器上,我运行一个包含此行的 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=True
with ,subprocess.run()
除非您特别需要允许子进程返回错误状态。
实际上,使用check=True
或 时,如果子进程返回非零退出状态,subprocess.check_*
Python 将抛出CalledProcessError
异常。
一个常见的错误subprocess.run()
是,check=True
如果子进程失败,则忽略下游代码失败,并感到惊讶。
另一方面,check_call()
和的一个常见问题check_output()
是,盲目使用这些函数的用户在出现异常时会感到惊讶,例如当grep
没有找到匹配项时。(grep
无论如何,您应该用本机 Python 代码替换,如下所述。)
总而言之,您需要了解 shell 命令如何返回退出代码,以及在什么条件下它们会返回非零(错误)退出代码,并有意识地决定应如何处理它。
理解并可能使用text=True
akauniversal_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'))
text
Python 3.7为关键字参数引入了更简短、更具描述性和更易于理解的别名,之前该名称有些具有误导性universal_newlines
。
理解shell=True
vsshell=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 代码替换。普通的管道(grep
或tr
或cut
等)和简单的 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']export
env=subprocess
shell=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,特别是如果管道有两个或三个以上的进程(尽管请查看pipes
Python 标准库中的模块或许多更现代、更通用的第三方竞争对手)。作业控制允许您中断作业、在后台运行它们、将它们返回到前台等。停止和继续进程的基本 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)
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 项目管理必备:盘点2024年13款好用的项目管理软件