NimでオレオレCLIツールを作った


まえがき

  • Nimで自分用の簡易なCLIツールを作って普段使いしている話を書く

なぜNimで作ったか

選定理由は以下のとおりです。

  • ネイティブバイナリを出力できる
  • 高速
  • 書きやすい
  • 読みやすい
  • めちゃくちゃ少ないコード量で目的を達成できる
  • 静的型付け言語なのでコンパイル時に型チェックができる
  • テストしやすい

成果物

GitHub - jiro4989/yourutils

インストール

あまりにもオレオレすぎたのでNimのパッケージに追加のPRはだしてません。

nimble installにURLを指定してやればNimのパッケージに存在しなくてもインストールできます。

nimble install https://github.com/jiro4989/yourutils

作ったコマンド

12個あります。それぞれは非常に単純で簡素なコマンドです。

flat

複数行の標準入力を任意の列数にする。
-dオプションで区切り文字を指定できる。

$ seq 5 | flat
1 2 3 4 5

$ seq 5 | flat -n 2
1 2
3 4
5

$ seq 5 | flat -d ,
1,2,3,4,5

rep

任意の回数文字を連続して出力する。

$ rep 5 A
AAAAA

$ echo 'A B' | rep 5 -i
A BA BA BA BA B

$ rep 1 3 5 A
A
AAA
AAAAA

$ rep $(seq 5) A
A
AA
AAA
AAAA
AAAAA

ucut

cutのunicode対応版。オプションは必要最低限。
マルチバイト文字で入力を区切って取り出せる。

$ echo 1あ2あ3 | ucut -d-f 1,2
1 2

-fのフィールドは何個同じやつを設定しても良いようにしてます。

$ echo "田中,男,20歳,野球,東京都,なし" | ucut -d , -D " " -f 1,2,2,3,2,1,4
田中 男 男 20歳 男 田中 野球

codepoint

文字のコードポイントを出力する。

$ echo hello world | codepoint
char code_point code_point(hex)
h 104 \U68
e 101 \U65
l 108 \U6C
l 108 \U6C
o 111 \U6F
  32 \U20
w 119 \U77
o 111 \U6F
r 114 \U72
l 108 \U6C
d 100 \U64

align

テキストの位置を左、中央、右に揃えるコマンド。
マルチバイト文字の表示上の文字幅の違いにも対応している。

% echo $'123\nあいう\nえお' | align right 
   123
あいう
  えお

% echo $'1234\nああああああ\nうえお' | align center -p =  
====1234====
ああああああ
===うえお===

aggr

CSVなどの列データを集計して件数、最小値、最大値、合計、平均値、中央値、95パーセンタイル値を計算する。
-fで何列目のデータを集計するかも指定できます。

$ seq 100 | aggr -D ,
file_name,field,count,min,max,total,avg,median,95percentile
stdin,0,100,1,100,5050,50.5,51,96

subnet

IPアドレスとCIDRを引数に渡すとそのサブネットマスクを出力する。

$ subnet 100.100.200.1/24
100.100.200.1   24  01100100011001001100100000000001    11111111111111111111111100000000

Bashのブレース展開と組み合わせて出力したり。

$ subnet 100.100.200.1/{1..32}
ip_address      cidr    bin     mask
100.100.200.1   1       01100100011001001100100000000001        10000000000000000000000000000000
100.100.200.1   2       01100100011001001100100000000001        11000000000000000000000000000000
100.100.200.1   3       01100100011001001100100000000001        11100000000000000000000000000000
100.100.200.1   4       01100100011001001100100000000001        11110000000000000000000000000000
100.100.200.1   5       01100100011001001100100000000001        11111000000000000000000000000000
100.100.200.1   6       01100100011001001100100000000001        11111100000000000000000000000000
100.100.200.1   7       01100100011001001100100000000001        11111110000000000000000000000000
100.100.200.1   8       01100100011001001100100000000001        11111111000000000000000000000000
100.100.200.1   9       01100100011001001100100000000001        11111111100000000000000000000000
100.100.200.1   10      01100100011001001100100000000001        11111111110000000000000000000000
100.100.200.1   11      01100100011001001100100000000001        11111111111000000000000000000000
100.100.200.1   12      01100100011001001100100000000001        11111111111100000000000000000000
100.100.200.1   13      01100100011001001100100000000001        11111111111110000000000000000000
100.100.200.1   14      01100100011001001100100000000001        11111111111111000000000000000000
100.100.200.1   15      01100100011001001100100000000001        11111111111111100000000000000000
100.100.200.1   16      01100100011001001100100000000001        11111111111111110000000000000000
100.100.200.1   17      01100100011001001100100000000001        11111111111111111000000000000000
100.100.200.1   18      01100100011001001100100000000001        11111111111111111100000000000000
100.100.200.1   19      01100100011001001100100000000001        11111111111111111110000000000000
100.100.200.1   20      01100100011001001100100000000001        11111111111111111111000000000000
100.100.200.1   21      01100100011001001100100000000001        11111111111111111111100000000000
100.100.200.1   22      01100100011001001100100000000001        11111111111111111111110000000000
100.100.200.1   23      01100100011001001100100000000001        11111111111111111111111000000000
100.100.200.1   24      01100100011001001100100000000001        11111111111111111111111100000000
100.100.200.1   25      01100100011001001100100000000001        11111111111111111111111110000000
100.100.200.1   26      01100100011001001100100000000001        11111111111111111111111111000000
100.100.200.1   27      01100100011001001100100000000001        11111111111111111111111111100000
100.100.200.1   28      01100100011001001100100000000001        11111111111111111111111111110000
100.100.200.1   29      01100100011001001100100000000001        11111111111111111111111111111000
100.100.200.1   30      01100100011001001100100000000001        11111111111111111111111111111100
100.100.200.1   31      01100100011001001100100000000001        11111111111111111111111111111110
100.100.200.1   32      01100100011001001100100000000001        11111111111111111111111111111111

IPアドレス部に関してはIPのレンジを指定したりできる。

# 1-10 で1から10の値を全部出力

$ subnet 100.100.200.1-10/24
ip_address      cidr    bin     mask
100.100.200.1   24      01100100011001001100100000000001        11111111111111111111111100000000
100.100.200.2   24      01100100011001001100100000000010        11111111111111111111111100000000
100.100.200.3   24      01100100011001001100100000000011        11111111111111111111111100000000
100.100.200.4   24      01100100011001001100100000000100        11111111111111111111111100000000
100.100.200.5   24      01100100011001001100100000000101        11111111111111111111111100000000
100.100.200.6   24      01100100011001001100100000000110        11111111111111111111111100000000
100.100.200.7   24      01100100011001001100100000000111        11111111111111111111111100000000
100.100.200.8   24      01100100011001001100100000001000        11111111111111111111111100000000
100.100.200.9   24      01100100011001001100100000001001        11111111111111111111111100000000
100.100.200.10  24      01100100011001001100100000001010        11111111111111111111111100000000

-Cで出力に色を付けられる。

$ subnet -C 100.100.200.1/{16..32}

tb

タブ区切りの入力を任意のテーブル形式で出力する。

$ paste <(seq 5) <(seq 6 10) <(seq 11 15)
1       6       11
2       7       12
3       8       13
4       9       14
5       10      15

$ paste <(seq 5) <(seq 6 10) <(seq 11 15) | tb
|1|6|11|
|:---:|:---:|:---:|
|2|7|12|
|3|8|13|
|4|9|14|
|5|10|15|

HTML形式で出力。

$ paste <(seq 5) <(seq 6 10) <(seq 11 15) | tb -f html
<table>
<thead>
<tr><td>1</td><td>6</td><td>11</td></tr>
</thead>
<tbody>
<tr><td>2</td><td>7</td><td>12</td></tr>
<tr><td>3</td><td>8</td><td>13</td></tr>
<tr><td>4</td><td>9</td><td>14</td></tr>
<tr><td>5</td><td>10</td><td>15</td></tr>
</tbody>
</table>

AsciiDoc形式で出力。

$ paste <(seq 5) <(seq 6 10) <(seq 11 15) | tb -f adoc
[options="header"]
|=================
|1|6|11
|2|7|12
|3|8|13
|4|9|14
|5|10|15
|=================

renames

ディレクトリ配下の全てのディレクトリとファイル名を再帰的に変更する。
任意の文字を消したり、別の文字に置き換えたり、小文字にしたり大文字にしたりできる。
どのファイルが更新されるかのDry runもできる。

AmazonMusicで音楽を購入してDLしたときにファイル名に半角スペースやら全角スペースやらクオートやら記号やらが含まれていて血管がブチギレそうになったときに作った。

ファイル名から空白文字(半角スペース、全角スペース、タブ文字)を削除する例。
(deleteはデフォルトで上記文字が削除対象)

-dはDry runの指定です。リネームはしません。

任意の文字を別の文字に置換する例。

-Fは変更対象のみ出力する指定。

zshprompt

Zshのプロンプト。直前のコマンドの成功の可否でプロンプトの表情が変わる。
あと時刻によって色が変わったりする。

tiff

時刻と時刻の差を求める。
画像のTIFFと勘違いされる可能性があるので名前変えたいけれどイイカンジの名前が思いうかばない。

$ tiff 19:00 18:00
3600 seconds

$ tiff 19:00 18:00 -H
1 hours

$ tiff 19:00 18:00 -M
60 minutes

jsonfmt

標準入力から読み込んだJSONを整形して出力する。
内部的には標準ライブラリを使ってjson.prettyしてるだけ。

$ echo '{"a":1, "b":true, "c":[1, 2, 3], "d":{"a":1, "b":"test"}}' | jsonfmt
{
  "a": 1,
  "b": true,
  "c": [
    1,
    2,
    3
  ],
  "d": {
    "a": 1,
    "b": "test"
  }
}

コード量

コード量の一覧です。コメントとかも含まれてますが。

$ paste <(wc -l src/*.nim) <(wc -l tests/*.nim)
   63 src/aggr.nim         14 tests/tflat.nim
   79 src/align.nim       208 tests/trenames.nim
   36 src/codepoint.nim    23 tests/trep.nim
   40 src/flat.nim         79 tests/tsubnet.nim
   18 src/jsonfmt.nim      10 tests/tucut.nim
  160 src/renames.nim     334 合計
   41 src/rep.nim
  148 src/subnet.nim
   85 src/tb.nim
   58 src/tiff.nim
   34 src/ucut.nim
   37 src/zshprompt.nim
  799 合計

エラーハンドリングを必要最低限しかやってないのもありますが、めちゃくちゃコード量が少ないです。
とにかく楽に実装できたことを覚えています。

楽に実装できた理由は以下の3つだと思っています

感想

Nimは書いてて楽しい!

まとめ

以下のことについて書きました。

  • 作ったツールの紹介
  • NimはCLIツールを楽に実装できる
  • Nimは書いてて楽しい

ネイティブバイナリを出力できるNimです。
作ったバイナリを誰かに配布したりできます。
小さなツールの作成からまずはNimに触れてみてほしいです。

以上です。