You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 05:55:55