宇宙纪元

hinder110 的思考、读书与代码札记。

0%

读懂一个186行Python CLI工具

读懂一个 186 行 Python CLI 工具:从敲命令到理解每一行

开篇

我最近写了一个 workspace manager,一个命令就能打开某个任务需要的全部软件。比如做 CAD 设计,python workspace.py design 一键同时启动 AutoCAD 和 Photoshop,不用一个个手动开。

186 行代码,纯 Python 标准库,跑通了。但我不想停在”能用就行”,而是想彻底搞懂每一行为什么这么写。这篇文章记录我学到的 6 个知识点。

这脚本是干嘛的

做设计的日常:开机 → 打开 AutoCAD → 打开 Photoshop → 开始干活。每天重复。能不能一句话搞定?

1
python workspace.py design

AutoCAD 和 Photoshop 同时启动,像 IDE 保存项目标签页一样,只不过是在操作系统层级。

1
2
3
4
5
6
7
8
9
# 创建工作空间
python workspace.py create dev "开发环境"

# 往里面加软件
python workspace.py add dev vscode code
python workspace.py add dev chrome "C:\Program Files\Google\Chrome\Application\chrome.exe"

# 一键全开
python workspace.py dev

配置存在 ~/.workspaces.json,长这样:

1
2
3
4
5
6
7
8
9
{
"design": {
"name": "CAD设计",
"apps": [
{"name": "autocad", "path": "C:\\Program Files\\Autodesk\\AutoCAD 2021\\acad.exe"},
{"name": "photoshop", "path": "F:\\Program Files\\...\\Photoshop.exe"}
]
}
}

整体架构:一眼看到底

整个程序的骨架就一句话:CLI 工具 = 命令分发器。用户敲命令 → 解析 → 分发给对应函数执行。

看主函数最后这 7 行:

1
2
3
4
5
6
7
8
9
{
"create": cmd_create,
"delete": cmd_delete,
"list": cmd_list,
"show": cmd_show,
"add": cmd_add,
"remove": cmd_remove,
"launch": cmd_launch,
}[args.command](args)

字典的 key 是命令名,value 是函数对象。取出来,后面加 (args) 直接调用。这就是 Python 版的 switch-case —— C 里得写一长串 if-else if,Java 得更啰嗦,Python 一个字典搞定,干净利落。

命令行参数解析:argparse 怎么把字符串变成结构体

1
2
3
python workspace.py create dev "开发环境"
# ^^^^ ^^^ ^^^^^^^^^
# 位置0 位置1 位置2

args 是 argparse 自动构造的 Namespace 对象,跟 C 的 struct 一个概念——把命令行字符串变成有名字的字段:

1
2
3
4
5
6
7
// C 里你会这样
struct Args {
char *command;
char *key;
char *name;
};
Args args = {"create", "dev", "开发环境"};

Python 里 add_argument 就是声明字段,argparse 帮你填值:

1
2
3
subparsers = parser.add_subparsers(dest="command")   # → args.command
p_create.add_argument("key") # → args.key
p_create.add_argument("name") # → args.name

参数多了你不用手写解析逻辑,库全包了。

程序的记忆:JSON 持久化

没有 JSON 文件,程序一关,你建的 workspace 全丢了。每次运行都得重新 add 一遍软件——那还不如手动开。

JSON 是这个程序的”记忆”。启动时读记忆,结束时存记忆:

1
2
3
4
5
6
7
8
9
CONFIG_PATH = Path.home() / ".workspaces.json"

def load_config(): # 程序醒来 → 读记忆
with open(CONFIG_PATH, "r") as f:
return json.load(f) # JSON文本 → Python字典

def save_config(config): # 程序睡去 → 存记忆
with open(CONFIG_PATH, "w") as f:
json.dump(config, f, indent=2, ensure_ascii=False)

所有操作都是 读文件 → 改字典 → 写文件 三步:

1
用户敲命令 → load_config() → 在内存字典里增/删/改 → save_config() → 程序退出

Path.home() 跨平台取用户目录,Windows 是 C:\Users\你的用户名,macOS 是 /Users/你的用户名。配置文件放这里不会被 git 跟踪到,也不会跟项目代码混在一起。

启动软件的两种姿势

Windows 上打开东西分两种情况:

1
2
3
4
5
6
ext = os.path.splitext(path)[1].lower()

if ext in (".docx", ".pdf", ".png", ...):
os.startfile(path) # 文档:让Windows用默认程序打开
else:
subprocess.Popen(cmd, shell=True) # 可执行文件:直接运行

.exe 程序文件 → 自己跑起来,所以用 subprocess.Popen,新开一个进程执行。

.docx / .pdf 文档文件 → 自己不运行,是让关联程序打开它。os.startfile 等价于你在资源管理器里双击文件,Windows 自动找 Word 或 PDF 阅读器代开。

subprocess.Popen(cmd, shell=True) 怎么理解?shell=True 就是交给 Windows 的 cmd 去解释命令字符串。跟 C 语言里 system("notepad.exe") 干的是一模一样的事——调操作系统帮你启动程序。区别是 Popen 可以拿到进程对象,不阻塞当前脚本,更灵活。

最巧妙的技巧:偷偷帮你补全命令

这是 186 行里我最喜欢的设计。看这段:

1
2
3
4
5
COMMANDS = {"create", "delete", "list", "show", "add", "remove", "launch"}

if len(sys.argv) > 1 and sys.argv[1] not in COMMANDS \
and not sys.argv[1].startswith("-"):
sys.argv.insert(1, "launch")

思路:如果用户敲的第一个词不是已知命令,就当他是在说 workspace 名字,自动在前面塞一个 "launch"

1
2
3
用户输入:  python workspace.py design
↓ 检测 "design" 不在命令列表里
实际变成: python workspace.py launch design

少打 launch 六个字符,体验直接起飞。这是在 argparse 解析之前偷偷改了 sys.argv,把输入”骗”成合法格式。

收获

186 行代码,吃透后学到 6 样东西:

# 知识点 关键
1 命令分发 字典映射函数 = Python 的 switch
2 argparse 命令行 → 结构体,字段即参数
3 JSON 持久化 程序的”记忆”,读-改-写三步
4 Popen + startfile 程序自己跑,文档找人代开
5 shell=True 跟 C 的 system() 一回事
6 argv 前置注入 解析前动手脚,省用户打字

最重要的领悟是:代码不是跑起来就行,读懂设计意图才算真的掌握。 你多学到的每一点,下次自己写代码时都会冒出来帮你。


源码地址:github.com/hinder110/workspace-manager