用run.sh脚本代替来使用仅有.PHONY目标的Makefile

282 阅读2分钟

当你有一个只有.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"。

可重用性 #

让我们看一个例子,我以三种方式运行覆盖率。

  • 用于开发,使用HTML报告和上下文("谁测试了什么")。
  • 用于跨Python版本/解释器的测试,使用Tox;上下文可能很有用,但会增加运行时间
  • 用于持续集成1;不需要上下文。

如果特定模块的覆盖率低于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;除了上面的内容外,它还有。

  • 更复杂的例子
  • 一个解决方法,使$0-dispatch在被调用时能够工作。bash run.sh
  • 基于Julia Evans的模式,将entrgit ls-files 一起使用的封装器。