Skip to content

Commit ba459b9

Browse files
committed
新增“Python 命令行之旅:使用 click 实现 git 命令”
1 parent 4d87560 commit ba459b9

File tree

3 files changed

+321
-0
lines changed

3 files changed

+321
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- 3.3 [深入 click(二)](contents/Python/cmdline/click-3.md)
2929
- 3.4 [深入 click(三)](contents/Python/cmdline/click-4.md)
3030
- 3.5 [深入 click(四)](contents/Python/cmdline/click-5.md)
31+
- 3.6 [使用 click 实现 git 命令](contents/Python/cmdline/click-6.md)
3132
2. [用 Python 生成有“灵魂”的二维码:QRcode](contents/Python/QRcode/content.md)
3233
3. 聊聊 Python 的单元测试框架
3334
- 3.1 [unittest](contents/Python/unittest/unittest.md)

contents/Python/cmdline/click-6.md

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
# Python 命令行之旅:使用 click 实现 git 命令
2+
3+
## 一、前言
4+
5+
在前面五篇介绍 `click` 的文章中,我们全面了解了 `click` 的强大能力。按照惯例,我们要像使用 `argparse``docopt` 一样使用 `click` 来实现 git 命令。
6+
7+
本文的关注点并不在 `git` 的各种命令是如何实现的,而是怎么使用 `click` 去打造一个实用命令行程序,代码结构是怎样的。因此,和 `git` 相关的操作,将会使用 `gitpython` 库来简单实现。
8+
9+
为了让没读过 `使用 xxx 实现 git 命令``xxx``argparse``docopt`) 的小伙伴也能读明白本文,我们仍会对 `git` 常用命令和 `gitpython` 做一个简单介绍。
10+
11+
```plain
12+
本系列文章默认使用 Python 3 作为解释器进行讲解。
13+
若你仍在使用 Python 2,请注意两者之间语法和库的使用差异哦~
14+
```
15+
16+
## 二、git 常用命令
17+
18+
当你写好一段代码或增删一些文件后,会用如下命令查看文件状态:
19+
20+
```bash
21+
git status
22+
```
23+
24+
确认文件状态后,会用如下命令将的一个或多个文件(夹)添加到暂存区:
25+
26+
```bash
27+
git add [pathspec [pathspec ...]]
28+
```
29+
30+
然后使用如下命令提交信息:
31+
32+
```bash
33+
git commit -m "your commit message"
34+
```
35+
36+
最后使用如下命令将提交推送到远程仓库:
37+
38+
```bash
39+
git push
40+
```
41+
42+
我们将使用 `click``gitpython` 库来实现这 4 个子命令。
43+
44+
## 三、关于 gitpython
45+
46+
[gitpython](https://gitpython.readthedocs.io/en/stable/intro.html) 是一个和 `git` 仓库交互的 Python 第三方库。
47+
我们将借用它的能力来实现真正的 `git` 逻辑。
48+
49+
安装:
50+
51+
```bash
52+
pip install gitpython
53+
```
54+
55+
## 四、思考
56+
57+
在实现前,我们不妨先思考下会用到 `click` 的哪些功能?整个程序的结构是怎样的?
58+
59+
**click**
60+
61+
`git` 的 4 个子命令的实现其实对应于四个函数,每个函数使用 `click``command` 来装饰。
62+
而对于 `git add``git commit`,则分别需要表示参数的 `click.argument` 和表示选项的 `click.option` 来装饰。
63+
64+
**程序结构**
65+
66+
程序结构上:
67+
68+
- 实例化 `Git` 对象,供全局使用
69+
- 定义 `cli` 函数作为命令组,也就是整个命令程序的入口
70+
- 定义四个命令对应的实现函数 `status``add``commit``push`
71+
72+
则基本结构如下:
73+
74+
```python
75+
import os
76+
import click
77+
from git.cmd import Git
78+
79+
git = Git(os.getcwd())
80+
81+
82+
@click.group()
83+
def cli():
84+
"""
85+
git 命令行
86+
"""
87+
pass
88+
89+
90+
@cli.command()
91+
def status():
92+
"""
93+
处理 status 命令
94+
"""
95+
pass
96+
97+
98+
@cli.command()
99+
@click.argument('pathspec', nargs=-1)
100+
def add(pathspec):
101+
"""
102+
处理 add 命令
103+
"""
104+
pass
105+
106+
107+
@cli.command()
108+
@click.option('-m', 'msg')
109+
def commit(msg):
110+
"""
111+
处理 -m <msg> 命令
112+
"""
113+
pass
114+
115+
116+
@cli.command()
117+
def push():
118+
"""
119+
处理 push 命令
120+
"""
121+
pass
122+
123+
124+
if __name__ == '__main__':
125+
cli()
126+
```
127+
128+
下面我们将一步步地实现我们的 `git` 程序。
129+
130+
## 五、实现
131+
132+
假定我们在 [click-git.py](https://github.com/HelloGitHub-Team/Article/blob/master/contents/Python/cmdline/click-git.py) 文件中实现我们的 `git` 程序。
133+
134+
### 5.1 status 子命令
135+
136+
`status` 子命令不接受任何参数和选项,因此其实现函数只需 `cli.command()` 装饰。
137+
138+
```python
139+
@cli.command()
140+
def status():
141+
"""
142+
处理 status 命令
143+
"""
144+
cmd = ['git', 'status']
145+
output = git.execute(cmd)
146+
click.echo(output)
147+
```
148+
149+
不难看出,我们最后调用了真正的 `git status` 来实现,并打印了输出。
150+
151+
### 5.2 add 子命令
152+
153+
`add` 子命令相对于 `status` 子命令,需要接受任意个 pathspec 参数,因此增加一个 `click.argument` 装饰器,并且在 `add` 函数中需要增加同名的 `pathspec` 入参。
154+
`click` 处理后的 `pathspec` 其实是个元组,和列表相加前,需要先转换为列表。
155+
156+
```python
157+
@cli.command()
158+
@click.argument('pathspec', nargs=-1)
159+
def add(pathspec):
160+
"""
161+
处理 add 命令
162+
"""
163+
cmd = ['git', 'add'] + list(pathspec)
164+
output = git.execute(cmd)
165+
click.echo(output)
166+
```
167+
168+
当我们执行 `python3 click-git.py add --help` 时,结果如下:
169+
170+
```plain
171+
Usage: click-git.py add [OPTIONS] [PATHSPEC]...
172+
173+
处理 add 命令
174+
175+
Options:
176+
--help Show this message and exit.
177+
```
178+
179+
既然 `git add` 能接受任意多个 `pathspec`,那么 `add(pathspec)` 的参数其实改为复数形式更为合适,但我们又希望帮助信息中是单数形式,这就需要额外指定 `metavar`,则有:
180+
181+
```python
182+
@cli.command()
183+
@click.argument('pathspecs', nargs=-1, metavar='[PATHSPEC]...')
184+
def add(pathspecs):
185+
"""
186+
处理 add 命令
187+
"""
188+
cmd = ['git', 'add'] + list(pathspecs)
189+
output = git.execute(cmd)
190+
click.echo(output)
191+
```
192+
193+
### 5.3 commit 子命令
194+
195+
`add` 子命令相对于 `status` 子命令,需要接受 `-m` 选项,因此增加一个 `click.option` 装饰器,指定选项名称 `msg`,并且在 `commit` 函数中增加同名入参。
196+
197+
```python
198+
@cli.command()
199+
@click.option('-m', 'msg')
200+
def commit(msg):
201+
"""
202+
处理 -m <msg> 命令
203+
"""
204+
cmd = ['git', 'commit', '-m', msg]
205+
output = git.execute(cmd)
206+
click.echo(output)
207+
```
208+
209+
### 5.4 push 子命令
210+
211+
`push` 子命令同 `status` 子命令一样,不接受任何参数和选项,因此其实现函数只需 `cli.command()` 装饰。
212+
213+
```python
214+
@cli.command()
215+
def push():
216+
"""
217+
处理 push 命令
218+
"""
219+
cmd = ['git', 'push']
220+
output = git.execute(cmd)
221+
click.echo(output)
222+
```
223+
224+
至此,我们就实现了一个简单的 `git` 命令行,使用 `python click-git.py status` 便可查询项目状态。
225+
226+
非常方便的是,每个命令函数的 `docstring` 都将作为这个命令的帮助信息,因此,当我们执行 `python3 click-git.py --help` 会自动生成如下帮助内容:
227+
228+
```plain
229+
Usage: click-git.py [OPTIONS] COMMAND [ARGS]...
230+
231+
git 命令行
232+
233+
Options:
234+
--help Show this message and exit.
235+
236+
Commands:
237+
add 处理 add 命令
238+
commit 处理 -m <msg> 命令
239+
push 处理 push 命令
240+
status 处理 status 命令
241+
```
242+
243+
想看整个源码,请戳 [click-git.py](https://github.com/HelloGitHub-Team/Article/blob/master/contents/Python/cmdline/click-git.py) 。
244+
245+
## 六、小结
246+
247+
本文简单介绍了日常工作中常用的 `git` 命令,然后提出实现它的思路,最终一步步地使用 `click``gitpython` 实现了 `git` 程序。
248+
249+
对比 `argparse``click` 的实现版本,你会发现使用 `click` 来实现变得特定简单:
250+
251+
- 相较于 `argparse`,子解析器、参数类型什么的统统不需要关心
252+
- 相较于 `docopt`,参数解析和命令调用处理也不需要关心
253+
254+
这无疑是 `click` 最大的优势了。
255+
256+
关于 `click` 的讲解将告一段落,回顾下 `click` 的至简之道,你会爱上它。
257+
258+
现在,你已学会了三个命令行解析库的使用了。但你以为这就够了吗?`click` 已经够简单了吧,够直接了吧?但它仍然不是最简单的。
259+
260+
在下篇文章中,将为大家介绍一个由谷歌出品的在 Python 界很火的命令行库 —— `fire`

contents/Python/cmdline/click-git.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
import click
3+
from git.cmd import Git
4+
5+
6+
git = Git(os.getcwd())
7+
8+
9+
@click.group()
10+
def cli():
11+
"""
12+
git 命令行
13+
"""
14+
pass
15+
16+
17+
@cli.command()
18+
def status():
19+
"""
20+
处理 status 命令
21+
"""
22+
cmd = ['git', 'status']
23+
output = git.execute(cmd)
24+
click.echo(output)
25+
26+
27+
@cli.command()
28+
@click.argument('pathspecs', nargs=-1, metavar='[PATHSPEC]...')
29+
def add(pathspecs):
30+
"""
31+
处理 add 命令
32+
"""
33+
cmd = ['git', 'add'] + list(pathspecs)
34+
output = git.execute(cmd)
35+
click.echo(output)
36+
37+
38+
@cli.command()
39+
@click.option('-m', 'msg')
40+
def commit(msg):
41+
"""
42+
处理 -m <msg> 命令
43+
"""
44+
cmd = ['git', 'commit', '-m', msg]
45+
output = git.execute(cmd)
46+
click.echo(output)
47+
48+
49+
@cli.command()
50+
def push():
51+
"""
52+
处理 push 命令
53+
"""
54+
cmd = ['git', 'push']
55+
output = git.execute(cmd)
56+
click.echo(output)
57+
58+
59+
if __name__ == '__main__':
60+
cli()

0 commit comments

Comments
 (0)