その make、 task に置換可能です!
はじめに
弊社では、ビルド、マイグレーション、Lint など、頻繁に実行する長いコマンドを簡単に扱うため、make
コマンドを利用していました。
make
コマンドのいいところは、ほとんどの Linux ディストリビューションが標準で用意しているところで、Docker コンテナであっても簡単にインストールすることができます。
構文はかなり難解ですが、頑張れば大抵のことはできます。
しかし、コマンドライン引数を扱うには ARG=foo
のように独特な形式で指定しなければならず、不便さを感じていました。
そこで、インストールが容易で、引数の扱いが柔軟なタスクランナーを探していたところ、task
コマンドに出会いました。
task と make の比較
task
コマンドと make
コマンドを簡単に比較してみます。
task のいいところ
- タスクランナーとして作られている
- コマンドライン引数を受け取れる
- コマンドライン引数の前に
--
を入れなければならない点には注意
- コマンドライン引数の前に
-
.env
を読み込める- Rails などのアプリケーションで使っている設定をそのまま使える
- 実行前に Y/N の入力確認ができる
-
make
だとシェル芸で実現しないといけなかったことが、task
だとtask
の機能を使って実現できることが多い -
make
のように変更がなければ再コンパイルしないような設定もできる - 設定が YAML で読みやすい
- 実行時の表示が見やすい
-
make
には劣るが、Go で作られているのでインストールは比較的容易
make のいいところ
- OS のパッケージ管理ソフトでインストールできる
- 知ってる人が多い
- でも使いこなせる人はそこまで多くはない
インストールの問題さえ気にならなければ「make
を使う理由はない」と言ってしまってもいいかもしれません。
使用例
task
コマンドのイメージを掴んでもらうために、実際に弊社で使っている例をいくつか紹介したいと思います。
コマンドライン引数を使う
私たちが make
から task
に乗り換える動機にもなった、コマンドライン引数を扱う方法です。
shell:
desc: アプリケーションのコンテナでコマンドを実行
cmd: docker compose exec app {{.CLI_ARGS | default "bash"}}
# コンテナの中で `bash` を実行
$ task shell
# コンテナの中で `ls -lF` を実行
$ task shell -- ls -lF
--
の後に指定した引数が全て .CLI_ARGS
という変数に入ります。
この例では、引数が指定された場合はそれを、指定されなければ bash
を実行するようにしています。
ただし、.CLI_ARGS
の内容によって処理を分けたりしたい場合は、工夫が必要そうでした。
task
には変数の内容を簡単にパースする方法が用意されてなさそうだったので、シェルの機能などを使って自前でパースする必要がありそうです。
.env
を読み込む
昨今のフレームワークでは環境変数を設定する代わりに .env
というファイルに設定したい内容を書いておくものが多いですが、task
コマンドでもそれと同じことができます。
この機能のいいところは、フレームワークで使っている .env
ファイルを task
コマンドでもそのまま流用できるところです。
db:console:
desc: .env.development の設定を使って DB に接続
dotenv: ['.env.{{.RAILS_ENV | default "development"}}']
cmd: MYSQL_PWD="${DB_PASSWORD}" mysql -h "${DB_HOST}" -u "${DB_USER}" "${DB_NAME}" {{.CLI_ARGS}}
$ task db:console
.env.development
に記載された接続情報を使って DB に接続します。
$ RAILS_ENV=test task db:console
この例では読み込む .env
ファイルの指定に環境変数を使っているので、この環境変数を設定すれば、.env.development
の代わりに .env.test
を読み込ませることもできます。
また、task
コマンドではタスクごとに別々の .env
ファイルを読み込めるので、このタスクでは開発環境用の .env.development
を読み込み、別のタスクではテスト用の .env.test
を読み込むといった使い分けもできます。
コマンド実行前のチェック
ないと困るというほどの機能ではないのですが、コマンドの実行と実行条件のチェックを別々に書けるので、タスク定義が読みやすくなります。
fmt:
desc: コンテナの中で go fmt を実行
preconditions:
- sh: test ! -f /.dockerenv
msg: "Warning: このタスクはコンテナの外から実行してください"
cmd: docker compose exec app go fmt
$ task fmt
task: Warning: このタスクはコンテナの外から実行してください
tasl: precondition not met
この例では、コンテナの外から実行するコマンドを間違ってコンテナの中で実行してしまわないように、実行する環境に /.dockerenv
というファイルがないことを確認しています。
プロンプトで確認
make
でも頑張ればできますが、これが 1 行で書けるのはかなり嬉しいです。
db:reset:
desc: データベースをリセット
prompt: 本当にデータベースをリセットしますか?
cmds:
- task: db:drop
- task: db:create
- task: db:migrate
$ task db:reset
本当にデータベースをリセットしますか? [y/N]:
実行すると確認のプロンプトが表示され、Y
を入力した場合だけ DB がリセットされます。
ディレクトリ一覧を表示
これは、他のコマンドと組み合わせて使うことを想定したタスクの例になります。
module-dirs:
desc: ワークスペースに含まれるモジュールのディレクトリのパスを表示
cmd: go work edit -json | jq -r '.Use[].DiskPath'
silent: true
以下のように使います。
$ task module-dirs | while read dir; do touch "$dir/README.md"; done
この際に余計な出力があると邪魔なので、silent: true
で task
コマンド自体の出力をさせないようにしています。
各ディレクトリに対してコマンドを実行
複数の対象に対して同じコマンドを実行したい場合は for:
を使います。
goget:
desc: ワークスペースに含まれる全てのモジュールで go get . を実行
vars:
MODULES:
sh: task module-dirs
cmds:
- for:
var: MODULES
cmd: cd "{{.ITEM}}" && go get {{.CLI_ARGS | default "."}}
ignore_error: true
$ task goget -- -t .
この例では、先ほどの module-dirs
で得た全てのディレクトリに順に入って go get .
を実行しています。
ignore_error: true
を指定することで、エラーがあっても続きを処理するようにしています。
変数を使う
vars:
を使うと環境変数とは別に Taskfile.yml
の中でだけ有効な変数を定義することができます。
特定のタスクだけで有効な変数も、全てのタスクで有効なグローバル変数も定義できます。
vars:
GOLANGCI_LINT_VERSION: v1.62.2
tasks:
install:lint:
desc: Linter をインストール
cmd: |-
wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh |
sh -s -- -b $(go env GOPATH)/bin {{.GOLANGCI_LINT_VERSION}}
requires:
vars: [GOLANGCI_LINT_VERSION]
$ task install:lint
task: [install:lint] wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh |
sh -s -- -b $(go env GOPATH)/bin v1.62.2
2024-12-10 16:27:16 URL:https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh [11591/11591] -> "-" [1]
golangci/golangci-lint info checking GitHub for tag 'v1.62.2'
golangci/golangci-lint info found version: 1.62.2 for v1.62.2/linux/arm64
golangci/golangci-lint info installed /go/bin/golangci-lint
この例ではインストールするツールのバージョンをグローバル変数で定義しています。
CPU アーキテクチャごとに実行する内容を変える
x86_64 と ARM64 で実行する内容を変えたり、あるいは想定してないアーキテクチャではタスクを実行させないこともできます。
install:aws-cli:
desc: AWS CLI をインストール
platforms: [linux/amd64, linux/arm64]
dir: /usr/src
cmds:
- cmd: curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
platforms: [linux/amd64]
- cmd: curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip"
platforms: [linux/arm64]
- unzip awscliv2.zip
- ./aws/install
- defer: rm -rf awscliv2.zip aws
$ task install:aws-cli
task: [install:aws-cli] curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 62.3M 100 62.3M 0 0 35.5M 0 0:00:01 0:00:01 --:--:-- 35.5M
task: [install:aws-cli] unzip awscliv2.zip
Archive: awscliv2.zip
creating: aws/
creating: aws/dist/
inflating: aws/README.md
...
inflating: aws/dist/docutils/parsers/rst/include/s5defs.txt
task: [install:aws-cli] ./aws/install
You can now run: /usr/local/bin/aws --version
task: [install:aws-cli] rm -rf awscliv2.zip aws
この例では、x86_64 上の Linux か、ARM64 上の Linux でしかタスクを実行できないようにした上で、それぞれで別々のソースからファイルをダウンロードするようにしています。
最後のコマンドに付けている defer:
は、手前のコマンドに成功しても失敗しても実行するようにする指示で、この例では一時ファイルを削除しています。
シェルで頑張る
もちろん make
のようにシェルで頑張ることもできます。
skipped-tests:
desc: `t.Skip()` を含むテストのパスを表示
cmd: |-
tests=$(find . -name '*_test.go' -exec grep -l 't\.Skip()' '{}' + | sort)
if [ -n "$tests" ]; then
echo "$tests"
else
echo "No skipped tests found." >&2
fi
silent: true
$ task test:skipped
No skipped tests found.
この例ではシェルの if を使って条件分岐をさせていますが、実際に使ってみると、シェルで頑張ってなんとかしないといけない場面はほとんどありませんでした。
おわりに
普段使うコマンドをどんどん追加していき、気づいたら 100 個近くのタスクを登録していました。
お陰でほとんどの作業を簡単なタスク名の入力だけで実行できるようになりました。
引数の問題にぶつかる前は、make
コマンドでも十分なんじゃないかと思っていたのですが、make
コマンドで色々なタスクをやらせようとすると、どうしても Makefile
が複雑になりがちで、シェルを使いこなしている人にしかメンテナンスできなくなってしまいます。
task
コマンドを使うには、Taskfile.yml
の仕様を覚える必要はありますが、ドキュメントもしっかりしていますし、何より仕様がシンプルでわかりやすいです。
使いやすければ、あれもこれもタスクにしようと考えるようになり、開発者体験もよくなっていきます。
この記事を読んで task
コマンドに興味を持たれた方は、ぜひ試しに使ってみてください。
We are hiring!!
株式会社カウンターワークスでは、開発者体験を向上させながら事業を推進できるメンバーを募集しています。
興味のある方はぜひ以下のリンクからご応募ください!
ポップアップストアや催事イベント向けの商業スペースを簡単に予約できる「SHOPCOUNTER」と商業施設向けリーシングDXシステム「SHOPCOUNTER Enterprise」を運営しています。エンジニア採用強化中ですので、興味ある方はお気軽にご連絡ください! counterworks.co.jp/
Discussion