当你有一个只有.PHONY 目标的Makefile时,你可以把它变成一个带有函数的shell脚本,并通过在结尾处添加"$@" ,对它们进行调度。
这使得事情更容易读和写,允许向 "目标 "传递参数,并能在脚本内部和外部重复使用。
为什么首先要有一个Makefile? #
我一直在我的Pythonfeed reader库中使用Makefile,以便为常见的开发工作提供方便的快捷方式:安装依赖,运行测试,等等。
(我最初是从Flask那里学到的模式,尽管他们在1.0之后就不再使用了。)
下面是一个简略的版本,让你感受一下
.PHONY: test typing
test:
pytest --runslow
# mypy does not work on pypy as of January 2020
typing:
test $$( python -c 'import sys; print(sys.implementation.name)' ) = pypy \
&& echo "mypy does not work on pypy, doing nothing" \
|| mypy --strict src
对我来说,这有两个主要的坏处。
- 没有办法向目标传递参数,例如在调用
pytest -v的同时,也得到 "默认 "的--runslow选项。(在这种情况下,我可以使用addoptsconfig键--但我不想强迫每个人都使用--runslow,我只是想表明这是推荐的方式)。 - 它使编写全功能的脚本变得更加困难;这是有可能的,但结果往往是可读性差。
输入run.sh #
我们可以把它重新写成一个shell脚本;让我们把它叫做run.sh 。
#!/bin/bash
function test {
pytest --runslow "$@"
}
function typing {
local impl=$( python -c 'import sys; print(sys.implementation.name)' )
# mypy does not work on pypy as of January 2020
if [[ $impl == pypy ]]; then
echo "mypy does not work on pypy, doing nothing"
return
fi
mypy --strict src "$@"
}
"$@"
最后的$@ 将脚本参数分配给一个函数(所以./run.sh test 调用test);test 中的$@ 将剩余的参数传递出去(所以./run.sh test -v 最终会运行pytest --runslow -v).
为什么我认为它很酷 #
可执行的文档 #
脚本是记录特定项目开发工具的一种简单方式--只要稍加注意,它就会成为*可执行的文档;*这是一个巨大的好处,在原文中也有强调。
我强烈考虑在我的run.sh ,并将其直接纳入开发者文档,而不是书面文档中。
大多数命令是不言而喻的,如果你想以不同的方式运行某些东西,你可以直接把它复制粘贴到终端(对于Makefile来说并不直接)。 见鬼,如果你使用兼容的shell,你甚至可以把它源化,并拥有一种 "项目shell"。
可重用性 #
让我们看一个例子,我以三种方式运行覆盖率。
如果特定模块的覆盖率低于100%,所有案例都应该失败。
run.sh使得在tos/CI下运行时可以跳过上下文,这使CI的运行时间减少了10-30%。 同时,它避免了重复一些相当多的命令。
现在,面向开发者的coverage-all 命令看起来像这样:
function coverage-all {
coverage-run --cov-context=test "$@"
coverage-report --show-contexts
}
... tox.ini命令看起来像这样:
[testenv]
commands = ./run.sh coverage-run --cov-append
[testenv:coverage-report]
commands = ./run.sh coverage-report
[testenv:typing]
commands = ./run.sh typing
...而CI调用的函数看起来像这样:
function ci-run {
coverage-run && coverage-report && typing
}
这种可重用性延伸到在任何需要命令的地方使用这些函数。
timeout 5 ./run.sh myfunction
......包括在脚本本身中使用(原文中称之为$0-dispatch)。
function typing-dev {
find src -name '*.py' | entr -cdr "$0" typing "$@"
}
在这里,entr 接收一个命令 (和它的参数),并在src 中的 Python 文件发生变化时运行它。注意,我们用$0 来调度脚本的typing "目标"。
你可以在这里找到 reader 的完整 run.sh;除了上面的内容外,它还有。