春江暮客

春江暮客的个人学习分享网站

Python subprocess 通配符不展开:参数列表、空格文件名与输出重定向

2026-11-03 技术
Python subprocess 通配符不展开:参数列表、空格文件名与输出重定向

终端里能用的 *.txt,放进 Python 的 subprocess.run() 后却原样传给了程序;把 > 或 | 放进参数列表,也没有产生文件或管道。通常不是 Python 找不到文件,而是把 shell 的语法当成了程序参数。

本文面向初级到中级读者,先打印子进程实际收到的参数,再用 Python 显式选择文件、逐个执行统计,并将结果重定向到文件。全程保持 shell=False,不拼接 shell 命令。范围是可信本地目录中的小型 UTF-8 文本文件,不是任意外部命令的安全执行框架。

1. 方法:先区分参数与 shell 语法

Python 的 subprocess 参数说明区分了直接执行程序与通过 shell 执行。这里用参数列表:每个元素就是一个参数,空格不需要额外引号,通配符、变量与重定向符号不会由 shell 解释。

处理方式很直接:文件匹配交给 glob,文件输出交给 stdout,固定 Python 子程序用 sys.executable 启动。遇到可执行程序找不到,先参考 PATH 与命令预检,不要通过切换 shell=True 掩盖问题。

2. 准备:创建小型输入目录

使用已有的 Python 3.10 或更新版本,无第三方依赖。本文命令在 Linux、Python 3.11.4 下验证,适用于 bash/zsh。所有操作都在新目录内进行;重复实验时更换目录名,避免覆盖旧样例。

python3 --version
mkdir subprocess-argv-demo
cd subprocess-argv-demo
mkdir inputs
printf 'alpha\nbeta\n' > 'inputs/report one.txt'
printf 'gamma\n' > inputs/second.txt
printf 'hidden\n' > inputs/.private.txt

三个文件中,.private.txt 用于验证隐藏文件策略。将后续三个脚本保存在当前 subprocess-argv-demo 目录,不要放进 inputs。

3. 复现:打印子进程收到的内容

  1. 将以下代码保存为 show_argv.py:
import json
import sys

print(json.dumps(sys.argv[1:], ensure_ascii=True))
  1. 执行参数探针:
python3 - <<'PY_PROBE'
import subprocess
import sys

result = subprocess.run(
    [sys.executable, "show_argv.py", "*.txt", "report one.txt",
     "|", "$HOME", ">", "result.txt"],
    shell=False, check=True, capture_output=True,
    text=True, encoding="utf-8", timeout=5,
)
print(result.stdout, end="")
PY_PROBE

输出如下,且不会创建 result.txt:

["*.txt", "report one.txt", "|", "$HOME", ">", "result.txt"]

report one.txt 保持为一个参数。列表里的 "report one.txt" 是 Python 字符串写法,不是传给程序的引号;不要再在字符串里添加一层 shell 引号。| 在这里也只是普通字符,不会连接两个程序。

4. 实现:显式匹配,再逐个调用

  1. 将单文件统计器保存为 count_text.py:
"""Count nonempty lines in one trusted UTF-8 text file. Python 3.10+."""
import argparse
import json
from pathlib import Path
import sys


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("path", type=Path)
    args = parser.parse_args()
    try:
        with args.path.open(encoding="utf-8") as handle:
            count = sum(1 for line in handle if line.strip())
    except (OSError, UnicodeError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(json.dumps({"file": args.path.name, "nonempty_lines": count},
                     ensure_ascii=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
  1. 将批量入口保存为 batch_count.py,与统计器放在一起:
"""Count top-level non-hidden *.txt files without invoking a shell."""
import argparse
import glob
import json
from pathlib import Path
import subprocess
import sys


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("directory", type=Path)
    args = parser.parse_args()
    worker = Path(__file__).resolve().with_name("count_text.py")
    try:
        root = args.directory.resolve(strict=True)
        if not root.is_dir():
            raise ValueError("input must be a directory")
        names = sorted(glob.glob("*.txt", root_dir=root))
        if not names:
            raise ValueError("no matching *.txt files")
        results = []
        for name in names:
            path = root / name
            if not path.is_file():
                raise ValueError(f"not a regular file: {name!r}")
            result = subprocess.run(
                [sys.executable, str(worker), "--", str(path)],
                shell=False, stdin=subprocess.DEVNULL,
                capture_output=True, text=True, encoding="utf-8",
                check=True, timeout=10,
            )
            results.append(json.loads(result.stdout))
    except subprocess.CalledProcessError as exc:
        print(f"ERROR: worker exit={exc.returncode}; "
              f"{exc.stderr.strip()}", file=sys.stderr)
        return 1
    except (OSError, ValueError, subprocess.TimeoutExpired) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(json.dumps({"files": results}, ensure_ascii=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

这里的 *.txt 是代码中的固定匹配规则,不是用户输入的 shell 表达式。按 glob 官方文档,root_dir 指定搜索根目录;结果需要自行排序。这种模式只匹配顶层非隐藏名称,不递归,也不展开 ~。因此 .private.txt 不参与统计。

每次只传一个绝对文件路径,避免把全部文件挤进一次调用。-- 是本例统计器的 argparse 选项终止标记,不是所有工具都支持的通用协议。目录匹配为 *.txt 时会被明确拒绝;指向普通文件的符号链接会被接受,所以这不是路径隔离工具。

  1. 查看用法并执行:
python3 batch_count.py --help
python3 batch_count.py inputs

第二条命令输出:

{"files": [{"file": "report one.txt", "nonempty_lines": 2}, {"file": "second.txt", "nonempty_lines": 1}]}

没有匹配文件返回 1,不将“未处理任何输入”当成成功;参数错误返回 2。子程序失败、读取失败或超时返回 1,错误写入 stderr。父程序只有在全部文件处理成功后才输出 JSON,避免把半份批次报告当成完整结果。

5. 输出文件:用 stdout,而不是参数里的大于号

在同一目录运行:

python3 - <<'PY_REDIRECT'
from pathlib import Path
import subprocess
import sys

with Path("counts.json").open("xb") as output:
    subprocess.run(
        [sys.executable, "batch_count.py", "inputs"],
        shell=False, stdin=subprocess.DEVNULL,
        stdout=output, check=True, timeout=30,
    )
PY_REDIRECT

xb 要求 counts.json 尚不存在,防止覆盖旧报告;重复运行应换新文件名。重定向在启动前就打开文件,子程序失败时可能留下空文件,不能只凭文件存在判断成功。正式更新需要保留旧版本的报告时,参考 Python 原子写入 JSON。

这里让 stderr 留在终端,只有 stdout 写进 JSON 文件。后续步骤必须检查退出码;不要在命令失败后继续解析残留报告。

6. 验证:成功、隐藏文件与空匹配

将下面代码保存为 check_demo.py,运行 python3 check_demo.py。它依赖第 2 节原样创建的输入,以及第 3 节未生成 result.txt 的状态。

import json
from pathlib import Path
import subprocess
import sys

probe = subprocess.run(
    [sys.executable, "show_argv.py", "*.txt", "report one.txt",
     "|", "$HOME", ">", "result.txt"],
    capture_output=True, text=True, encoding="utf-8", check=True, timeout=5,
)
assert json.loads(probe.stdout) == [
    "*.txt", "report one.txt", "|", "$HOME", ">", "result.txt",
]
result = subprocess.run(
    [sys.executable, "batch_count.py", "inputs"],
    capture_output=True, text=True, encoding="utf-8", timeout=30,
)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {"files": [
    {"file": "report one.txt", "nonempty_lines": 2},
    {"file": "second.txt", "nonempty_lines": 1},
]}
assert not Path("result.txt").exists()
Path("empty").mkdir(exist_ok=True)
failed = subprocess.run(
    [sys.executable, "batch_count.py", "empty"],
    capture_output=True, text=True, encoding="utf-8", timeout=30,
)
assert failed.returncode == 1
assert failed.stdout == ""
assert "no matching *.txt files" in failed.stderr
assert Path("inputs/.private.txt").read_text() == "hidden\n"
print("PASS: exact arguments, counts, hidden-file exclusion, empty-match failure")

成功时输出 PASS: exact arguments, counts, hidden-file exclusion, empty-match failure。空匹配测试刻意触发失败,不会产生成功 JSON。该检查不修改原始输入,只创建一个空测试目录。

7. 排错与边界

现象 原因与操作
程序提示找不到 *.txt 直接传入了通配符;先在 Python 中匹配,再传具体路径。
文件名多了引号 把 shell 引号写进了参数值;移除字符串内的额外引号。
no matching *.txt files 检查目录、扩展名大小写和隐藏文件;本例不递归。
ERROR: worker exit=1 查看紧随其后的读取错误;修复文件编码、权限或缺失的 count_text.py 后重跑。
counts.json 已存在 保留旧文件,改用新的输出文件名。
想在列表里添加 ` `

捕获输出会占用内存,本例子程序每次仅返回一小条 JSON,但一行输入仍可能很长,最终报告也随文件数增长。文件可能在匹配后被改动;请在停止写入的可信目录使用,不把它当快照或完整性审计。超时和日志的进一步处理见 subprocess 超时、错误与日志文件。

shell=False 不会限制程序权限,也不能阻止被调用工具将参数解释为选项。本文只启动自己编写的 Python 脚本;不要改成拼接不可信字符串并开启 shell,相关边界见 Python subprocess 安全说明。

8. 小结

先打印真实参数,再决定哪些步骤需要由 Python 实现。匹配文件、调用程序和重定向输出分别处理,不让 shell 语法混进参数列表。接入自动化前,验证成功、空匹配与失败退出,再消费输出报告。

标签

1024 12306 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management filecmp filenames files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics multiprocessing mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode urllib usdt utf-8 uv venv vhhbert vite web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它