背景
日頃長いコマンドを打つことが多く、 fish のコマンド履歴に頼り切りになっています。何度も履歴を遡ると、それなりの手間です。
$ # テスト実行
$ cabal test --enable-options
$ # 特定のテスト実行
$ cabal test --enable-options --test-options '-p /SegTree/'
$ # doctest の実行
$ cabal repl --with-ghc=doctest --repl-options='-w -Wdefault'
$ # などなど
ここではタスクランナーを導入し、より簡単にコマンドを打てるようにします。
タスクランナー
haskell-jp の皆様の動向を伺いつつ、幾つかのタスクランナーを試してみました。
bash
無意識に選択肢から外していましたが、凄腕の方も bash を使われていました。僕も man bash を読んだので bash script が書けます:
x スクリプト
#!/usr/bin/env -S bash -euE
IFS=$'\n\t'
_ME="$(basename "$0")"
_cmd_help() {
    cat <<EOS
${_ME} is a simple task runner
help       Shows this message
doctest    Generates Haddock document
test       Runs cabal test
EOS
}
_cmd_doctest() {
    cd "$(dirname "$0")"
    cabal repl --with-ghc=doctest --repl-options='-w -Wdefault' "$@"
}
_cmd_test() {
    cd "$(dirname "$0")"
    cabal test --enable-tests --test-options ''"$@"
}
_run() {
    if [ $# -eq 0 ] ; then
        _cmd_help "$@"
        return
    fi
    local _cmd="${1}"
    shift 1
    case "${_cmd}" in
        'h' | 'help' | '-h' | '--help')
            _cmd_help "$@" ;;
        'dt' | 'doctest')
            _cmd_doctest "$@" ;;
        't' | 'test')
            _cmd_test "$@" ;;
        *)
            echo "no such command ${_cmd}" ;;
    esac
}
_run "$@"
使い勝手は良いと思います:
$ ./x
x is a simple task runner
help       Shows this message
doctest    Generates Haddock document
test       Runs cabal test
$ ./x dt
$ ./x t '-p /SegTree/'
サブコマンドを fzf で選ぶようにしても良いですね。問題は、見ての通りスクリプトが長く、スマートではありません。よりきらびやかな世界を探してみます。
make
make も無意識に選択肢から除外していましたが、やはり凄腕の方々が使われており、普通に便利なツールに見えます。 compilerbook (挫折) 以来ですが、 Makefile も試してみます。
主な文法
make <target> で定義されたコマンドを実行できます。参考: Makefile Tutorial
target: prerequisites
	command
- シェルコマンドの $は$$の形でエスケープします
- @を付けるとコマンドの実行履歴の表示を抑制できます
(脱線) Emacs でタブ文字を表示する
エディタの設定でタブ文字と行末の空白を表示します:
(setopt show-trailing-whitespace t)
(setopt whitespace-style '(tabs  tab-mark))
(require 'whitespace)
(global-whitespace-mode 1)
この記事 を参考に、サイドバー (neotree) ではタブ表示 (と行番号表示) を抑制します:
(defun my-neotree-setup (&rest _)
  (display-line-numbers-mode -1)
  (whitespace-mode -1))
:hook (neo-after-create-hook . my-neotree-setup)
準備できました:

リスト
まずはサブコマンド (target) の一覧を表示してます。 Stack overflow からコマンドを拾ってきました:
.PHONY: help
help:	## Shows this help.
	@echo 'Makefile targets'
	@echo ''
	@sed -ne '/@sed/!s/## //p' $(MAKEFILE_LIST)
.PHONY: doctest
doc:	## Runs doctest.
	cabal repl --with-ghc=doctest --repl-options='-w -Wdefault'
.PHONY: test
test:		## Runs local test
	cabal test --enable-tests --test-options "$(p)"
これで target の一覧を表示できます:
$ make
Makefile targets
help:	Shows this help.
doc:	Runs doctest.
test:	Runs local test.
ただ target: prerequisites の形でコマンドを書くと破綻します。また最近の make には --print-targets オプションもあるとか。
エイリアスを定義する
エイリアス相当の target も作れます:
.PHONY: t
t: test
$ make t
引数を渡す
Target に引数を渡すためには、 arg=value の形で変数定義します:
.PHONY: test
test:		## Runs local test
	cabal test --enable-tests --test-options "$(p)"
あまり使い勝手は良くないですね:
$ make test p='-p /SegTree'
cabal test --enable-tests --test-options "-p /SegTree"
Build profile: -w ghc-9.8.4 -O1
In order, the following will be built (use -v for more details):
 - ac-library-hs-1.1.0.0 (test:ac-library-hs-test) (file /home/tbm/dev/hs/ac-library-hs/dist-newstyle/build/x86_64-linux/ghc-9.8.4/ac-library-hs-1.1.0.0/cache/build changed)
 - ac-library-hs-1.1.0.0 (test:benchlib-test) (file /home/tbm/dev/hs/ac-library-hs/dist-newstyle/build/x86_64-linux/ghc-9.8.4/ac-library-hs-1.1.0.0/cache/build changed)
強引に引数を扱うハック も見ましたが、制限があります。
サブディレクトリからの実行
サブディレクトリからは make <target> できませんでした。残念。
just
just もベター make に見えます。
エディタの設定 (Emacs)
;; https://github.com/leon-barrett/just-mode.el/blob/main/just-mode.el
(leaf just-mode) ;; :ensure t
;; https://github.com/psibi/justl.el
(leaf justl)
シェルの設定 (fish)
if command -sq just
    alias j just
end
Justfile
早速使ってみます。先程の Makefile より綺麗です:
# shows this help message
help:
    @just -l
# runs the benchmark
bench:
    cabal bench --benchmark-options='--output a.html'
# generates Haddock document
doc:
    cabal haddock "$@"
[private]
alias d := doc
test opts='':
    cabal test --enable-tests --test-options '{{opts}}'
[private]
alias t := test
# 略
リスト表示が素敵です:

just
実際のコマンド実行も良い感じに:
$ just t
$ just t '-p /SegTree/'
$ just dt
その他メリットとしては、
- $のエスケープが必要ありませんでした。
- サブディレクトリから justコマンド実行すると、ルートからの実行になりました。
task
task も良さそうですね。未だに書いたことがありませんが、 GitHub Actions の独自言語に近そうです。
version: '3'
tasks:
  doctest:
    aliases: [dt]
    cmds:
      - cabal repl --with-ghc=doctest --repl-options='-w -Wdefault'
  test:
    aliases: [t]
    cmds:
      - cabal test --enable-tests --test-options ''{{.CLI_ARGS}}
引数を渡すには -- で区切る必要がありそうです。これだけちょっと面倒です:
$ task t -- '-p /SegTree/'
cargo-make
cargo-make もベター make 的なツールです。 cargo make として実行できる他、 makers がスタンドアローン版としてインストールされます。
Rust プロジェクトを前提にしている節はあります:
$ makers
[cargo-make] INFO - cargo make 0.37.23
[cargo-make] INFO -
[cargo-make] INFO - Build File: Makefile.toml
[cargo-make] INFO - Task: default
[cargo-make] INFO - Profile: development
[cargo-make] INFO - Execute Command: "cargo" "fmt"
`cargo metadata` exited with an error: error: could not find `Cargo.toml` in `/home/tbm/dev/hs/ac-library-hs/verify` or any parent directory
This utility formats all bin and lib files of the current crate using rustfmt.
Usage: cargo fmt [OPTIONS] [-- <rustfmt_options>...]
Makefile.toml を書いてみます:
[tasks.bench]
alias = "b"
command = "cabal"
args = ["bench", "--benchmark-options='--output a.html'"]
[tasks.doc]
command = "cabal"
args = ["haddock", "$@"]
[tasks.d]
alias = "doc"
[tasks.test]
command = "cabal"
args = ["test", "--enable-tests", "--test-options", "${@:}"]
[tasks.t]
alias = "test"
ちゃんと使えますね:
$ makers t
$ makers t '-p /SegTree/'
サブディレクトリから実行すると、親ディレクトリの Makefile.toml を見つけてくれませんでした。そこは残念です。
shake
shake も make の代替です。タスクランナーとしても利用できます:
{- cabal:
build-depends: base, shake
-}
import Development.Shake
import Development.Shake.Command
import Development.Shake.FilePath
import Development.Shake.Util
main :: IO ()
main = shakeArgs shakeOptions {shakeFiles = "_build"} $ do
  phony "doc" $ do
    cmd_ ["cabal", "haddock"]
  phony "doctest" $ do
    cmd_ ["cabal", "repl", "--with-ghc=doctest", "--repl-options='-w -Wdefault'"]
  -- alias の代わり
  phony "dt" $ need ["doctest"]
  phony "test" $ do
    cmd_ ["cabal", "test", "--enable-tests"]
  phony "t" $ need ["test"]
呼び出し方はもう少しスマートにしたいところです:
$ cabal run Shakefile.hs -- t
リスト機能は minad 神からもリクエストされていました:
$ cabal run Shakefile.hs -- --help
<略>
Targets:
  - doc
  - doctest
  - dt
  - test
  -
引数を受け取るには shakeArgsWith を使うことになりそうですが、使い方を理解するのが大変そうです。
その他の選択肢
- cargo-xtask
 参考: Make your own make
- nix run
感想
シンプルなタスクランナーとしては、 just と task が良さそうです。特に just が好みだったので、僕のリポジトリには追加していくと思います。