|
| 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`。 |
0 commit comments