Прекратите использовать Makefile для выполнения повторяющихся задач
TL;DR
Makefile
имеет важные ограничения при использовании его для выполнения повторяющихся задач оболочки.
Лучшей альтернативой является использование сценария оболочки с функциями, которые я назвал taskfile
. Попробуйте, выполнив следующую команду в своем терминале, которая создаст базовый taskfile
в рабочем каталоге:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/acecilia/taskfile/master/start.sh)"
Как мы здесь оказались?
Когда программный проект растет, в большинстве случаев у вас появляется список повторяющихся задач, которые необходимо выполнять в верхней части репозитория. Например, несколько простых: build
, test
, _7 _...
Есть несколько альтернатив для организации кода, который реализует каждая из этих задач:
- В python можно определить каждую из повторяющихся задач как функцию внутри
py
файла - У пользователей
npm
даже есть специальный инструмент для этого - В качестве независимого от языка решения можно сохранить каждую повторяющуюся задачу в ее собственном исполняемом сценарии и иметь папку, содержащую все из них. После, используйте
direnv
, чтобы добавить их вPATH
Но давайте сосредоточимся на одном из самых популярных и распространенных решений: определить каждую из повторяющихся задач как цель внутри Makefile
Плюсы и минусы Makefile
Использование Makefile
для хранения и выполнения этих повторяющихся задач поначалу кажется хорошим решением:
- Практически все знают, как запустить
Makefile
, когда его видят. - Инструмент командной строки
make
является мультиплатформенным и во многих случаях поставляется предварительно установленным в ОС, поэтому дополнительных действий по установке не требуется. - Синтаксис выглядит ясным: каждая повторяющаяся задача определяется как цель
- Цели могут зависеть друг от друга: выполнение одной задачи может запускать любую другую задачу в определенном порядке.
build:
bazel build ...
test: build
bazel test ...
Но как только проект разрастается и повторяющиеся задачи становятся более сложными, чем однострочная команда, возникают некоторые проблемы. То, что должно быть простым, становится сложным, многословным или не интуитивно понятным:
- Выполнение многострочных команд становится многословным и трудным для чтения, потому что каждая строка должна иметь суффикс
\
:
my_target1:
for i in $$(ls); do \
echo "This is a file: $$i"; \
done
- Определение переменных сложно или невозможно, что вынуждает нас переписывать код, чтобы удовлетворить
Makefile
требования к синтаксису:
my_target2:
$(eval MY_VAR := "this is a local variable")
echo $(MY_VAR)
- Введение условного выполнения требует дополнительных знаний синтаксиса
Makefile
или написания условия в синтаксисе оболочки. Это делает код подробным и трудным для чтения. - Использование переменных оболочки требует экранирования символов
$
, что затрудняет чтение кода:
my_target3:
echo "$$TMPDIR"
- При выполнении задачи все команды, выполняемые внутри нее, по умолчанию распечатываются. Чтобы этого избежать, вам нужно поставить перед командой префикс
@
или полностью избежать печати команд, добавив.SILENT
в началоMakefile
:
my_target4:
@echo "hello world"
Makefile
требует использования табуляции вместо пробелов. Это может привести к проблемам при смешивании пробелов и табуляции, что, вероятно, произойдет- Нет простого способа передать переменные целям
Makefile
Вы можете понять, как все эти мелкие проблемы становятся основной проблемой, поскольку эти повторяющиеся задачи растут и усложняются. Проблема в том, что мы упускаем из виду Makefile
: make
- инструмент автоматизации сборки, который никогда не предназначался для использования в качестве альтернативы для повторного выполнения задач.
Альтернатива: файл задачи
В большинстве случаев эти повторяющиеся задачи представляют собой простые команды оболочки: наиболее идеальным решением было бы записать их в файл сценария оболочки. Нам также понадобится способ группировки кода на основе повторяющихся задач, которые необходимо выполнить. Решение очень простое: использовать сценарий оболочки с функциями внутри.
#!/bin/zsh
set -euo pipefail # 1
name="Andres" # 2
say_hello() { surname="$1" # 3 echo "Hello, I am $name $surname" # 4 }
# What this task does: Says hello and bye # 5 say_hello_and_bye() { say_hello $@ # 6 echo "Bye!" }
"$@" # 7
Давайте рассмотрим детали:
set -euo pipefail
включает своего рода строгий режим для запуска скрипта- Можно определять глобальные переменные
- Можно определить локальные переменные
- Можно легко повторно использовать глобальные и локальные переменные
- Есть возможность добавлять комментарии и документировать свой код
- Некоторые задачи могут вызывать другие задачи.
- Вот что заставляет все работать: выполняет все параметры, переданные в скрипт, как если бы они были функцией
Итак, приведенный выше сценарий можно вызвать следующим образом:
- Запуск от имени
./taskfile_example.sh say_hello Cecilia
напечатает:
Hello, I am Andres Cecilia
- Запуск от имени
./taskfile_example.sh say_hello_and_bye Cecilia
напечатает:
Hello, I am Andres Cecilia Bye!
- Если вы хотите пройти лишнюю милю, вы можете переименовать скрипт в
taskfile
и добавить псевдонимalias task="./taskfile"
в свою оболочку, так что вызов задач внутри файла задач станет еще короче. Некоторые примеры:
task say_hello Cecilia
task say_hello_and_bye Cecilia
task build
task test
Попробуйте прямо сейчас
Выполнение следующей команды с вашего терминала создаст базовый taskfile
в рабочем каталоге:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/acecilia/taskfile/master/start.sh)"