用 Python argparse 做文本搜索命令:互斥选项与退出码

举报
yd_210400787 发表于 2026/11/05 10:50:25 2026/11/05
【摘要】 用标准库 argparse 实现文本搜索命令,演示互斥选项、递归搜索、Unicode 大小写处理和 0/1/2 退出码,并提供可运行的完整示例。

临时脚本常从 sys.argv[1] 开始,参数一多就容易出现“少传一个参数导致索引错误”“两个输出模式同时开启”等问题。Python 标准库的 argparse 可以把参数定义、帮助文本和输入错误放在一起。下面做一个小型文本搜索命令:指定文件或目录和关键词;目录默认只搜索当前层的 .txt 文件,加 -r 才递归。

先约定命令的行为

  • python search_text.py a.txt python -i:忽略大小写,输出 文件:行号:内容。
  • --count:只输出匹配行数,同一行即使出现多次关键词也只计一次。
  • --names-only:只输出至少有一行匹配的文件名,每个文件只输出一次。
  • --count 与 --names-only 互斥;同时传入时由 argparse 报错。
  • 退出码 0 表示找到匹配,1 表示正常搜索但没有匹配,2 表示参数或读取错误。这让 shell 脚本能区分“没找到”和“命令出错”。

完整代码

保存为 search_text.py,只用 Python 标准库:

"""Search UTF-8 text files with explicit command-line exit codes."""

import argparse
import sys
from pathlib import Path


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="在 UTF-8 文本文件中查找关键词")
    parser.add_argument("path", type=Path, help="文件或目录")
    parser.add_argument("keyword", help="要查找的关键词")
    parser.add_argument("-r", "--recursive", action="store_true", help="递归搜索子目录")
    parser.add_argument("-i", "--ignore-case", action="store_true", help="忽略大小写")
    mode = parser.add_mutually_exclusive_group()
    mode.add_argument("--count", action="store_true", help="只输出匹配行数")
    mode.add_argument("--names-only", action="store_true", help="只输出有匹配的文件名")
    args = parser.parse_args()
    if not args.path.exists():
        parser.error(f"路径不存在:{args.path}")
    if not args.keyword:
        parser.error("关键词不能为空")
    return args


def files_under(path: Path, recursive: bool):
    if path.is_file():
        yield path
    elif path.is_dir():
        entries = path.rglob("*.txt") if recursive else path.glob("*.txt")
        yield from sorted(p for p in entries if p.is_file())
    else:
        raise OSError(f"不是普通文件或目录:{path}")


def main() -> int:
    args = parse_args()
    needle = args.keyword.casefold() if args.ignore_case else args.keyword
    matches = 0
    named_files = set()
    try:
        for path in files_under(args.path, args.recursive):
            with path.open("r", encoding="utf-8") as stream:
                for line_no, line in enumerate(stream, 1):
                    haystack = line.casefold() if args.ignore_case else line
                    if needle not in haystack:
                        continue
                    matches += 1
                    if args.count:
                        continue
                    if args.names_only:
                        if path not in named_files:
                            print(path)
                            named_files.add(path)
                    else:
                        print(f"{path}:{line_no}:{line.rstrip()}")
    except (OSError, UnicodeError) as exc:
        print(f"读取失败:{exc}", file=sys.stderr)
        return 2
    if args.count:
        print(matches)
    return 0 if matches else 1


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

add_mutually_exclusive_group() 负责检查两个输出模式,免去手写冲突判断。parser.error() 会显示用法和错误原因,并以状态码 2 退出;路径不存在和空关键词都在解析后检查。Path.glob("*.txt") 只查目录当前层,Path.rglob("*.txt") 查全部子目录。传入单个文件时,则直接处理该文件,不受扩展名限制。

这里用 casefold() 做不区分大小写的比较,它比简单的 lower() 更适合 Unicode 文本。逐行读取避免一次把大文件放进内存。读取时明确指定 UTF-8;如果文件编码不符或没有读取权限,程序打印错误并返回 2。实际业务中也可以按需求增加 --encoding,但不要悄悄忽略解码错误,否则搜索结果可能不完整。

运行核对

假设 a.txt 依次有 Python 很好、python 也好、其他内容 三行,sub/b.txt 有 PYTHON 第三行? 一行。在这两个文件的上级目录运行:

$ python search_text.py a.txt python -i
a.txt:1:Python 很好
a.txt:2:python 也好

$ python search_text.py . python -i --count
2

$ python search_text.py . python -i -r --count
3

$ python search_text.py . python -i -r --names-only
a.txt
sub/b.txt

第一条逐行输出,第二条只查当前目录,第三条递归后多找到子目录的一行。传入不存在的路径,或同时传 --count --names-only,都会得到退出码 2。若搜索成功但没有命中,--count 输出 0,进程退出码为 1。在自动化里可以据此决定继续处理、跳过,还是报错。

这段程序刻意保持小而完整:参数语义、错误提示和退出码都有明确约定。继续扩展时,建议先写清新选项与现有选项是否冲突,再添加实现,避免命令行接口随着脚本增长变得难用。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。