まえがき
プライベートでの開発でNimをタスクランナーとして使ってみたら思いの外快適でした。
何が良かったか、何故良かったかについて書きます。
Nimについて知らないかたは以下の記事を参照ください。
NimScriptを使ってみる
早速ですがNimScriptを書いてみます。
以下はNimScriptです。以下のコードを hello.nims として保存します。
(拡張子nimではなくnimsです)
echo "Hello NimScript"
保存した前述のコードを実行してみます。
以下のように実行します。
$ 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
というオプションを渡して実行すると、余計な出力は除外できます。
$ nim --hints:off hello.nims
Hello NimScript
いちいち --hints:off
と書きたくない方はaliasを設定すると良いと思います。
僕は以下aliasを.bashrc
に書いて使用しています。
alias nims='nim --hints:off'
「なんだ、ただのNimのコード実行しただけじゃん」と思った方もいるかも知れません。
このあとはNimScriptが通常のNimと何が違うかについて説明します。
NimScriptってなに?
公式がNimScriptについて説明しています。
厳密に言えば、NimScriptはNimの組み込み仮想マシン(VM)で評価できるNimのサブセ
ットです。このVMは、Nimのコンパイル時機能評価機能に使用されます。
前述の記事をつまみ食いしながら説明します。
何ができるの/何ができないの
できないこと
NimとNimScriptの違いで一番意識するのは、NimScriptには使えない標準ライブラリが存在する点です。
前述のリンクに使用可能な標準モジュールの一覧が記載されています。
例えば、以下の標準ライブラリはNimScriptで使用できません。
- json
- streams
- unidecode
できること
しかしながら、NimScriptでだけできることがあります。
NimScriptはデフォルトでnimscriptライブラリを読み込んでいます。
nimscriptにはブロックスコープ内でだけcd
できるwithDir
を始め、
ターミナル操作でよく使うファイル操作系のプロシージャ、外部コマンドの実行、環境変数の操作などが可能です。
普段何気なく使うシェルの代わりにNimScriptを試用するということも十分に可能です。
それ以外にもいろいろ違いがあるのですが、詳細は前述の公式ドキュメントを参照してください。
NimScriptを使うと何が嬉しいの?
前述の公式ドキュメントを要約すると以下のメリットがあります
- Nimのコードがそのまま使えるので違う言語を使うことのコンテキストスイッチがない
- クロスプラットフォームで動作する
- (Nimがインストールされているなら)
- OSやアーキテクチャの違いを判別できるモジュールが提供されている
- BashやBatchの代わりとして通用する
- Unixならシェバンにも対応している
- Nimの強力なメタプロがそのままできる
- Nimの標準ライブラリの殆どが使える
- DevOpsやシステム管理固有の task を実行するための機能を備えている
タスクランナーとしてのNimScript
前述のメリットの一つに task について触れました。
nimscriptライブラリには task
という機能(template)が存在します。
これとNimScriptがとても便利で、タスクランナーとして有用なのではないか?
と考えたのが今回の記事の趣旨です。
タスクを定義してみる
以下のNimScriptを書いてカレントディレクトリに config.nims として保存します。
このファイル名は固定です。
task hello, "HelloWorldを出力する":
echo "Hello World"
カレントディレクトリこのファイルが存在する状態で、nimコマンドを実行します。
$ ls config.nims
config.nims
$ nim --hints:off
hello HelloWorldを出力する
タスク名とその説明が出力されました。
タスクを実行してみます。
$ 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
を実行してみます。
$ 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ディレクトリ配下で以下のコマンドを使用します。
$ terraform plan
$ terraform apply
$ terraform destroy
Ansible
NimScriptを使わないで操作するときはAnsibleディレクトリ配下で以下のコマンドを使用します。
$ ansible-playbook -i hosts/prd/inventory site.yml
Terraformはカレントディレクトリのtfファイルを読み込んで実行するためカレントディレクトリが重要です。
Ansibleの編集をしていてTerraformの操作がしたくなったときに
いちいちディレクトリを移動するのが面倒です。
しかしterraformの操作をconfig.nimsに書いておくだけで、
プロジェクト配下のどこからでもタスクを呼び出せてハッピーです。
抜粋ですが、こんな感じのconfig.nimsを書いて使っていました。
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という迷路生成のコマンドのリポジトリのnimbleファイルです。
# 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ファイルと同じ階層にいる状態で以下のように実行します。
$ nimble docs
$ nimble ci
$ nimble archive
簡単、かつ便利ですね。
よく使う処理をわざわざシェルスクリプトとして作る必要もなくなります。
taskの連続呼び出しも可能
build, test, docs, archive などのタスクがあるときに、全部を一気に実行してくれるタスクも欲しくなると思います。
そういう時は selfExec
というプロシージャで実現できます。
selfExec
の引数にタスク名を書くだけです。
以下のように使います。
config.nims
task all, "全てのタスクを実行する":
selfExec "build"
selfExec "test"
selfExec "docs"
selfExec "archive"
task build, "アプリをビルドする":
echo "アプリをビルドした"
task test, "アプリをテストする":
echo "アプリをテストした"
task docs, "ドキュメントを生成する":
echo "ドキュメントを生成した"
task archive, "リリース物を生成する":
echo "リリース物を生成するした"
実行結果。
$ 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のシェルスタイルガイドにも書かれているのですが、シェルはコマンドのラップなどの簡単な処理のみに使用を限定するべきです。
100行を超えたら別の言語で書き直したほうがよいです。
タスク定義ファイルはいろんな処理を詰め込んで肥大化しやすいですし、
そこにわざわざ保守性の低くなりやすいシェルを使うのは得策ではないと考えます。
Makefileじゃだめなの?
CのビルドツールでUnixOSだとだいたいデフォルトで入ってるので手軽に使えて便利だと思います。
異なるタスクのチェインもやりやすくて、簡単なタスクのみのときはとても強力だと思います。
ただこれも記述するタスクの実態はシェルですから、前述の問題点をすべて引き継いでいます。
さらにMakefileは複数行の処理を書きづらく、
条件分岐やループ処理を書き始めると途端にめちゃくちゃ可読性が下がります。
シェル変数のエスケープとMakefile固有の構文の混在でますます可読性が下がり、バグを含みやすいです。
こちらもタスクが肥大化する懸念があるなら使うべきではないと思います。
その他のプログラミング言語
Node.jsのプロジェクトならJSで書いてpackage.jsonから呼び出すようにするので良いと思います。
RubyならRakefile、Pythonならfabricなど。
各言語には各言語のツールがあるので、それに合わせるのが適当だと考えます。
そこに無理にNimScriptを導入するのはコンテキストスイッチの発生でかえって保守コストが上がると考えます。
Nimを採用したプロジェクトならNimScriptは
タスクランナーとして大いに役立つと思います。
まとめ
以下の内容について書きました。
- NimScriptとNimの違い
- NimScriptのメリット
- タスクランナーとして使う例
- 他の手段との比較
NimScriptはNimを使うプロジェクトでとても役立つと思います。
オレオレNimScriptを公開してくださってる方もいて、依存ライブラリのインストールタスクや、静的ビルドのタスクなどいろいろ用意してあってとても参考になります。
NimScriptはNimをインストールしている環境なら誰でも使用可能です。
ぜひ試してみてください。
以上です。