秒速で設定ファイルをテンプレート化するsyringeというツールを作った

# TL;DR

* インフラ系の設定をテンプレート化する際に、マクロやテンプレート系のスクリプトを書くことがある。
* しかし、マクロはコードが冗長で管理が煩雑になりがち。テンプレートエンジンを使ったスクリプトも全員が書けるわけではないので割とハードル高め。
* そこで、設定を簡単にテンプレート化できるsyringeというツールを作った。
* ダウンロードは[Releases · tanksuzuki/syringe](https://github.com/tanksuzuki/syringe/releases)からどうぞ。

# 背景

インフラの仕事をしていると、ホスト名やIP等、部分的に異なるコンフィグを大量に作成することがあります。

そんな時はマクロ(VBA)を使ってテンプレート化したりするわけですが、あまり良い方法とは言えません。そもそもVBAはコンフィグ作成に適切な言語では無いからです。
参照や数式だけで済むレベルならまだしも、ループや条件分岐でゴリゴリ文字列加工・結合し始めるとブラックボックス化が進みメンテが煩雑だったり、別件に流用しづらくなってきます。

では良い方法は何かというと、テンプレートエンジンを使う方法です。
Go言語のテンプレートエンジンを使って簡単なCiscoのコンフィグをテンプレート化すると、下記のようになります。

[Go Playground](https://play.golang.org/p/X9xsaO-TNs)

```go
package main

import (
	"fmt"
	"os"
	"text/template"
)

func main() {
	m := map[string]string{
		"hostname": "Router1",
		"ip":       "192.168.0.1",
		"mask":     "255.255.255.0",
	}

	s := `hostname {{.hostname}}
!
interface GigabitEthernet0/0
 ip address {{.ip}} {{.mask}}
!`

	t := template.New("config")
	t, err := t.Parse(s)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s", err)
		os.Exit(1)
	}
	t.Execute(os.Stdout, &m)
}
```

実行結果：

```
hostname Router1
!
interface GigabitEthernet0/0
 ip address 192.168.0.1 255.255.255.0
!
```

マクロと違って文字列処理に特化しているので、コードも割とシンプルに書けます。
…が、**あくまでマクロと比べたら**の場合です。

上記のコードで本質的な部分は、挿入する値を定義する

```
"hostname": "Router1",
"ip":       "192.168.0.1",
"mask":     "255.255.255.0",
```

と、値をどこに挿入するかを定義した

```
hostname {{.hostname}}
!
interface GigabitEthernet0/0
 ip address {{.ip}} {{.mask}}
!
```

だけです。それ以外は人間にとっては無駄。
コードを書いた経験が無い方には、ハードルでしかないです。

なので、どうでも良いコードを書かずに目的を達成できるツールを作りました。

# デモ

以下は簡単な使用例です。
Goテンプレート形式で書かれたファイルに、コマンドラインで定義した値を挿入しています。

![Demo](https://raw.githubusercontent.com/tanksuzuki/syringe/master/doc/vflag.gif)

挿入する値が多かったり、配列を使いたい場合はTOML、JSON、環境変数で定義されている値を取り込むこともできます。

# 使い方

```
$ syringe --help
Usage:
  syringe [options] <template> [<backend>...]

Application Options:
  -b, --backend=     Backend type (default: toml) [$SY_BACKEND]
      --debug        Enable debug logging [$SY_DEBUG]
      --delim-left=  Template start delimiter (default: {{) [$SY_DELIML]
      --delim-right= Template end delimiter (default: }}) [$SY_DELIMR]
  -h, --help         Show this help
  -v, --variable=    Set key/values (format key:value)
      --version      Show version information
```

`<template>`にて、値を挿入するテンプレートを指定します。

挿入する値は、`-v, --variable`フラグと`<backend>`ファイルで指定します。
`-v, --variable`フラグと`<backend>`は複数回指定が可能です。

出力までの流れは下記の通りです。

1. パイプで渡された値があれば取り込む
2. `<backend>`ファイルか、環境変数から値を取り込む
3. コマンド(`-v, --variable`フラグ)で指定された値を取り込む
4. 取り込んだ値をテンプレートに埋め込む
5. 埋め込んだ結果を標準出力に出力

※ 同じキーが存在する場合は、後に取り込んだ値で上書きされます。

```
+-------------------------+      +-----------+         +------------+
| 1. Stdin from pipe      |----->|           |         |            |
+-------------------------+      |           |         |            |
                                 |           |         |            |
+-------------------------+      |           | Insert  |   Golang   | Merged string  +----------+
| 2. <backend> or Env var |----->| Key/Value |-------->|  Template  |--------------->|  Stdout  |
+-------------------------+      |   Table   |         | <template> |                +----------+
                                 |           |         |            |
+-------------------------+      |           |         |            |
| 3. -v, --variable flag  |----->|           |         |            |
+-------------------------+      +-----------+         +------------+
```

`<backend>`で指定したファイルとパイプで渡された入力は、デフォルトでTOMLとして解釈されます。他のフォーマットを使う場合は、`-b, --backend`フラグで下記の中から指定してください。

* env
* json
* toml（デフォルト）

※ `env`を指定した場合、`<backend>`は指定できません。また、パイプでの入力は破棄されます。

# インストール

[Releases · tanksuzuki/syringe](https://github.com/tanksuzuki/syringe/releases)からダウンロードできます。
外部依存が無いので、バイナリ1つあれば使用できます。

Windows, Linux, Mac対応です。

# まとめ

まとめるとこんな感じです。

## VS 手書き（非テンプレート）

* 手で書くより早いしミスも減る
* どのパラメータを指定すべきか一目瞭然(syringeで外部ファイルを取り込む場合)

## VS マクロ

* ダラダラと長いコードを書く必要が無い
    * ブラックボックス化を防止できる
    * 運用負荷が減る（変更しやすい）
* テンプレートをgit管理しやすい

## VS テンプレートエンジン

* 各言語の環境を整えなくても実行できる（バイナリ1つあればOK）
* プログラミングの知識がなくても使える（ハードルが低い）

# 付録：逆引き簡易マニュアル

## 外部コマンドの出力結果をテンプレートに取り込む

`exec`関数を使うと、指定したコマンドの結果をテンプレートに取り込みます。

```
This configuration was generated at {{exec "date +%Y-%m-%d"}}
```

実行結果：

```
This configuration was generated at 2016-05-02
```

`exec`関数内でのパイプはサポートしていないので、`exec`から外部のスクリプトをキックするか、`sh -c`的な方法で対応してください。

## ネットワークアドレスを求める

`toNetwork`関数を使います。
第二引数は、サブネットマスク形式、プレフィックス長形式どちらも指定可能です。

```
Network address is {{toNetwork "192.168.1.1" "255.255.255.0"}}
```

or

```
Network address is {{toNetwork "192.168.1.1" "24"}}
```

実行結果：

```
Network address is 192.168.1.0
```

## サブネットマスクからプレフィックス長に変換する

`toPrefixLen`関数を使います。

```
Prefix length is {{toPrefixLen "255.255.255.0"}}
```

実行結果：

```
Prefix length is 24
```

## プレフィックス長からサブネットマスクに変換する

`toSubnetMask`関数を使います。

```
Subnet mask is {{toSubnetMask "24"}}
```

実行結果：

```
Subnet mask is 255.255.255.0
```

## テンプレート内から別のテンプレートを読み込む

`exec`関数から`syringe`を実行してください。

parent.txt

```
This is parent.txt
{{exec "syringe child.txt"}}
```

child.txt

```
This is child.txt
```

実行結果(`syringe parent.txt`)：

```
This is parent.txt
This is child.txt
```

## ループさせる

TOMLで配列(Array of tables)を停止して、テンプレートで`range`指定すればOKです。

route.toml

```
[[static]]
destination = "192.168.0.0"
mask = "255.255.255.0"
nexthop = "172.16.0.1"

[[static]]
destination = "192.168.1.0"
mask = "255.255.255.0"
nexthop = "172.16.0.1"

[[static]]
destination = "192.168.2.0"
mask = "255.255.255.0"
nexthop = "172.16.0.1"
```

template.txt

```
{{range .static}}
ip route {{.destination}} {{.mask}} {{.nexthop}}
{{end}}
```

実行結果(`syringe template.txt route.toml`)：

```
ip route 192.168.0.0 255.255.255.0 172.16.0.1
ip route 192.168.1.0 255.255.255.0 172.16.0.1
ip route 192.168.2.0 255.255.255.0 172.16.0.1
```

## コメントを書く

TOMLの場合：

```
# この行はコメントです
key = "value" # 行の途中からもコメントを書けます
```

Goテンプレートの場合：

```
{{/* この行はコメントです */}}
{{/* 複数行の
コメントも
書くことができます　*/}}
```

## 実行結果をファイルに出力する

syringeにファイル出力機能は無いため、リダイレクトで対応してください。

```
$ syringe template.txt -v foo:bar > out.txt
```