使用Python subprocess.run跨目录调用脚本时pytest执行报错求助
我用Python的subprocess.run调用和源文件目录较远的.sh和.py文件,由于两个目录仅通过该方式关联,直接导入不现实。简化后的目录结构如下:
parent-dir/ ├── dirA │ ├── main.py │ └── main_test.py └── dirB ├── app.sh └── utility.py
各文件代码
main.py
from pathlib import Path from typing import Tuple, Optional import subprocess import sys dir_a_path = Path(__file__).resolve().parent # 获取父目录 parent_dir = dir_a_path.parent dir_b_path = str(parent_dir.joinpath("dirB")) def execute_bash_command(command: str, cwd: Optional[str]=None) -> Tuple[int, str, str]: """在新shell中运行bash命令 exit_code, stdout, stderr = execute_bash_command(command) 参数: command (str): 要执行的命令 cwd (Optional[str]): 命令执行的工作目录,替代`cd some_dir/ && command`写法 返回: Tuple[int, str, str]: 退出码、标准输出、标准错误 """ try: print(f"Executing {command} from cwd: {cwd}") output = subprocess.run(command, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, shell=True) stdout = output.stdout.decode("utf-8").strip() print(stdout) return (output.returncode, stdout, "") except subprocess.CalledProcessError as cpe: print(f"error: {cpe}") stdout = cpe.stdout.decode("utf-8").strip() stderr = cpe.stderr.decode("utf-8").strip() return (cpe.returncode, stdout, stderr) def call_app() -> Tuple[int, str, str]: command = [ "./app.sh", "install" ] command = " ".join(command) return execute_bash_command(command=command, cwd=dir_b_path) def call_utility() -> Tuple[int, str, str]: command = [ sys.executable, "greet" ] command = " ".join(command) return execute_bash_command(command=command, cwd=dir_b_path)
main_test.py
from main import call_app, call_utility def test_call_app(): code, stdout, stderr = call_app() assert code == 0 assert stdout == "Installing application..." def test_call_utility(): code, stdout, stderr = call_utility() assert code == 0 assert stdout == "running command greet" def test_execute_bash_command(): """ 创建测试文件 """ exit_code, stdout, _ = execute_bash_command("touch tmp/test.txt") assert exit_code == 0
app.sh
#!/bin/bash if [ "$1" = "greet"]; then echo "Hello, world!" elif [ "$1" = "install"]; then echo "Installing application..." else echo "Usage: $0 {greet|install}" exit 1 fi
utility.py
import sys if __name__ == "__main__": print(f"running command {sys.argv[1]}")
错误情况
直接在终端运行python utility.py greet或./app.sh install一切正常,但用python -m pytest执行main_test.py时,出现子进程退出码异常:
错误信息1:
Executing ./app.sh install from cwd: /mnt/c/Users/XXX/source/python-testing/parent-dir/dirB error: Command './app.sh install' returned non-zero exit status 1.
错误信息2:
Executing /usr/bin/python3 greet from cwd: /mnt/c/Users/XXX/source/python-testing/parent-dir/dirB error: Command '/usr/bin/python3 greet' returned non-zero exit status 2.
PowerShell和WSL2环境下运行pytest都会出现该问题,与实际代码库中的错误一致。想了解无法正常运行的原因,subprocess.run的cwd参数是否使用错误,并希望得到这类代码的重构指导。
1. 修复app.sh的语法错误
app.sh的条件判断存在语法问题:if [ "$1" = "greet"]; then中,双引号与右方括号]之间缺少空格。Bash的test命令([ ])要求括号内的每个元素必须用空格分隔,否则会被识别为无效表达式,导致脚本进入else分支返回退出码1。正确写法:
#!/bin/bash if [ "$1" = "greet" ]; then echo "Hello, world!" elif [ "$1" = "install" ]; then echo "Installing application..." else echo "Usage: $0 {greet|install}" exit 1 fi
2. 修复call_utility的命令缺失
call_utility中的命令缺少要执行的脚本名utility.py,当前命令相当于运行python greet,Python会将greet视为模块名查找,找不到则返回退出码2。正确命令需包含脚本文件名:
def call_utility() -> Tuple[int, str, str]: command = [ sys.executable, "utility.py", "greet" ] command = " ".join(command) return execute_bash_command(command=command, cwd=dir_b_path)
3. subprocess.run的cwd参数使用说明
你的cwd参数用法是正确的,它确实会让子进程在指定目录下执行命令。但不建议使用shell=True+字符串拼接命令,这种方式存在安全风险(若命令包含用户输入易触发注入),且容易出现空格转义问题。更安全的方式是传递命令列表,并保持shell=False(默认值):
修改execute_bash_command函数以支持命令列表:
def execute_bash_command(command: str | list[str], cwd: Optional[str]=None) -> Tuple[int, str, str]: """执行命令 exit_code, stdout, stderr = execute_bash_command(command) 参数: command (str | list[str]): 要执行的命令(优先使用列表形式提升安全性) cwd (Optional[str]): 命令执行的工作目录 返回: Tuple[int, str, str]: 退出码、标准输出、标准错误 """ try: print(f"Executing {command} from cwd: {cwd}") # 根据命令类型选择执行方式 if isinstance(command, str): output = subprocess.run(command, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, shell=True) else: output = subprocess.run(command, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) stdout = output.stdout.decode("utf-8").strip() print(stdout) return (output.returncode, stdout, "") except subprocess.CalledProcessError as cpe: print(f"error: {cpe}") stdout = cpe.stdout.decode("utf-8").strip() stderr = cpe.stderr.decode("utf-8").strip() return (cpe.returncode, stdout, stderr)
调用部分直接传递命令列表:
def call_app() -> Tuple[int, str, str]: command = [ "./app.sh", "install" ] return execute_bash_command(command=command, cwd=dir_b_path) def call_utility() -> Tuple[int, str, str]: command = [ sys.executable, "utility.py", "greet" ] return execute_bash_command(command=command, cwd=dir_b_path)
4. 其他重构建议
- 保留
Path对象而非字符串路径:dir_b_path可直接保留为Path类型,subprocess.run支持Path作为cwd参数,避免字符串路径拼接错误:dir_b_path = parent_dir.joinpath("dirB") # 无需转换为str - 调试时打印
stderr:在测试函数中添加print(stderr),可快速查看子进程的错误输出,定位问题。 - 给
app.sh添加可执行权限:运行chmod +x dirB/app.sh,确保脚本可直接执行,否则需用bash app.sh install形式调用。
内容的提问来源于stack exchange,提问作者Lacrosse343

