🙆

Windows でも go-task を便利に使う

2022/09/04に公開

タスクランナーといえば GNU Make だったり npm だったりといろいろあるが、 Windows になるととたんに使えないことが多い。
が、 go-task は Windows でもかなり便利に使える。なのにあまり Windows で使っている記事が見当たらない。非常にもったいないので備忘録として自分で書いておくことにした。

参考リンク

インストール手順

パッケージマネージャには Scoop を使う。

# コマンドライン
scoop bucket add extras
scoop install task

使い方

タスクの定義は Taskfile.yaml または Taskfile.yml に記述する。
違う名前を使うことも task -t TaskAnother.yml で可能だが、混乱のもとなのであまりお勧めはしない。タスク定義を分割したいなら、後述する includes を使った方がいい。
task --init を実行すると Taskfile.yaml が以下の内容で生成される。

# https://taskfile.dev

version: '3'

vars:
  GREETING: Hello, World!

tasks:
  default:
    cmds:
      - echo "{{.GREETING}}"
    silent: true

タスクを実行する場合は task <TASKNAME> で実行できる。 task だけで実行するとタスク名 default のタスクが実行される。

# コマンドライン
$ task
Hello, World!

Taskfile.yaml から silent: true を削除すると、実行したコマンドも表示される。

# コマンドライン
$ task
task: [default] echo "Hello, World!"
Hello, World!

タスクファイルを分割する場合は includes を使う。
分割したタスクを実行するには task prefix:taskname とする。

# subdir/Taskfile.yaml
version: "3"

tasks:
  subtask:
    cmds:
      - echo "{{.GREETING}}"
    vars:
      GREETING: '{{default "Hello, World!" .GREETING}}'
# Taskfile.yaml
version: "3"

includes:
  sub: subdir/Taskfile.yaml
# コマンドライン
$ task sub:subtask
task: [sub:subtask] echo "Hello, World!"
Hello, World!

注意点

go-task がシェルスクリプトを使えるのは mvdan/sh: A shell parser, formatter, and interpreter with bash support; includes shfmt のおかげ。
ただしいくつか制限があって、実行できるのは PATH が通っている実行ファイルのみ。
IF NOT EXIST {{.DIR}} mkdir -p {{.DIR}} とやっても "IF": executable file not found in $PATH となってエラーになる。
※なぜか echo は使える

バッチファイルのように使いたい場合は cmd /c "hoge" とやればいい。

# Taskfile.yaml
tasks:
  exist:
    cmds:
      - cmd /c "IF NOT EXIST {{.DIR}} mkdir -p {{.DIR}}"
    vars:
      DIR: test

また、実行したコマンドが長すぎて複数行に分けたい場合は >- を使う。
YAML の定義上使えるはずの他の定義だと複数行として認識されなかった。

tasks:
  digest:
    cmds:
      - >-
        aws ecr describe-images --repository-name REPO_NAME --image-ids imageTag=latest
        | jq -r \".imageDetails[].imageDigest\"

変数の上書き

go-task では変数が便利に使える。
以下のように変数を定義すると、変数部分だけ変えたタスクが定義できる。

# Taskfile.yaml
tasks:
  default:
    cmds:
      - echo "{{.GREETING}}"
    vars:
      GREETING: '{{default "Hello, World!" .GREETING}}'

  hello:
    cmds:
      - task: default
        vars: { GREETING: "Hello, Another!" }
# コマンドライン
$ task hello
task: [default] echo "Hello, Another!"
Hello, Another!

これにより、環境別に実行したいタスクを簡単に定義できる。

# Taskfile.yaml
task:
  deploy:
    cmds:
      - echo sls deploy --stage {{.STAGE}} --aws-profile {{.STAGE}}-profile
    vars:
      STAGE: '{{default "dev" .STAGE}}'

  deploy-stg:
    cmds:
      - task: deploy
        vars: { STAGE: "stg" }

  deploy-prod:
    cmds:
      - task: deploy
        vars: { STAGE: "prod" }
# コマンドライン
$ task deploy
task: [deploy] sls deploy --stage dev --aws-profile dev-profile
sls deploy --stage dev --aws-profile dev-profile

$ task deploy-stg
task: [deploy] sls deploy --stage stg --aws-profile stg-profile
sls deploy --stage stg --aws-profile stg-profile

$ task deploy-prod
task: [deploy] sls deploy --stage prod --aws-profile prod-profile
sls deploy --stage prod --aws-profile prod-profile

ちなみに別のタスクファイルのタスクを呼び出したい場合、 タスク名にプレフィックスを付ければいい。

# Taskfile.yaml
includes:
  sub: subdir/Taskfile.yaml
tasks:
  subhello:
    cmds:
      - task: sub:subtask
        vars: { GREETING: "Hello, Subtask!" }

動的な変数

sh を使うと、コマンドの実行結果を変数に格納することができる。
よく Linux で HASH=$(git log -n -1 --format=%h) とできるのに、 Windows だと for /f "usebackq delims=" %%A in (`git log -n -1 --format=%h`) do set HASH=%%A としかできなくて悔しい思いをしたやつだ。

# Taskfile.yaml
tasks:
  hash:
    vars:
      HASH:
        sh: git log -n 1 --format=%h
    cmds:
      - echo git commit hash is [{{.HASH}}].
# コマンドライン
$ task hash
task: [hash] echo git commit hash is [695e863].
git commit hash is [695e863].

ディレクトリ指定

go-task の実行ディレクトリは Taskfile.yaml が存在するディレクトリとなる。

# Z:\src\tasktest\Taskfile.yaml
tasks:
  pwd:
    cmds:
      - cmd /c "cd"
# コマンドライン
$ cd Z:\src\tasktest
$ task pwd
Z:\src\tasktest
$ mkdir subdir
$ cd subdir
$ task -t ..\Taskfile.yaml pwd
Z:\src\tasktest

この挙動を変更する場合は dir を指定する。

# Z:\src\tasktest\Taskfile.yaml
tasks:
  pwd:
    dir: subdir
    cmds:
      - cmd /c "cd"
# コマンドライン
$ cd Z:\src\tasktest
$ task pwd
Z:\src\tasktest\subdir

ちなみに includes した別ファイルのタスクの実行ディレクトリは includes 元の Taskfile.yaml のディレクトリになる。
この挙動を変更したい場合は includes の書き方を以下のようにする。

# Taskfile.yaml
includes:
  sub:
    taskfile: subdir/Taskfile.yml
    dir: ./subdir

不要なタスクを実行しない

preconditions を指定すると、条件に合致する場合のみタスクを実行することができる。

# Taskfile.yaml
tasks:
  pwd:
    cmds:
      - echo subdir exists!
    preconditions:
      - sh: "[ -e subdir ]"
        msg: "subdir does not exist!" # 条件に合致しない場合のメッセージ
# コマンドライン
# subdir が存在しない場合
$ task pwd
task: subdir does not exist!
task: precondition not met

# subdir が存在する場合
subdir exists!

後始末

タスクの途中で一時ファイルを出力したりして、タスク完了時にその一時ファイルを削除したりすることも可能。

# Taskfile.yml
tasks:
  default:
    cmds:
      - echo v1.0.0>VERSION
      - cmd /c "type VERSION"
      - defer: cmd /c "DEL VERSION"
      - cmd /c "if exist VERSION echo VERSION EXISTS!"

この defer は golang と同じように、このタスクが完了してから実行される。
そのため、このタスクの出力は以下のようになる。

# コマンドライン
$ task
task: [default] echo v1.0.0>VERSION
task: [default] cmd /c "type VERSION"
v1.0.0
task: [default] cmd /c "if exist VERSION echo VERSION EXISTS!"
VERSION EXISTS!
task: [default] cmd /c "DEL VERSION"

タスク一覧

タスクが少ないうちは記憶しておけるが、多くなってくるとさすがにきつい。
そんな時は task -l でタスク一覧が確認できる。ただし、各タスクに desc で説明を記載しておく必要がある。

# Taskfile.yaml
tasks:
  cmd1:
    desc: This is 1st command.
    cmds: echo cmd1!
  cmd2:
    desc: This is 2nd command.
    cmds: echo cmd2!
  cmd3:
    cmds: echo cmd3!
# コマンドライン
$ task -l
task: Available tasks for this project:
* cmd1: This is 1st command.
* cmd2: This is 2nd command.

peco と組み合わせる

task -l だと一覧が確認できるだけで、そこまで便利ではない。
peco/peco と組み合わせて工夫するとかなり便利になる。

# Taskfile.yaml
version: "3"
tasks:
  default:
    vars:
      COLON:
        sh: >-
          tmp=$(task -l ^| peco);
          if [ "${tmp:0:2}" = "* " ]; then
            tmp=${tmp:2} # `*` が `echo *` とパースされるのを防ぐ
            list=(${tmp/,/ });
            echo ${list[0]}
          else
            echo
          fi
      TASKNAME:
        sh: tmp={{.COLON}}; echo "${tmp:0:-1}"
    cmds:
      - cmd /c "cls" # peco の残像が残る場合の対策
      - task {{.TASKNAME}}
    preconditions:
      - sh: '[[ "{{.COLON}}" == "{{.TASKNAME}}:" ]]'
        msg: "タスクを選択してください"

  cmd1:
    desc: This is 1st command.
    cmds:
      - echo cmd1!
  cmd2:
    desc: This is 2nd command.
    cmds:
      - echo cmd2!
  cmd3:
    desc: This is 3rd command.
    cmds:
      - echo cmd3!





Discussion