背景

日頃長いコマンドを打つことが多く、 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)

準備できました:

2025-01-18-tabs.png
Figure 1: タブ文字を可視化

リスト

まずはサブコマンド (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

# 略

リスト表示が素敵です:

2025-01-18-just.png
Figure 2: just

実際のコマンド実行も良い感じに:

$ just t
$ just t '-p /SegTree/'
$ just dt

その他メリットとしては、

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

shakemake の代替です。タスクランナーとしても利用できます:

{- 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 を使うことになりそうですが、使い方を理解するのが大変そうです。

その他の選択肢

感想

シンプルなタスクランナーとしては、 justtask が良さそうです。特に just が好みだったので、僕のリポジトリには追加していくと思います。