为什么传递变量到subprocess.Popen时,尽管传递了参数列表,但仍无法工作?

21

我有一个脚本,通过subprocess.Popen调用另一个Python脚本。但是因为我将参数存储在变量中。

servers[server]['address']
servers[server]['port']
servers[server]['pass']

我无法执行该命令

p = subprocess.Popen(
    ["python mytool.py -a ", servers[server]['address'], "-x", servers[server]['port'], "-p", servers[server]['pass'], "some additional command"],
    shell=True,
    stdout=subprocess.PIPE
)

1
使用变量构建一个字符串作为命令,或将它们作为参数列表传递。 - beroe
在Python中调用Python作为子进程本身就是一种反模式;更好的解决方案通常是从子进程重构脚本,以便您可以import它并直接从父脚本调用它。确实存在一些情况下您真正需要一个子进程(例如,如果脚本使用信号,您需要在父进程中以不同方式处理它们),但更多时候您可能不应该这样做。 - tripleee
1
另外需要说明的是,正如subprocess文档中已经指出的那样,如果您的用例已经被更高级别的函数subprocess.run()和相关函数处理,则应避免使用Popen。基本上,除非您需要并行处理或与正在运行的进程进行交互,否则不要使用Popen(如果确实需要使用,请注意等待子进程等操作,因为Popen不会为您执行这些操作)。 - tripleee
6个回答

14

去掉shell=True。在Unix系统上,如果shell=TruePopen()的参数会被不同对待:(参考)

import sys
from subprocess import Popen, PIPE

# populate list of arguments
args = ["mytool.py"]
for opt, optname in zip("-a -x -p".split(), "address port pass".split()):
    args.extend([opt, str(servers[server][optname])])
args.extend("some additional command".split())

# run script
p = Popen([sys.executable or 'python'] + args, stdout=PIPE)
# use p.stdout here...
p.stdout.close()
p.wait()
请注意,在需要外部输入的命令中传递shell=True存在安全风险,正如文档中所述的警告所示。在文档中

13

当您调用subprocess.Popen时,可以传递字符串或列表作为要运行的命令。如果传递一个列表,则应以特定方式拆分其项。

在您的情况下,您需要将其拆分为类似以下的内容:

command = ["python",  "mytool.py", "-a", servers[server]['address'], 
           "-x", servers[server]['port'], 
           "-p", servers[server]['pass'], 
           "some",  "additional", "command"]
p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
这是因为如果你传递一个列表,Popen 就会假定你已经将命令行分割成单词(即最终出现在 sys.argv 中的值),因此它不需要这样做。
你的调用方式会尝试运行一个名为 "python mytool.py -a" 的二进制文件,这不是你想要的。
另一种解决方法是将所有单词连接成一个字符串(然后由 Popen 进行拆分——参见subprocess.list2cmdline)。但如果可能的话,最好使用列表版本——它可以更简单地控制命令行如何分割(例如,如果参数中有空格或引号等,则无需纠结于引号字符的引用)。

我用来存储输出的变量,在打印时为空(我猜这还没有被执行)。 - GaNi
我想你的意思是当你从p.stdout读取时,没有输出?那是因为命令没有被运行。 - babbageclunk
实际上,shell=True 可能会在这里搞混 - 除非你正在使用 globbing(例如扩展文件列表),否则最好将其关闭。 - babbageclunk
没有设置shell=true,脚本将无法执行,会显示“sh: mytool.sh命令未找到”。 - GaNi
在命令行中键入“which mytool.sh”时,将其完整路径输入到mytool.sh中。 - babbageclunk

5

您的问题在于第一个Popen参数是str类型。请将其替换为list类型。以下代码可以正常工作:

address = servers[server]['address']
port = servers[server]['port']
pass = servers[server]['pass']

command = "python mytool.py -a %s -x %d -p %s some additional command" % (address, port, pass)
p = subprocess.Popen(command.split(), stdout=subprocess.PIPE)
#        it is a list^^^^^^^^^^^^^^^  shell=False

如果从可信来源获取了command参数,您可以构造command并将其与shell=True一起使用,如下所示:
import pipes as p
command = "python mytool.py -a {} -x {} -p {} some additional command".format(p.quote(address), p.quote(port), p.quote(pass))
p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)

注意1:使用shell=True构建的command可能存在安全漏洞。使用pipes.quote()可减少注入风险。
注意2:自python2起,pipes.quote()已被弃用;对于python3,请使用shlex模块。


之前遇到了类似的问题,但现在已经解决了。谢谢 :) - GaNi
1
如果您传递一个带有 shell=True 的列表,则第一个参数被视为 shell 脚本,后续参数作为该脚本的参数 ($0, $1 等)。此 shlex.split() 操作的结果并不是用于解析的列表;实际上,它将运行不带参数的 python - Charles Duffy
1
第一部分有很大的改进。关于第二部分,最好演示如何使用pipes.quote()(或在Python 3中使用shlex.quote())来创建可以安全地替换到shell命令中的内容,而不是只是放入带有许多警告的不安全代码。 - Charles Duffy
@CharlesDuffy,更新了。现在我无法检查最后的更新。请审核一下,谢谢。 - Michael Kazarian

1
你应该将命令连接成一个完整的字符串:
p = subprocess.Popen("python mytool.py -a " + servers[server]['address'] + " -x " + servers[server]['port'] + " -p " + servers[server]['pass'] + " some additional command", shell=True, stdout=subprocess.PIPE)

我已经将输出保存到变量中,但没有任何输出或脚本执行的迹象。可能将直接错误输出以进行检查? - GaNi
请添加 "stderr=subprocess.PIPE" - ciphor
如果有人给你提供了一个包含 $(rm -rf ~) 的地址列表,你最好希望没有人使用这种基于连接的方法。这是极其不安全的;即使您确实想要使用 shell=True,唯一安全的方法是从脚本代码中传递变量:p = subprocess.Popen(['python mytool.py -a "$1" -x "$2" -p "$3" some additional command', '_', str(servers[server]['address']), str(servers[server]['port']), str(servers[server]['pass'])], shell=True, stdout=subprocess.PIPE) - Charles Duffy

0

你可以通过传递参数来设置环境

env={whatever_you_want: values} | os.environ

在你的子进程调用中。


-1

使用案例

运行n个shell脚本或python脚本

创建一个run.py文件,负责将参数(整数)传递给shell文件。

假设您想要运行n(4)个接受1个参数的shell脚本或python脚本。

创建一个名为run.python的文件,并使用以下代码:

下面的代码说明了:

  • instanceQty = 要运行的shell脚本数量
  • os.getcwd() = 当前文件的路径
  • mockScript.sh = shell脚本,我将其放在与run.py相同的目录中

要运行shell脚本

# this is python file with name run.py
import subprocess,os
instanceQty = 4
for i in range(0, instanceQty):
    print(os.getcwd())
    subprocess.Popen(f"{os.getcwd()}/mockScript.sh {i}",shell=True,executable='/bin/bash')

运行Python脚本

import subprocess,os,sys
instanceQty = 4
for i in range(0, instanceQty):
    print(os.getcwd())
    subprocess.Popen([sys.executable,f"{os.getcwd()}/mockScript.py",str(i)])
    

使用以下命令运行此文件

python run.py

在MacOS上出现权限问题

sudo chmod ug+x mockScript.sh

sudo chmod ug+x run.py

所有代码已在Python 3.8.1和MacOs 12.0.1环境下测试通过。


在这里调用 os.getcwd() 是完全不必要的;请参阅 什么是当前工作目录? - tripleee
“权限问题”似乎也是初学者的一个误解。如果您明确运行 python script.py,则只需要对 script.py 具有读取访问权限(尽管为了方便交互使用,您可能还希望脚本具有 shebang 并标记为可执行)。 - tripleee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接