タスクランナーにNim(nimscript)を使う

# まえがき

プライベートでの開発でNimをタスクランナーとして使ってみたら思いの外快適でした。
何が良かったか、何故良かったかについて書きます。

Nimについて知らないかたは以下の記事を参照ください。

* [至高の言語、Nimを始めるエンジニアへ](https://qiita.com/rigani/items/6e87c7cee6903ed65ed2)
* [Nimを知ってほしい](https://qiita.com/happy_packet/items/3c59abf9875f4f6869c9)

# NimScriptを使ってみる

早速ですがNimScriptを書いてみます。
以下はNimScriptです。以下のコードを **hello.nims** として保存します。
(拡張子nimではなくnimsです)

```nim
echo "Hello NimScript"
```

保存した前述のコードを実行してみます。
以下のように実行します。

```bash
$ nim hello.nims
Hint: used config file '/home/jiro4989/.choosenim/toolchains/nim-1.0.2/config/nim.cfg' [Conf]
Hint: used config file '/tmp/hello.nims' [Conf]
Hello NimScript
```

はい、NimScriptできました。簡単ですね。
ただしこれだと`Hint:`というメッセージも一緒に出力されて目障りに感じるかも知れません。
その時は `--hints:off` というオプションを渡して実行すると、余計な出力は除外できます。

```bash
$ nim --hints:off hello.nims
Hello NimScript
```

いちいち `--hints:off` と書きたくない方はaliasを設定すると良いと思います。
僕は以下aliasを`.bashrc`に書いて使用しています。

```bash
alias nims='nim --hints:off'
```

「なんだ、ただのNimのコード実行しただけじゃん」と思った方もいるかも知れません。
このあとはNimScriptが通常のNimと何が違うかについて説明します。

# NimScriptってなに？

公式がNimScriptについて説明しています。

https://nim-lang.org/docs/nims.html

> 厳密に言えば、NimScriptはNimの組み込み仮想マシン（VM）で評価できるNimのサブセ
> ットです。このVMは、Nimのコンパイル時機能評価機能に使用されます。

前述の記事をつまみ食いしながら説明します。

# 何ができるの/何ができないの

## できないこと

NimとNimScriptの違いで一番意識するのは、NimScriptには使えない標準ライブラリが存在する点です。
前述のリンクに使用可能な標準モジュールの一覧が記載されています。

例えば、以下の標準ライブラリはNimScriptで使用できません。

* json
* streams
* unidecode

## できること

しかしながら、NimScriptでだけできることがあります。
NimScriptはデフォルトで[nimscript](https://nim-lang.org/docs/nimscript.html)ライブラリを読み込んでいます。

nimscriptにはブロックスコープ内でだけ`cd`できる`withDir`を始め、
ターミナル操作でよく使うファイル操作系のプロシージャ、外部コマンドの実行、環境変数の操作などが可能です。

普段何気なく使うシェルの代わりにNimScriptを試用するということも十分に可能です。

それ以外にもいろいろ違いがあるのですが、詳細は前述の公式ドキュメントを参照してください。

# NimScriptを使うと何が嬉しいの？

前述の公式ドキュメントを要約すると以下のメリットがあります

* Nimのコードがそのまま使えるので違う言語を使うことのコンテキストスイッチがない
* クロスプラットフォームで動作する
  * (Nimがインストールされているなら)
* OSやアーキテクチャの違いを判別できるモジュールが提供されている
* BashやBatchの代わりとして通用する
  * Unixならシェバンにも対応している
* Nimの強力なメタプロがそのままできる
* Nimの標準ライブラリの殆どが使える
* DevOpsやシステム管理固有の **task** を実行するための機能を備えている

# タスクランナーとしてのNimScript

前述のメリットの一つに task について触れました。
nimscriptライブラリには `task` という機能(template)が存在します。
これとNimScriptがとても便利で、タスクランナーとして有用なのではないか？
と考えたのが今回の記事の趣旨です。

## タスクを定義してみる

以下のNimScriptを書いてカレントディレクトリに **config.nims** として保存します。
このファイル名は固定です。

```nim
task hello, "HelloWorldを出力する":
  echo "Hello World"
```

カレントディレクトリこのファイルが存在する状態で、nimコマンドを実行します。

```bash
$ ls config.nims
config.nims

$ nim --hints:off
hello                HelloWorldを出力する
```

タスク名とその説明が出力されました。
タスクを実行してみます。

```bash
$ nim --hints:off hello
Hello World
```

ファイル名を指定しないでタスクが実行できました。簡単ですね。

NimScriptでは、現在のパスに到達する過程の全てのconfig.nimsを読み込んでくれます。
config.nims内に `task` templateを使用してタスクを定義すると、それらが全て使用可能になります。

たとえば以下のようにconfig.nimsを配置します。

```
/
`-tmp/
  +-parent/
  | +-child/
  | | `- config.nims
  | `-config.nims
  `-config.nims
```

このとき、`child`配下で`nim --hints:off`を実行してみます。

```bash
$ nim --hints:off
hello                HelloWorldを出力する
parent               Parent
child                Child
```

それぞれのconfig.nimsのタスク定義が読み込まれていることがわかりました。

この挙動により、リポジトリのルート直下にconfig.nimsを配置して、
config.nimsにタスクを集約しつつ、リポジトリ配下のどこからでもタスクを呼び出せるようになります。
`npm`とか同じような挙動ですね。便利です。

# 実際にタスクランナーとして使ってみた

自分のプライベートリポジトリでNimScriptを使ってみました。
AWSインフラのリソース定義と、そのプロビジョニングコードを管理するinfraというリポジトリです。

infraリポジトリではterraformとansibleを使用しています。
プロジェクト構造は以下のようになっています。

```
infra/
+- terraform/
|  +- ec2.tf
|  +- network.tf
|  `- terraform.tfvars
|     ...
+- ansible/
|  +- hosts/
|  +- roles/
|  `- site.yml
`- config.nims
```

## Terraform

NimScriptを使わないで操作するときはTerraformディレクトリ配下で以下のコマンドを使用します。

```bash
$ terraform plan
$ terraform apply
$ terraform destroy
```

## Ansible

NimScriptを使わないで操作するときはAnsibleディレクトリ配下で以下のコマンドを使用します。

```bash
$ ansible-playbook -i hosts/prd/inventory site.yml
```

Terraformはカレントディレクトリのtfファイルを読み込んで実行するためカレントディレクトリが重要です。

Ansibleの編集をしていてTerraformの操作がしたくなったときに
いちいちディレクトリを移動するのが面倒です。

しかしterraformの操作をconfig.nimsに書いておくだけで、
プロジェクト配下のどこからでもタスクを呼び出せてハッピーです。

抜粋ですが、こんな感じのconfig.nimsを書いて使っていました。

```nim
import strformat, strutils
from os import `/`

let
  tf = "terraform"
  tfDir = thisDir() / tf
  cmdTf = tf
  ans = "ansible"
  ansDir = thisDir() / ans

################################################################################
# Terraform
################################################################################

task plan, "AWSリソースの生成のdry run":
  withDir tfDir:
    exec &"{cmdTf} plan"

task apply, "AWSリソースを生成する":
  withDir tfDir:
    exec &"{cmdTf} apply"

task destroy, "AWSリソースを削除する":
  withDir tfDir:
    exec &"{cmdTf} destroy"

task setMyIP, "terraform.tfvarsに自身のグローバルIPをセットする":
  withDir tfDir:
    # コマンドの実行結果の取得
    let myIP = gorge "curl -s ifconfig.io"

    # my_ipの行を書き換え
    var contents: seq[string]
    let varFile = "terraform.tfvars"
    for line in readFile(varFile).split("\n"):
      let l =
        if line.startsWith("my_ip"):
          &"""my_ip = "{myIP}" """
        else:
          line
      contents.add(l)
    writeFile(varFile, contents.join("\n"))

task fmt, "tfファイルのソースコードを整形する":
  withDir tfDir:
    exec &"{cmdTf} fmt"

################################################################################
# Ansible
################################################################################

task up, "docker-composeを起動する":
  withDir ansDir:
    exec "docker-compose up -d"

task ssh, "sshdコンテナに接続する":
  exec "ssh jiro4989@123.45.67.8"

task ping, "Ansibleで疎通確認をする":
  withDir ansDir:
    exec "ansible all -i hosts/local/inventory -m ping -k"
```

# Nimbleもタスクランナー

Nimのプロジェクトだと、プロジェクトルート直下に`*.nimble`というファイルを配置します。
コレも実はNimScriptで、nimble用にさらに特殊な変数が利用可能になっています。

このファイルにも`task`を定義可能で、
ドキュメント生成やデプロイファイルの生成などをタスクとして定義すると便利です。

以下は拙作の[maze](https://github.com/jiro4989/maze)という迷路生成のコマンドのリポジトリのnimbleファイルです。

```nim
# Package

version       = "1.0.1"
author        = "jiro4989"
description   = "A command and library to generate mazes"
license       = "MIT"
srcDir        = "src"
installExt    = @["nim"]
bin           = @["maze"]
binDir        = "bin"


# Dependencies

requires "nim >= 1.0.2"

task docs, "Generate documents":
  rmDir "docs"
  exec "nimble doc --project --index:on -o:docs src/maze.nim"

task ci, "Run CI":
  exec "nim -v"
  exec "nimble -v"
  exec "nimble check"
  exec "nimble install -Y"
  exec "nimble test -Y"
  exec "nimble docs -Y"
  exec "nimble build -d:release -Y"
  exec "./bin/maze -h"
  exec "./bin/maze -v"

import strformat

task archive, "Create archived assets":
  let app = "maze"
  let assets = &"{app}_{buildOS}"
  let dir = &"dist/{assets}"
  mkDir &"{dir}/bin"
  cpFile &"bin/{app}", &"{dir}/bin/{app}"
  cpFile "LICENSE", &"{dir}/LICENSE"
  cpFile "README.adoc", &"{dir}/README.adoc"
  withDir "dist":
    exec &"tar czf {assets}.tar.gz {assets}"
```

`docs`, `ci`, `archive`という3つのタスクを独自に定義しています。
呼び出す時はそれぞれnimbleファイルと同じ階層にいる状態で以下のように実行します。

```bash
$ nimble docs
$ nimble ci
$ nimble archive
```

簡単、かつ便利ですね。
よく使う処理をわざわざシェルスクリプトとして作る必要もなくなります。

## taskの連続呼び出しも可能

build, test, docs, archive などのタスクがあるときに、全部を一気に実行してくれるタスクも欲しくなると思います。
そういう時は `selfExec` というプロシージャで実現できます。
`selfExec`の引数にタスク名を書くだけです。
以下のように使います。

config.nims

```nim
task all, "全てのタスクを実行する":
  selfExec "build"
  selfExec "test"
  selfExec "docs"
  selfExec "archive"

task build, "アプリをビルドする":
  echo "アプリをビルドした"

task test, "アプリをテストする":
  echo "アプリをテストした"

task docs, "ドキュメントを生成する":
  echo "ドキュメントを生成した"

task archive, "リリース物を生成する":
  echo "リリース物を生成するした"
```

実行結果。

```bash
$ nims all
Hint: used config file '/home/jiro4989/.choosenim/toolchains/nim-1.0.4/config/nim.cfg' [Conf]
Hint: used config file '/tmp/config.nims' [Conf]
アプリをビルドした
Hint: used config file '/home/jiro4989/.choosenim/toolchains/nim-1.0.4/config/nim.cfg' [Conf]
Hint: used config file '/tmp/config.nims' [Conf]
アプリをテストした
Hint: used config file '/home/jiro4989/.choosenim/toolchains/nim-1.0.4/config/nim.cfg' [Conf]
Hint: used config file '/tmp/config.nims' [Conf]
ドキュメントを生成した
Hint: used config file '/home/jiro4989/.choosenim/toolchains/nim-1.0.4/config/nim.cfg' [Conf]
Hint: used config file '/tmp/config.nims' [Conf]
リリース物を生成するした
```

# タスクランナーの他の選択肢

## シェルスクリプトじゃだめなの？

一番最初に候補になる手段だと思います。
数行程度の処理をまとめたりする用途だとこちらのほうが楽かも知れません。
`getopts`などのBash組み込みの機能を使えば引数からサブコマンドを渡すことでタスクランナーにすることが可能です。

しかしシェルは可読性が下がりやすく保守しづらいです。
[Googleのシェルスタイルガイド](http://google.github.io/styleguide/shell.xml)にも書かれているのですが、シェルはコマンドのラップなどの簡単な処理のみに使用を限定するべきです。
100行を超えたら別の言語で書き直したほうがよいです。

タスク定義ファイルはいろんな処理を詰め込んで肥大化しやすいですし、
そこにわざわざ保守性の低くなりやすいシェルを使うのは得策ではないと考えます。

## Makefileじゃだめなの？

CのビルドツールでUnixOSだとだいたいデフォルトで入ってるので手軽に使えて便利だと思います。
異なるタスクのチェインもやりやすくて、簡単なタスクのみのときはとても強力だと思います。

ただこれも記述するタスクの実態はシェルですから、前述の問題点をすべて引き継いでいます。

さらにMakefileは複数行の処理を書きづらく、
条件分岐やループ処理を書き始めると途端にめちゃくちゃ可読性が下がります。
シェル変数のエスケープとMakefile固有の構文の混在でますます可読性が下がり、バグを含みやすいです。

こちらもタスクが肥大化する懸念があるなら使うべきではないと思います。

## その他のプログラミング言語

Node.jsのプロジェクトならJSで書いてpackage.jsonから呼び出すようにするので良いと思います。
RubyならRakefile、Pythonならfabricなど。
各言語には各言語のツールがあるので、それに合わせるのが適当だと考えます。
そこに無理にNimScriptを導入するのはコンテキストスイッチの発生でかえって保守コストが上がると考えます。

Nimを採用したプロジェクトならNimScriptは
タスクランナーとして大いに役立つと思います。

# まとめ

以下の内容について書きました。

* NimScriptとNimの違い
* NimScriptのメリット
* タスクランナーとして使う例
* 他の手段との比較

NimScriptはNimを使うプロジェクトでとても役立つと思います。
[オレオレNimScript](https://github.com/kaushalmodi/nim_config#list-available-tasks)を公開してくださってる方もいて、依存ライブラリのインストールタスクや、静的ビルドのタスクなどいろいろ用意してあってとても参考になります。

NimScriptはNimをインストールしている環境なら誰でも使用可能です。
ぜひ試してみてください。

以上です。