Linux 报 Argument list too long:用 find 和 Python 处理大量文件
目录里的文本文件越来越多,原来能运行的 wc -c data/*.txt 突然报 Argument list too long。给路径加引号可以保留空格,但不能解决展开后的参数总量超限。
本文面向初中级 Linux/Python 用户,用两个只读方法统计 .txt 文件的字节数:GNU 工具通过标准输入接收文件名,Python 则只接收目录路径,再在进程内部遍历文件。两种方法都能处理带空格和换行的文件名。
1. 方法选择:把文件列表移出命令参数
Python 的 errno 文档把 E2BIG 定义为参数列表过长。常见原因是 shell 在启动程序之前把通配符展开为大量路径;参数与环境占用的空间受到系统限制,单个参数也可能过长。不要把某台机器的 ARG_MAX 当成固定文件数量上限。
| 需求 | 方法 |
|---|---|
| 临时统计,系统有 GNU find 和 GNU wc | find -print0 配合 wc --files0-from=- |
| 输出 JSON、接入定时任务或增加业务逻辑 | Python 内部遍历目录 |
subprocess.run(["wc", "-c", *paths]) 虽然没有使用 shell,仍把所有路径放进一次进程启动的参数列表,也会遇到长度限制。已有命令的超时与错误处理可参考外部命令执行教程。
2. 准备环境与测试文件
需要 Linux、Bash、Python 3.9 或更新版本。方法一另外需要 GNU findutils 和 GNU coreutils;本文在 Python 3.11.4、GNU wc 9.7 下验证,无第三方 Python 依赖。若系统没有 GNU 工具,直接使用方法二,无需为此全局安装软件。
- 新建演示目录,重复运行时换一个目录名:
python3 --version
find --version
wc --version
mkdir argument-list-demo
cd argument-list-demo
python3 - <<'PY_DATA'
from pathlib import Path
root = Path("data")
root.mkdir()
for name, content in [("normal.txt", b"abc\n"),
("two words.txt", b"12\n"),
("line\nbreak.txt", b"xy\n")]:
(root / name).write_bytes(content)
(root / "ignore.csv").write_text("not counted\n", encoding="utf-8")
PY_DATA
三个文本文件共 10 字节。其中一个名称含空格,另一个名称真的包含换行;CSV 不计入。下面所有命令都在这个演示目录执行。
3. 方法一:用 NUL 分隔文件名
- 在 Bash 中执行:
set -o pipefail
find ./data -type f -name '*.txt' -print0 | wc -c --files0-from=-
最后一行的合计应为 10 total,排版空格可能不同。*.txt 必须加引号,模式由 find 处理;-print0 用 NUL 分隔路径,换行和空格不会拆开名称。GNU wc 的 --files0-from=- 从标准输入读取这些路径,命令本身只有固定数量的参数。
这些选项见 GNU find 文件名处理说明和 GNU wc 手册。pipefail 让 find 扫描失败也能使流水线失败;退出码非零时,不要使用已经打印出来的部分统计。
输出中的文件名可能跨行,不适合按空白或换行解析。没有匹配文件时不会提供可依赖的合计行;只有一个文件时也不一定有 total。机器读取结果时建议采用下面固定结构的 JSON 输出。
4. 方法二:让 Python 在进程内部遍历
- 将完整代码保存为
size_tree.py,也可下载 size_tree.py:
"""Summarize regular *.txt files without expanding filenames into argv."""
import argparse
import errno
import json
import os
from pathlib import Path
import stat
import sys
def stop_walk(error):
raise error
def summarize(directory):
root = Path(directory).resolve(strict=True)
if not root.is_dir():
raise NotADirectoryError(errno.ENOTDIR, "not a directory", str(root))
files = total = 0
for parent, _, names in os.walk(root, followlinks=False, onerror=stop_walk):
for name in names:
if not name.endswith(".txt"):
continue
info = (Path(parent) / name).lstat()
if stat.S_ISREG(info.st_mode):
files += 1
total += info.st_size
return {"files": files, "bytes": total}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("directory", type=Path)
args = parser.parse_args()
try:
report = summarize(args.directory)
except OSError as exc:
label = errno.errorcode.get(exc.errno, "OSERROR")
print(f"ERROR {label}: {exc}", file=sys.stderr)
return 1
print(json.dumps(report, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
- 查看帮助并统计:
python3 size_tree.py --help
python3 size_tree.py ./data
预期输出:
{"bytes": 10, "files": 3}
脚本只把目录作为一个参数传入,然后按目录遍历。它不会建立整棵目录树的文件列表,但 os.walk 仍会为当前目录生成名称列表,超大单层目录仍需内存。
os.walk 官方文档说明,扫描错误默认会被忽略;这里用 onerror 重新抛出异常,避免把漏扫当成成功。lstat() 与 S_ISREG 只计入普通文件,跳过文件符号链接,不进入目录符号链接。传入的根路径会先解析,若根本身是符号链接,会扫描其目标。
这里累加的是文件元数据中的逻辑字节数,不读取内容、不验证可读性,也不等于实际磁盘占用。GNU wc 需要能够读取文件,因此权限受限时两者的结果可能不同。硬链接按路径重复计数;备份前的容量预算可继续参考磁盘空间预检。
5. 验证特殊名称与失败退出
在同一目录运行下列自检。测试使用独立临时目录并自动清理,不改动演示数据:
python3 - <<'PY_CHECK'
from pathlib import Path
from tempfile import TemporaryDirectory
import json
import subprocess
import sys
from size_tree import summarize
script = Path("size_tree.py").resolve()
with TemporaryDirectory() as work:
root = Path(work)
assert summarize(root) == {"files": 0, "bytes": 0}
nested = root / "nested"
nested.mkdir()
for name in ("two words.txt", "line\nbreak.txt", "-option.txt"):
(nested / name).write_bytes(b"abc\n")
(root / "alias.txt").symlink_to(nested / "two words.txt")
(root / "loop").symlink_to(root, target_is_directory=True)
result = subprocess.run([sys.executable, str(script), str(root)],
capture_output=True, text=True, check=True)
assert json.loads(result.stdout) == {"files": 3, "bytes": 12}
for bad, label in [(root / "missing", "ENOENT"),
(nested / "-option.txt", "ENOTDIR")]:
result = subprocess.run([sys.executable, str(script), str(bad)],
capture_output=True, text=True)
assert result.returncode == 1 and not result.stdout, result
assert label in result.stderr, result.stderr
print("PASS: empty, nested, unusual names, symlinks, error exits")
PY_CHECK
应输出 PASS: empty, nested, unusual names, symlinks, error exits。退出码约定:成功为 0;路径或扫描失败为 1,且标准输出没有部分 JSON;参数用法错误由 argparse 返回 2。
6. 排错与接入自动化
| 现象 | 具体处理 |
|---|---|
wc: unrecognized option --files0-from |
当前 wc 不是兼容的 GNU 版本;使用 Python 方法。 |
仍出现 Argument list too long |
检查是否又用了 *.txt 展开、命令替换或一次性拼接所有路径;只传目录或走标准输入。 |
Python 输出 ERROR ENOENT / ENOTDIR |
检查实际工作目录和传入路径,传已有目录。 |
Python 输出 ERROR EACCES |
用任务实际账号检查目录及其父目录的访问权限;不要忽略扫描失败。 |
JSON 字节数与 du 不同 |
脚本统计逻辑大小,du 常统计磁盘块;两者口径不同。 |
接入定时任务时使用绝对解释器、脚本和数据目录路径,并检查退出码,具体见定时任务路径排障。不要写成 python3 size_tree.py ./data/*.txt,这个脚本接收的是一个目录。
两种方法都不是目录快照:扫描期间文件新增、删除、替换或写入,会影响结果。应在自己控制、内容稳定的目录中运行;followlinks=False 也不能防御不可信用户在扫描过程中替换路径。不要把这段统计代码直接改造成以高权限运行的递归删除器。
7. 小结
处理大量文件时,把文件列表放到标准输入或 Python 进程内部,就能避开一次性展开所有文件名的参数限制。使用 NUL 分隔符保留特殊名称,自动化场景则用 JSON 和明确退出码接收结果。先在小目录验证统计口径与失败行为,再扫描真实数据。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/linux-argument-list-too-long-python.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。