用 Python argparse 做文本搜索命令:互斥选项与退出码
临时脚本常从 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。在自动化里可以据此决定继续处理、跳过,还是报错。
这段程序刻意保持小而完整:参数语义、错误提示和退出码都有明确约定。继续扩展时,建议先写清新选项与现有选项是否冲突,再添加实现,避免命令行接口随着脚本增长变得难用。
- 点赞
- 收藏
- 关注作者
评论(0)