568 Python CLI 工具开发:argparse 实战¶
什么是 CLI 工具?为什么要把 Python 脚本变成 CLI?¶
白话解释¶
CLI(Command-Line Interface,命令行界面)工具就是你在终端里直接敲命令就能用的程序。
想象一下你现在的工作方式:每次要过滤丰度表,你得打开 Python 脚本,找到文件路径那行代码,手动改成新的路径,再运行。这就像每次做饭都要重新画一遍菜谱一样麻烦。
而 CLI 工具的方式是:你直接在终端敲一行命令,告诉它输入文件是什么、阈值是多少、输出到哪里,它就帮你搞定了。就像一个训练好的厨师,你只要点菜就行。
对比:改代码 vs 命令行¶
# 改代码方式(每次都要编辑脚本)
# 1. 打开 filter_abundance.py
# 2. 找到第 15 行,把 input_file = "old_data.tsv" 改成 "new_data.tsv"
# 3. 运行 python filter_abundance.py
# CLI 工具方式(直接在终端用)
python filter_abundance.py --input new_data.tsv --threshold 0.01 --output filtered.tsv
# 甚至可以直接:
# filter_abundance --input new_data.tsv --threshold 0.01 --output filtered.tsv
为什么生信工程师需要会写 CLI 工具?¶
- 复用性:同一个脚本,不同数据直接换参数就行,不用改代码
- 自动化:可以嵌入 Shell 脚本和流程管理工具(如 Snakemake)
- 协作:同事不需要看你的代码,看
--help就知道怎么用 - 专业:面试官看到你会写规范的 CLI 工具,会觉得你有工程素养
argparse 基础¶
argparse 是 Python 标准库自带的命令行参数解析模块(不需要额外安装),从 Python 3.2 开始就有了。它能帮你: - 自动解析命令行参数 - 自动生成 --help 帮助信息 - 自动做参数类型检查
最简示例¶
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""最简单的 argparse 示例"""
import argparse # 导入命令行参数解析模块
# 1. 创建解析器对象,description 是 --help 时显示的描述
parser = argparse.ArgumentParser(
description="一个简单的生信 CLI 工具示例"
)
# 2. 添加参数
parser.add_argument("input_file", help="输入文件路径") # 位置参数(必填,不加 --)
# 3. 解析参数
args = parser.parse_args() # 把命令行输入的参数解析成对象
# 4. 使用参数
print(f"你输入的文件是:{args.input_file}") # 通过 args.属性名 访问
运行效果:
$ python demo.py sample.fastq
你输入的文件是:sample.fastq
$ python demo.py --help
usage: demo.py [-h] input_file
一个简单的生信 CLI 工具示例
positional arguments:
input_file 输入文件路径
options:
-h, --help show this help message and exit
位置参数 vs 可选参数¶
import argparse
parser = argparse.ArgumentParser(description="参数类型示例")
# 位置参数(positional argument):不加 -- 前缀,按顺序填写,必须提供
parser.add_argument("input_file", help="输入文件路径")
# 可选参数(optional argument):加 -- 前缀,可以不提供
parser.add_argument("--output", "-o", help="输出文件路径") # -o 是短选项
parser.add_argument("--threshold", "-t",
type=float, # 参数类型,默认是字符串
default=0.01, # 默认值
help="丰度过滤阈值(默认:0.01)")
parser.add_argument("--verbose", "-v",
action="store_true", # 只要出现就是 True,不需要跟值
help="显示详细信息")
args = parser.parse_args()
add_argument 常用参数一览¶
| 参数 | 作用 | 示例 |
|---|---|---|
type | 指定参数类型 | type=int、type=float |
default | 默认值 | default=0.01 |
required | 是否必填(仅可选参数) | required=True |
help | 帮助说明 | help="输入文件" |
choices | 限定可选值 | choices=["S", "G", "F"] |
nargs | 参数个数 | nargs="+" 表示一个或多个 |
action | 参数行为 | action="store_true" |
metavar | 帮助中显示的占位名 | metavar="FILE" |
互斥参数组¶
有时候两个参数不能同时使用,比如 --json 和 --csv 只能选一个:
import argparse
parser = argparse.ArgumentParser(description="互斥参数示例")
# 创建互斥组:组内参数只能选一个
group = parser.add_mutually_exclusive_group()
group.add_argument("--json", action="store_true", help="输出 JSON 格式")
group.add_argument("--csv", action="store_true", help="输出 CSV 格式")
args = parser.parse_args()
# 如果同时写 --json --csv,argparse 会自动报错
实战案例 1:丰度表过滤工具¶
这是一个完整的、可以直接用的 CLI 工具,功能是过滤宏基因组丰度表中低丰度的物种。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
丰度表过滤工具 - 过滤低丰度物种
用法:python filter_abundance.py --input abundance.tsv --threshold 0.01 --output filtered.tsv
"""
import argparse # 命令行参数解析
import sys # 系统相关(用于退出码)
import csv # CSV/TSV 文件读写
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(
description="过滤宏基因组丰度表中的低丰度物种",
epilog="示例:python filter_abundance.py -i abundance.tsv -t 0.01 -o filtered.tsv"
)
# 必填参数
parser.add_argument(
"--input", "-i",
required=True, # 必须提供
metavar="FILE", # --help 中显示 FILE 而不是 INPUT
help="输入丰度表文件(TSV 格式,第一列物种名,第二列丰度)"
)
# 可选参数,有默认值
parser.add_argument(
"--threshold", "-t",
type=float, # 自动转换为浮点数
default=0.01, # 默认阈值 1%
help="丰度过滤阈值,低于此值的物种将被过滤(默认:0.01)"
)
parser.add_argument(
"--output", "-o",
default=None, # 默认不指定,输出到终端
metavar="FILE",
help="输出文件路径(不指定则输出到终端)"
)
parser.add_argument(
"--keep-header",
action="store_true", # 出现就是 True
help="保留表头行"
)
return parser.parse_args() # 返回解析后的参数对象
def filter_abundance(input_path, threshold, keep_header):
"""
读取丰度表并过滤低丰度物种
参数:
input_path: 输入文件路径
threshold: 丰度阈值
keep_header: 是否保留表头
返回:
filtered_rows: 过滤后的行列表
total_count: 总物种数
kept_count: 保留的物种数
"""
filtered_rows = [] # 存放过滤后的数据
total_count = 0 # 总物种计数
kept_count = 0 # 保留的物种计数
header = None # 表头
# 打开文件,指定编码防止中文乱码
with open(input_path, "r", encoding="utf-8") as f:
reader = csv.reader(f, delimiter="\t") # 用 tab 分隔读取
for i, row in enumerate(reader): # 逐行遍历
if i == 0 and keep_header: # 第一行且需要保留表头
header = row
continue
if len(row) < 2: # 跳过格式不对的行
continue
species = row[0] # 第一列:物种名
try:
abundance = float(row[1]) # 第二列:丰度值(转浮点数)
except ValueError:
continue # 转换失败就跳过这行
total_count += 1 # 总数 +1
if abundance >= threshold: # 丰度大于等于阈值才保留
filtered_rows.append(row)
kept_count += 1 # 保留数 +1
return header, filtered_rows, total_count, kept_count
def main():
"""主函数:解析参数 → 过滤数据 → 输出结果"""
args = parse_args() # 获取命令行参数
# 执行过滤
header, filtered_rows, total, kept = filter_abundance(
args.input,
args.threshold,
args.keep_header
)
# 统计信息输出到 stderr(不影响重定向)
print(f"总物种数:{total}", file=sys.stderr)
print(f"保留物种数:{kept}(阈值 >= {args.threshold})", file=sys.stderr)
print(f"过滤掉:{total - kept} 个低丰度物种", file=sys.stderr)
# 决定输出目标:文件 or 终端
if args.output:
out_file = open(args.output, "w", encoding="utf-8", newline="")
else:
out_file = sys.stdout # 不指定文件就输出到终端
writer = csv.writer(out_file, delimiter="\t") # TSV 格式写入
if header: # 如果有表头,先写表头
writer.writerow(header)
for row in filtered_rows: # 逐行写入过滤后的数据
writer.writerow(row)
if args.output: # 如果写了文件,关闭文件
out_file.close()
print(f"结果已保存到:{args.output}", file=sys.stderr)
# 入口:只有直接运行此脚本时才执行 main()
if __name__ == "__main__":
main()
使用方式:
# 基本用法
python filter_abundance.py -i abundance.tsv -t 0.01 -o filtered.tsv
# 保留表头
python filter_abundance.py -i abundance.tsv -t 0.005 -o filtered.tsv --keep-header
# 输出到终端(方便用管道继续处理)
python filter_abundance.py -i abundance.tsv -t 0.01 | head -5
实战案例 2:Kraken2 报告解析工具¶
Kraken2 是宏基因组分析中最常用的物种分类工具。它的报告格式比较特殊,需要专门解析。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Kraken2 报告解析工具
用法:python parse_kraken2.py --report kraken2.report --level S --top 20
"""
import argparse # 命令行参数解析
import csv # CSV 读写
import json # JSON 输出
import sys # 系统相关
# Kraken2 报告中的分类级别代码对照表
LEVEL_MAP = {
"D": "Domain", # 域(如 Bacteria)
"P": "Phylum", # 门
"C": "Class", # 纲
"O": "Order", # 目
"F": "Family", # 科
"G": "Genus", # 属
"S": "Species", # 种
}
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(
description="解析 Kraken2 报告,提取指定分类级别的物种信息",
formatter_class=argparse.RawDescriptionHelpFormatter, # 保留 epilog 的换行格式
epilog="""
分类级别代码:
D - Domain(域) P - Phylum(门) C - Class(纲)
O - Order(目) F - Family(科) G - Genus(属)
S - Species(种)
示例:
python parse_kraken2.py --report sample.report --level S --top 20
python parse_kraken2.py -r sample.report -l G -n 10 --format csv -o result.csv
"""
)
parser.add_argument(
"--report", "-r",
required=True,
metavar="FILE",
help="Kraken2 报告文件路径"
)
parser.add_argument(
"--level", "-l",
default="S",
choices=list(LEVEL_MAP.keys()), # 只能选这些值
help="分类级别代码(默认:S,即 Species 种)"
)
parser.add_argument(
"--top", "-n",
type=int,
default=20,
help="显示丰度最高的前 N 个物种(默认:20)"
)
parser.add_argument(
"--format", "-f",
default="table",
choices=["table", "csv", "json"], # 限定三种格式
help="输出格式(默认:table)"
)
parser.add_argument(
"--output", "-o",
default=None,
metavar="FILE",
help="输出文件路径(不指定则输出到终端)"
)
parser.add_argument(
"--min-reads",
type=int,
default=0,
help="最低 reads 数过滤(默认:0,不过滤)"
)
return parser.parse_args()
def parse_kraken2_report(report_path, level, min_reads):
"""
解析 Kraken2 报告文件
Kraken2 报告格式(6 列,tab 分隔):
1. 该分类占总 reads 的百分比
2. 该分类及子分类的 reads 数
3. 仅该分类的 reads 数
4. 分类级别代码(如 S, G, F)
5. NCBI TaxID
6. 物种名(前面有缩进空格表示层级)
"""
results = [] # 存放解析结果
with open(report_path, "r", encoding="utf-8") as f:
for line in f:
line = line.rstrip("\n") # 去掉行尾换行符
parts = line.split("\t") # 按 tab 分割
if len(parts) < 6: # 不够 6 列就跳过
continue
percentage = float(parts[0].strip()) # 第 1 列:百分比
reads_clade = int(parts[1].strip()) # 第 2 列:该分类及子分类 reads
reads_taxon = int(parts[2].strip()) # 第 3 列:仅该分类 reads
rank_code = parts[3].strip() # 第 4 列:分类级别代码
taxid = parts[4].strip() # 第 5 列:TaxID
name = parts[5].strip() # 第 6 列:物种名(去掉前后空格)
# 只保留指定级别的结果
if rank_code != level:
continue
# 过滤低 reads 数的结果
if reads_clade < min_reads:
continue
results.append({
"name": name,
"percentage": percentage,
"reads_clade": reads_clade,
"reads_taxon": reads_taxon,
"taxid": taxid,
})
# 按百分比从大到小排序
results.sort(key=lambda x: x["percentage"], reverse=True)
return results
def output_table(results, out_file):
"""以表格格式输出结果"""
# 表头
header = f"{'排名':<5}{'物种名':<40}{'丰度%':<10}{'Reads数':<12}{'TaxID':<12}"
print(header, file=out_file)
print("-" * len(header), file=out_file) # 分隔线
for i, item in enumerate(results, 1): # 从 1 开始编号
line = f"{i:<5}{item['name']:<40}{item['percentage']:<10.4f}{item['reads_clade']:<12}{item['taxid']:<12}"
print(line, file=out_file)
def output_csv(results, out_file):
"""以 CSV 格式输出结果"""
writer = csv.writer(out_file)
writer.writerow(["rank", "name", "percentage", "reads_clade", "reads_taxon", "taxid"])
for i, item in enumerate(results, 1):
writer.writerow([
i,
item["name"],
item["percentage"],
item["reads_clade"],
item["reads_taxon"],
item["taxid"],
])
def output_json(results, out_file):
"""以 JSON 格式输出结果"""
# ensure_ascii=False 让中文正常显示,indent=2 让格式好看
json.dump(results, out_file, ensure_ascii=False, indent=2)
print(file=out_file) # 末尾加换行
def main():
"""主函数"""
args = parse_args()
# 解析报告
results = parse_kraken2_report(args.report, args.level, args.min_reads)
# 截取 top N
results = results[:args.top]
# 输出统计信息到 stderr
level_name = LEVEL_MAP.get(args.level, args.level)
print(f"分类级别:{args.level}({level_name})", file=sys.stderr)
print(f"显示前 {args.top} 个结果", file=sys.stderr)
# 决定输出目标
if args.output:
out_file = open(args.output, "w", encoding="utf-8", newline="")
else:
out_file = sys.stdout
# 根据格式选择输出函数
if args.format == "table":
output_table(results, out_file)
elif args.format == "csv":
output_csv(results, out_file)
elif args.format == "json":
output_json(results, out_file)
if args.output:
out_file.close()
print(f"结果已保存到:{args.output}", file=sys.stderr)
if __name__ == "__main__":
main()
使用方式:
# 查看种级别 top 20(默认)
python parse_kraken2.py --report sample.report
# 查看属级别 top 10,CSV 格式输出
python parse_kraken2.py -r sample.report -l G -n 10 -f csv -o genus_top10.csv
# JSON 格式 + 过滤低 reads
python parse_kraken2.py -r sample.report -l S -n 50 -f json --min-reads 100
# 查看帮助
python parse_kraken2.py --help
进阶:Click 框架简介¶
Click(v8.3.3,2026 年 4 月发布)是一个更现代的 CLI 框架,用装饰器语法替代了 argparse 的"先创建解析器再添加参数"的方式,代码更简洁。
Click vs argparse 对比¶
| 特性 | argparse | Click |
|---|---|---|
| 是否标准库 | 是(不需要安装) | 否(需要 pip install) |
| 语法风格 | 面向对象(创建 parser,添加参数) | 装饰器(@click.command) |
| 学习曲线 | 中等 | 较低 |
| 子命令支持 | 需要 subparsers | @click.group 更直观 |
| 类型转换 | 有,但不够灵活 | 内置丰富类型(Path、File 等) |
丰度过滤工具的 Click 版本¶
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""丰度过滤工具 - Click 版本"""
import click # 需要先 pip install click
import csv # CSV 读写
import sys # 系统相关
@click.command() # 把函数变成 CLI 命令
@click.option("--input", "-i", "input_file", # input_file 是变量名(避免和内置 input 冲突)
required=True,
type=click.Path(exists=True), # 自动检查文件是否存在!
help="输入丰度表文件(TSV 格式)")
@click.option("--threshold", "-t",
type=float,
default=0.01,
show_default=True, # 在 --help 中自动显示默认值
help="丰度过滤阈值")
@click.option("--output", "-o",
type=click.Path(), # 输出路径(不检查是否存在)
default=None,
help="输出文件路径")
def filter_abundance(input_file, threshold, output):
"""过滤宏基因组丰度表中的低丰度物种。""" # 这个 docstring 会自动变成 --help 的描述
kept = 0 # 保留计数
total = 0 # 总数计数
rows = [] # 存放结果
with open(input_file, "r", encoding="utf-8") as f:
reader = csv.reader(f, delimiter="\t")
for row in reader:
if len(row) < 2:
continue
try:
abundance = float(row[1])
except ValueError:
continue
total += 1
if abundance >= threshold:
rows.append(row)
kept += 1
# click.echo 是 Click 推荐的 print 替代,兼容性更好
click.echo(f"保留 {kept}/{total} 个物种", err=True)
if output:
with open(output, "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f, delimiter="\t")
for row in rows:
writer.writerow(row)
click.echo(f"结果已保存到:{output}", err=True)
else:
for row in rows:
click.echo("\t".join(row))
if __name__ == "__main__":
filter_abundance() # Click 会自动处理参数解析
安装 Click:
让 CLI 工具可以直接运行¶
1. if __name__ == "__main__" 模式¶
这是 Python 的标准入口模式。白话解释:这行代码的意思是"如果这个文件是被直接运行的(而不是被其他文件 import 的),就执行 main() 函数"。
def main():
"""工具的主逻辑"""
# ... 你的代码 ...
# 只有直接运行时才执行,被 import 时不执行
if __name__ == "__main__":
main()
2. 添加 shebang 行 + 可执行权限¶
shebang(读作"sha-bang")是脚本第一行的 #! 标记,告诉系统用什么程序来运行这个脚本。
# 第一步:确保脚本第一行是 shebang
# #!/usr/bin/env python3 ← 这行就是 shebang
# 第二步:给脚本加上可执行权限
chmod +x filter_abundance.py # 让脚本可以直接运行
# 第三步:直接运行(不需要在前面写 python)
./filter_abundance.py --input data.tsv --threshold 0.01
3. 添加到 PATH(全局可用)¶
# 方法 1:创建个人 bin 目录
mkdir -p ~/bin # 创建 bin 目录(如果不存在)
cp filter_abundance.py ~/bin/filter_abundance # 复制脚本(去掉 .py 后缀更专业)
chmod +x ~/bin/filter_abundance # 加可执行权限
# 把 ~/bin 加入 PATH(写入 .bashrc 永久生效)
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc # 立即生效
# 现在可以在任何目录直接使用
filter_abundance --input data.tsv --threshold 0.01
# 方法 2:用 pip install -e . 安装(见知识库 569 包发布篇)
日志系统:logging 模块替代 print¶
很多初学者习惯用 print() 来调试和显示信息。但在正式的 CLI 工具中,应该用 logging 模块。
为什么?¶
| 特性 | logging | |
|---|---|---|
| 能控制显示级别 | 不能 | 能(DEBUG/INFO/WARNING/ERROR) |
| 能输出到文件 | 麻烦 | 简单 |
| 能显示时间戳 | 不能 | 能 |
| 生产环境能关掉 | 不能 | 能 |
完整示例:带 --verbose 的 CLI 工具¶
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""带日志功能的 CLI 工具示例"""
import argparse # 命令行参数解析
import logging # 日志模块
import sys # 系统相关
def setup_logging(verbose):
"""
设置日志系统
参数:
verbose: 是否开启详细模式(True 显示 DEBUG 级别信息)
"""
# 根据 verbose 参数决定日志级别
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(
level=level, # 日志级别
format="%(asctime)s [%(levelname)s] %(message)s", # 日志格式
datefmt="%Y-%m-%d %H:%M:%S", # 时间格式
stream=sys.stderr, # 输出到 stderr,不影响数据重定向
)
def main():
parser = argparse.ArgumentParser(description="带日志的生信工具示例")
parser.add_argument("--input", "-i", required=True, help="输入文件")
parser.add_argument("--verbose", "-v", action="store_true",
help="显示详细调试信息")
args = parser.parse_args()
# 初始化日志系统
setup_logging(args.verbose)
# 获取 logger 对象
logger = logging.getLogger(__name__)
logger.info("开始处理文件:%s", args.input) # INFO 级别:总是显示
logger.debug("打开文件中...") # DEBUG 级别:只有 -v 时显示
logger.debug("读取完成,共 1000 行")
logger.warning("发现 5 行格式异常,已跳过") # WARNING 级别:总是显示
logger.info("处理完成")
if __name__ == "__main__":
main()
运行效果:
# 普通模式:只显示 INFO 和 WARNING
$ python tool.py -i data.tsv
2026-05-09 10:30:00 [INFO] 开始处理文件:data.tsv
2026-05-09 10:30:01 [WARNING] 发现 5 行格式异常,已跳过
2026-05-09 10:30:01 [INFO] 处理完成
# 详细模式:额外显示 DEBUG
$ python tool.py -i data.tsv -v
2026-05-09 10:30:00 [INFO] 开始处理文件:data.tsv
2026-05-09 10:30:00 [DEBUG] 打开文件中...
2026-05-09 10:30:01 [DEBUG] 读取完成,共 1000 行
2026-05-09 10:30:01 [WARNING] 发现 5 行格式异常,已跳过
2026-05-09 10:30:01 [INFO] 处理完成
常见报错¶
1. error: unrecognized arguments¶
原因:参数名写错了,或者忘记用 add_argument 注册这个参数。
解决:检查 add_argument 中定义的参数名,注意 -- 前缀和拼写。
2. error: the following arguments are required¶
原因:必填参数没有提供。
解决:加上缺少的参数。用 --help 查看哪些参数是必填的。
3. error: argument --threshold: invalid float value¶
原因:参数定义了 type=float,但你传了一个不能转换为浮点数的值。
解决:传入正确类型的值,比如 --threshold 0.01。
4. FileNotFoundError: [Errno 2] No such file or directory¶
原因:argparse 只负责解析参数,不检查文件是否存在(除非用 Click 的 Path(exists=True))。
解决:在主逻辑中加文件存在性检查:
import os
if not os.path.exists(args.input):
print(f"错误:文件 {args.input} 不存在", file=sys.stderr)
sys.exit(1) # 以错误码退出
5. TypeError: expected str, bytes or os.PathLike object, not NoneType¶
原因:参数没有设 required=True,也没有设 default,结果值为 None。
解决:给可选参数设置 default 值,或在使用前检查是否为 None。
速查表¶
argparse 常用模板¶
#!/usr/bin/env python3
"""工具描述"""
import argparse
def main():
parser = argparse.ArgumentParser(description="工具描述")
parser.add_argument("--input", "-i", required=True, help="输入文件")
parser.add_argument("--output", "-o", default="out.tsv", help="输出文件")
parser.add_argument("--threshold", "-t", type=float, default=0.01, help="阈值")
parser.add_argument("--verbose", "-v", action="store_true", help="详细模式")
args = parser.parse_args()
# 你的逻辑写在这里
if __name__ == "__main__":
main()
add_argument 速查¶
| 写法 | 含义 |
|---|---|
"filename" | 位置参数(必填) |
"--output" | 可选参数 |
"--output", "-o" | 可选参数 + 短选项 |
type=int | 自动转为整数 |
type=float | 自动转为浮点数 |
default="out.tsv" | 默认值 |
required=True | 可选参数变必填 |
choices=["S","G","F"] | 限定选项 |
action="store_true" | 布尔开关 |
nargs="+" | 接受一个或多个值 |
nargs="*" | 接受零个或多个值 |
metavar="FILE" | 帮助中显示的占位名 |
help="说明文字" | 参数帮助说明 |
输出格式速查¶
| 场景 | 推荐做法 |
|---|---|
| 数据输出(可能被管道) | 写到 sys.stdout |
| 日志/进度信息 | 写到 sys.stderr 或用 logging |
| 错误信息 | 写到 sys.stderr,用 sys.exit(1) 退出 |
| 帮助信息 | argparse 自动处理 |
CLI 工具开发检查清单¶
- [ ] 有清晰的
--help描述 - [ ] 必填参数设了
required=True - [ ] 可选参数有合理的
default - [ ] 数值参数设了
type - [ ] 有限选项用了
choices - [ ] 数据输出到 stdout,日志输出到 stderr
- [ ] 有
if __name__ == "__main__"入口 - [ ] 错误时用
sys.exit(1)而不是exit() - [ ] 脚本第一行有 shebang(
#!/usr/bin/env python3)