テンプレートエンジン☆ザ・サード、nなGo!?, jet 和え

がんばったのに…

顛末:

実際に自分で記述してみればわかると思うんだけど、「{{template “…” .}}」だけでもう「なげーよ」と。はっきりいって「楽したくてマクロを定義したつもりなのに、余計に記述量が多い」となりかねず、そして「データを一つしか渡せない」がために「{{template "s_prosody" (.Eval `{"rate":"-10%"}`)}}」のようなことをしなければならない、のに、ここまで読んでくれた方はわかるかと思うが「構造の定義をテンプレート内で行うことは出来ない」のだ、素の Go text/template では。ワタシが「json_loads や .Eval を追加したから初めて出来るようになったこと」なのである。そしてまたしても「なげーよ、なげーよーー」。もうね、「こんなんだったら本物の XML を直接書いたほうが楽だっ」てことになりかねん、てことで、事実今そうなりつつある。

そういうわけで、ここまで頑張って実用になる使い方を探ってきたけれど、これはもう限界。これで出来ることもそれなりにはあるけれど、どんなにお世辞を重ねようが「使いにくいという事実は揺るがない」。ので、うん、ワタシとしてはもう標準ライブラリの text/template には未練はないかな。ほかのエンジンを探そうっと…。

標準ライブラリであるということが理由で、検証してみたことそのものは価値があることである。やりたかったことみたいにテンプレートを Go 言語の外で使いたい場合は結論の通りだけれど、Go でプログラミングする際に、プログラム内部で使う程度の用途にはかなり耐えると思うしね。だから後悔はしていない。疲れたけどよ。

そういうわけで、ほかのサードパーティ巡り開始である。おーさんごーによる列挙からいくつか候補が見つかる:

  • fasttemplate – Simple and fast template engine. Substitutes template placeholders up to 10x faster than text/template.
  • hero – Hero is a handy, fast and powerful go template engine.
  • jet – Jet template engine.
  • pongo2 – Django-like template-engine for Go.
  • quicktemplate – Fast, powerful, yet easy to use template engine. Converts templates into Go code and then compiles it.

やたらめったら速かぞ、と主張するエンジンがいくつかあるのだが、ひとまず「Converts templates into Go code and then compiles it.」に警戒しておきたい。quicktemplate のこの説明に近いことをほかのものでも見た気がしてる。そしてこのセンテンスを字句通りに理解するならば、ワタシに必要な要件「ランタイム依存しない」にフィットしない。ので、それっぽい(速さをうたうやつ)のを除外すると、まずは jet と pongo2 だけなのかな、って気がした。うん、ひとまずそいつらからよねと。

pongo2 は、python プロジェクトである Django の「Syntax- and feature-set-compatible with Django 1.7」といっていて、まぁ魅力的ろうとは思うんだけれど、その「Syntax- and feature-set-compatible with Django 1.7」部分のドキュメントが完全に Django ドキュメントに丸投げになっててちょっと心象悪くなった。いいんだけどさ、pongo2 は django そのものではないんだから、ドキュメントはちゃんと書いてほしかったかなぁ。(こういうことされると pongo2 にインライン python 記述が出来ると思いかねないが、きっとそれは出来なかろう、みたいなことよ、たとえば。)

pongo2 の印象が少し悪かったから、というだけではなくて、jet の「テンプレートの仕様についてのドキュメント」はちゃんとしてて、それだけで標準ライブラリ text/template へ抱いた不満はなさそうに感じている。どうかな?

jet についての最初の問題は、テンプレートの仕様についてのドキュメントがしっかりわかりやすく書かれているのに、なぜか Go API の説明は(標準ライブラリと同罪の非常に理解しにくい)リファレンスのみだと言うこと。これはちょっと苦労するかもしれん、という予感がする。

text/template は「汎用のテンプレート処理コマンドラインツール」が欲しいなら自分で書く必要があったわけだが、これはそれをする必要がない、とかってことはないのかな、と思ったりもした。ぱっとみではそういう出来合いのはなさそうなので、まぁ text/template でやったのと同じノリでやってみますか、と。ただ、「text/template でやったのと同じノリで」不足機能を補う、ということが必要になりそうなのかが今時点であんまり見えてなくて、ほんとにほとんど何もしないものになるかも。つまり「template のロード、n Go!」だけの数行で終わるやつね。さすがに変数の取り込みなどがあるので、ロード・実行、だけで終わるてことにはならんはずだけど、ワタシのブログのネタとしておもろいんかなそれ、みたいなね、そうならんかちょっと心配。

うん、まぁ四の五の言わずに。はじめてみよう。

まず。Go API のわかりやすい説明はないに等しいが、かろうじて Examples から飛ばされて jettest/test.goloaders/multi_test.go を参考にすれば、基礎的な初版が作れそうだ、から始まる〇〇生活。

うーん、ちょいと悶絶しながら出来てみた初版:

tmpljetexpr01.go (2022-02-11追記: Execute の戻りをみてない。ポカミス。無念。)
 1 package main
 2 
 3 import (
 4     "os"
 5     "github.com/CloudyKit/jet/v6"
 6 )
 7 
 8 func main() {
 9     l := jet.NewOSFileSystemLoader(".")
10     tmplfn := os.Args[1]
11     _, err := l.Open(tmplfn)
12     if err != nil {
13         panic(err)
14     }
15     set := jet.NewSet(l)
16     tmpl, err := set.GetTemplate(tmplfn)
17     if err != nil {
18         panic(err)
19     }
20     tmpl.Execute(os.Stdout, /*variable*/nil, /*context*/nil)
21 }

「Set」という名前から「いっぱいのてんぷれーとかき集め野郎」なのかと思ったら違った。あどばんすどな追加機能ではなくて、これは「玄関」。わかりにくい…。ソースコードのコメントに書いてある:

github.com/!cloudy!kit/jet@v2.1.2+incompatible/template.go
32 // Set is responsible to load,invoke parse and cache templates and relations
33 // every jet template is associated with one set.
34 // create a set with jet.NewSet(escapeeFn) returns a pointer to the Set
35 type Set struct {
36 	loader            Loader
37 	templates         map[string]*Template // parsed templates
38 	escapee           SafeWriter           // escapee to use at runtime
39 	globals           VarMap               // global scope for this template set
40 	tmx               *sync.RWMutex        // template parsing mutex
41 	gmx               *sync.RWMutex        // global variables map mutex
42 	defaultExtensions []string
43 	developmentMode   bool
44 }

ので、ほかのエンジンに慣れていると不思議なこの Set を経るセットアップは、どうやら不可避というか必須っぽく思える。まぁ慣れだ慣れ、とは思うけれど…、だからこそドキュメントをちゃんと書いて欲しいんだよなぁ。だって、ここに至ってもまだ「たぶん」とか「思える」としか言えないんだもん…。

データを何も与えられない例なのでなんのウマみも感じられないけれど一応動かせばこう:

tmpljet01.jet
1 Hi, {{.}}!
1 [me@host: tmpljetexpr01]$ ./tmpljetexpr01.exe tmpljet01.jet
2 Hi, !

ここから先の最初の流れは標準ライブラリの text/template でやったのと同じね。まずはデータをテンプレートに流し込めねーとそりゃテンプレートエンジンの使用としては両手両足を失っているようなもの、出来ることは随分限られてしまう、てこと。ここはかなり標準ライブラリの text/template でやったことをそのまま活用出来るね:

tmpljetexpr02.go (2022-02-11追記: Execute の戻りをみてない。ポカミス。無念。)
 1 package main
 2 
 3 import (
 4     "os"
 5     "path/filepath"
 6     "io/ioutil"
 7     "encoding/json"
 8     "github.com/ogier/pflag"
 9     mxj "github.com/clbanning/mxj/v2"
10     "github.com/CloudyKit/jet/v6"
11 )
12 
13 func json_loads(jsoncont string) interface{} {
14     var i interface{}
15     json.Unmarshal(([]byte)(jsoncont), &i)
16     return i
17 }
18 
19 func json_load(fn string) interface{} {
20     jsoncont, err := ioutil.ReadFile(fn)
21     if err != nil { panic(err) }
22     return json_loads(string(jsoncont))
23 }
24 
25 func xml_loads(xmlcont string) interface{} {
26     i, err := mxj.NewMapXml([]byte(xmlcont))
27     if err != nil { panic(err) }
28     return i
29 }
30 
31 func xml_load(fn string) interface{} {
32     xmlcont, err := ioutil.ReadFile(fn)
33     if err != nil { panic(err) }
34     return xml_loads(string(xmlcont))
35 }
36 
37 func main() {
38     ousage := pflag.Usage
39     pflag.Usage = func() {
40         ousage()
41         os.Exit(1)
42     }
43     pflag.Parse()
44 
45     l := jet.NewOSFileSystemLoader(".")
46     tmplfn := pflag.Arg(0)
47     _, err := l.Open(tmplfn)
48     if err != nil {
49         panic(err)
50     }
51     set := jet.NewSet(l)
52     tmpl, err := set.GetTemplate(tmplfn)
53     if err != nil {
54         panic(err)
55     }
56     var context interface{}
57     if len(pflag.Args()) > 1 {
58         if filepath.Ext(pflag.Arg(1)) == ".xml" {
59             context = xml_load(pflag.Arg(1))
60         } else {
61             context = json_load(pflag.Arg(1))
62         }
63     }
64     tmpl.Execute(os.Stdout, /*variable*/nil, context/*nil*/)
65 }

コマンドラインオプションはまだ何も考えてないがひとまずは「コピペ」の都合で pflag には予め依存しとく。どうせやりたくなるろうし…、てことよりも、「variableを追加する」んだと思ってたら、text/template でやってたのに直接対応するのは context なのかもしんない、そうなのか、てのはちょっとまだ悶々としてる。つまりこれ「jet.VarMap」の御し方がわからず「interface{}」なのでてことでやってみたら意図したものだった、てこと。うん…、variable と context の「両方について考える」のはひとまず保留にして、これで動かすと:

tmpljet01.jet (再掲)
1 Hi, {{.}}!
data4tmpljet01.json
1 [1, 2, 4]
data4tmpljet01.xml
1 <data>
2   <li>1</li>
3   <li>3</li>
4   <li>5</li>
5 </data>
1 [me@host: tmpljetexpr02]$ ./tmpljetexpr02.exe tmpljet01.jet data4tmpljet01.json
2 Hi, [1 2 4]!
3 [me@host: tmpljetexpr02]$ ./tmpljetexpr02.exe tmpljet01.jet data4tmpljet01.xml
4 Hi, map[data:map[li:[1 3 5]]]!

うん、ひとまずは。一応これで「context は駆使するテンプレート記述そのもの」で遊び始められる。たとえば:

tmpljet01-2.jet
1 {{.Greeting}}, {{.Name}}!
data4tmpljet01-2.json
1 {
2     "Greeting": "こんばちは",
3     "Name": "新世界(大阪)"
4 }
1 [me@host: tmpljetexpr02]$ ./tmpljetexpr02.exe tmpljet01-2.jet data4tmpljet01-2.json
2 こんばちは, 新世界(大阪)!

うん、いいね。

次は「variable と context の「両方について考える」」か、あるいはテンプレートの仕様についての学習に突入すべきか、どっちにすっかな…? うーん、最低でもこの2つのコンセプトについての理解はしてから進めたいよなぁ…、そうするか?

variable を使っている実例は eval_test.go の中に見つけた(たとえばここ)。うん、何をすればいいかはわかる、けれど、context との使い分けはかえってわからなくなった。つまり、上の例はそのままこうも出来るということなの:

tmpljetexpr02_2.go (2022-02-11追記: Execute の戻りをみてない。ポカミス。無念。)
 1 package main
 2 
 3 import (
 4     "os"
 5     "path/filepath"
 6     "io/ioutil"
 7     "encoding/json"
 8     "github.com/ogier/pflag"
 9     mxj "github.com/clbanning/mxj/v2"
10     "github.com/CloudyKit/jet/v6"
11 )
12 
13 func json_loads(jsoncont string) interface{} {
14     var i interface{}
15     json.Unmarshal(([]byte)(jsoncont), &i)
16     return i
17 }
18 
19 func json_load(fn string) interface{} {
20     jsoncont, err := ioutil.ReadFile(fn)
21     if err != nil { panic(err) }
22     return json_loads(string(jsoncont))
23 }
24 
25 func xml_loads(xmlcont string) interface{} {
26     i, err := mxj.NewMapXml([]byte(xmlcont))
27     if err != nil { panic(err) }
28     return i
29 }
30 
31 func xml_load(fn string) interface{} {
32     xmlcont, err := ioutil.ReadFile(fn)
33     if err != nil { panic(err) }
34     return xml_loads(string(xmlcont))
35 }
36 
37 func main() {
38     ousage := pflag.Usage
39     pflag.Usage = func() {
40         ousage()
41         os.Exit(1)
42     }
43     pflag.Parse()
44 
45     l := jet.NewOSFileSystemLoader(".")
46     tmplfn := pflag.Arg(0)
47     _, err := l.Open(tmplfn)
48     if err != nil {
49         panic(err)
50     }
51     set := jet.NewSet(l)
52     tmpl, err := set.GetTemplate(tmplfn)
53     if err != nil {
54         panic(err)
55     }
56     var variable = make(jet.VarMap)
57     if len(pflag.Args()) > 1 {
58         if filepath.Ext(pflag.Arg(1)) == ".xml" {
59             variable.Set("data", xml_load(pflag.Arg(1)))
60         } else {
61             variable.Set("data", json_load(pflag.Arg(1)))
62         }
63     }
64     tmpl.Execute(os.Stdout, variable/*nil*/, /*context*/nil)
65 }

階層が一つ下るのでテンプレートでの使用は少し変わる、当然:

tmpljet01-2-2.jet
1 {{data.Greeting}}, {{data.Name}}!

実行例は同じなので省略。

この VarMap に関数とかもぶちこめる、てのは text/template でやったのと同じノリ。ゆえ、「text/template でやったのと同じノリのものとしての完成品」としてはこちらの VarMap だけ使って実現出来る、てこと。だったら context はどう使ってやろうか、と。むむむ…、使い分けは自由に考えなはれ、ちぅことなのだろうか、それとも何か前提となるユースケースがあるのだろうか? それとも単に Map での階層化を伴うかグローバルか、てだけかなぁ? あまり真剣に考えたくないなら素直に「どっちもコマンドライン指定でぶっ込める」がいいかしらね?:

tmpljetexpr03.go (2022-02-11追記: Execute の戻りをみてない。ポカミス。無念。)
 1 package main
 2 
 3 import (
 4     "os"
 5     "path/filepath"
 6     "io/ioutil"
 7     "encoding/json"
 8     "github.com/ogier/pflag"
 9     mxj "github.com/clbanning/mxj/v2"
10     "github.com/CloudyKit/jet/v6"
11 )
12 
13 func json_loads(jsoncont string) interface{} {
14     var i interface{}
15     json.Unmarshal(([]byte)(jsoncont), &i)
16     return i
17 }
18 
19 func json_load(fn string) interface{} {
20     jsoncont, err := ioutil.ReadFile(fn)
21     if err != nil { panic(err) }
22     return json_loads(string(jsoncont))
23 }
24 
25 func xml_loads(xmlcont string) interface{} {
26     i, err := mxj.NewMapXml([]byte(xmlcont))
27     if err != nil { panic(err) }
28     return i
29 }
30 
31 func xml_load(fn string) interface{} {
32     xmlcont, err := ioutil.ReadFile(fn)
33     if err != nil { panic(err) }
34     return xml_loads(string(xmlcont))
35 }
36 
37 func main() {
38     var (
39         extravars = pflag.StringP(
40             "extravars", "v", "", "specify json or xml for the variables")
41     )
42     ousage := pflag.Usage
43     pflag.Usage = func() {
44         ousage()
45         os.Exit(1)
46     }
47     pflag.Parse()
48 
49     l := jet.NewOSFileSystemLoader(".")
50     tmplfn := pflag.Arg(0)
51     _, err := l.Open(tmplfn)
52     if err != nil {
53         panic(err)
54     }
55     set := jet.NewSet(l)
56     tmpl, err := set.GetTemplate(tmplfn)
57     if err != nil {
58         panic(err)
59     }
60     from_fn := func(fn string) interface{} {
61         if filepath.Ext(fn) == ".xml" {
62             return xml_load(fn)
63         } else {
64             return json_load(fn)
65         }
66     }
67     var context interface{}
68     if len(pflag.Args()) > 1 {
69         context = from_fn(pflag.Arg(1))
70     }
71     var variable = make(jet.VarMap)
72     if *extravars != "" {
73         outer := from_fn(*extravars)
74         for k, v := range outer.(map[string]interface{}) {
75             variable.Set(k, v)
76         }
77     }
78     tmpl.Execute(os.Stdout, variable, context)
79 }

ここでの発想としては、context は json, xml からロードしたトップレベルを使い、variables は「Map であることを前提に」階層をひとつ展開する、てことね。伝わるかしら。「data」という名前を固定するのがイヤだ、ていう言い方でも伝わる? ともあれ:

data4tmpljet01-3.json
1 {
2     "data": {
3         "Greeting": "こんばちは",
4         "Name": "新世界(大阪)"
5     }
6 }
1 [me@host: tmpljetexpr03]$ # context のほうに突っ込む例
2 [me@host: tmpljetexpr03]$ ./tmpljetexpr03.exe tmpljet01-2.jet data4tmpljet01-2.json
3 こんばちは, 新世界(大阪)!
4 [me@host: tmpljetexpr03]$ # variable のほうに突っ込む例
5 [me@host: tmpljetexpr03]$ ./tmpljetexpr03.exe tmpljet01-2-2.jet --extravars=data4tmpljet01-3.json
6 こんばちは, 新世界(大阪)!

まぁこれならあんまし難しいこと考えずに済むわな。

とりあえず必要に思うかどうかはまだわからんけど、出来る「ので」てことで json_load[s]/xml_load[s] をテンプレート側に公開しておく:

tmpljetexpr04.go (2022-02-11追記: Execute の戻りをみてない。ポカミス。無念。)
 1 package main
 2 
 3 import (
 4     "os"
 5     "path/filepath"
 6     "io/ioutil"
 7     "encoding/json"
 8     "github.com/ogier/pflag"
 9     mxj "github.com/clbanning/mxj/v2"
10     "github.com/CloudyKit/jet/v6"
11 )
12 
13 func json_loads(jsoncont string) interface{} {
14     var i interface{}
15     json.Unmarshal(([]byte)(jsoncont), &i)
16     return i
17 }
18 
19 func json_load(fn string) interface{} {
20     jsoncont, err := ioutil.ReadFile(fn)
21     if err != nil { panic(err) }
22     return json_loads(string(jsoncont))
23 }
24 
25 func xml_loads(xmlcont string) interface{} {
26     i, err := mxj.NewMapXml([]byte(xmlcont))
27     if err != nil { panic(err) }
28     return i
29 }
30 
31 func xml_load(fn string) interface{} {
32     xmlcont, err := ioutil.ReadFile(fn)
33     if err != nil { panic(err) }
34     return xml_loads(string(xmlcont))
35 }
36 
37 func main() {
38     var (
39         extravars = pflag.StringP(
40             "extravars", "v", "", "specify json or xml for the variables")
41     )
42     ousage := pflag.Usage
43     pflag.Usage = func() {
44         ousage()
45         os.Exit(1)
46     }
47     pflag.Parse()
48 
49     l := jet.NewOSFileSystemLoader(".")
50     tmplfn := pflag.Arg(0)
51     _, err := l.Open(tmplfn)
52     if err != nil {
53         panic(err)
54     }
55     set := jet.NewSet(l)
56     tmpl, err := set.GetTemplate(tmplfn)
57     if err != nil {
58         panic(err)
59     }
60     from_fn := func(fn string) interface{} {
61         if filepath.Ext(fn) == ".xml" {
62             return xml_load(fn)
63         } else {
64             return json_load(fn)
65         }
66     }
67     var context interface{}
68     if len(pflag.Args()) > 1 {
69         context = from_fn(pflag.Arg(1))
70     }
71     var variable = make(jet.VarMap)
72     variable.Set("json_loads", json_loads)
73     variable.Set("json_load", json_load)
74     variable.Set("xml_loads", xml_loads)
75     variable.Set("xml_load", xml_load)
76     if *extravars != "" {
77         outer := from_fn(*extravars)
78         for k, v := range outer.(map[string]interface{}) {
79             variable.Set(k, v)
80         }
81     }
82     tmpl.Execute(os.Stdout, variable, context)
83 }
tmpljet02.jet
1 {{- d := json_load("data4tmpljet01-3.json") -}}
2 {{d.data.Name}} - {{d.data.Greeting}}
1 [me@host: tmpljetexpr04]$ ./tmpljetexpr04.exe tmpljet02.jet
2 新世界(大阪) - こんばちは

まぁ text/template でやったのと同じ話なので、とりたてて興奮するもんでもなくて、ゼロカロリーである。

Go プログラムは少しこのままにしておいて、テンプレートの仕様について学んでおく。といっても text/template の時と違って、ドキュメントはカッチリきっちり書かれてるし、(pongo2と違って)ベース部分はどうやら text/template に似せてあるようで、スムーズに理解出来る。まぁ既にさらっと例示しちゃってるんでお察しかもしれんけどね。

設定で「{}」を使うか「[]」を使うか選択可能だ、と言っているけれど、その切り替えについては後回しにして、まずは前者固定として。

2022-02-19追記: デリミタを変えることが出来る、について、相変わらずドキュメントが非常に追いかけにくいのだが、jet.NewSet の第二引数(以降)で jet.WithDelims の戻りを与えることで変更出来る…のだが、本日時点で go get でお取り寄せ可能な公式バージョンは、コメントが使えなくなるバグ(#187)があって実用にならないので注意。

空白除去は text/template と同じだねコメントは text/template よりスッキリしてる:

1 {* this is a comment *}
2 {*
3     none of this will be executed:
4     {{ asd }}
5     {{ include "./foo.jet" }}
6 *}

VariablesExpressions はね、もう以下だけで text/template で不満だったのがきっとないんだろうと即座にわかるろ?:

1 {{ s := "helloworld" }}
2 {{ s[1] }}
1 {{ s := slice("foo", "bar", "asd") }}
2 {{ s[0] }}
3 {{ i := 2 }}
4 {{ s[i] }}
1 {{ m := map("foo", 123, "bar", 456) }}
2 {{ m["foo"] }}
3 {{ bar := "bar" }}
4 {{ m[bar] }}

つまり「普通だ」てことだよ。至極直感的に記述出来る。text/template の「惜しい」がやりきってある、てこと。もうなんかこれだけでも乗り換えた甲斐があった、とすら思うよね。

あと、きっともう気付いてると思うけど、text/template だけでやろうとしてたやつでは goval で実現しようとした「計算とか」は機能として組み込まれてる:

1 {{ (1 + 2) * 3 - 4.1 }}
2 {{ "HELLO" + " " + "WORLD!" }}

万事が万事、text/template で「こうだったら良かったのに…」が「そう」なってる、つまり、用語やコンセプト名から想像出来る通りの「書き方で」書けばそれが受け入れられる、と思えばほとんど合ってると思う。text/template 用語の「pipelines」は「どこがパイプラインやねん」だったのが「ちゃんとパイプライン」だし、Control structures や Expressions も「高級言語のソレ」のノリそのまんまで考えればそれがきっと正しい:

text/template では「そんな書き方知らんわ」と怒られる「素直な」記述
1 {{ item == true || !item2 && item3 != "test" }}

ので、そうした「text/template コンセプトをちゃんと完遂しただけ」と思われる機能については、「ドキュメント読めばわかーる」で良いであろう。

ここからは、text/template では「惜しい」ですらなかったダメ機能が jet でどうなってるか、だ。つまりメインエベントだわな。

全然ダメだった、のひとつ目は既に書いた。「テンプレート内で変数定義する際、構造を作れない(参照は出来るくせに)」については、「map」の形で書けるので、json_loads とかを使わなくていい、てこと。これだけでも存分に乗り換えてよかったね、て思うよね、と言った。

そして、「ほか」といっても TemplatesBlocks だけなのだから、これも「読めばわかーる」と言ってしまえばそうなんだけれど、text/template に見切りをつけた決定打となったのこそがこれなので、「text/template で書こうとしてちっとも楽できないと感じた実例」をば:

base.jet
 1 {*- most outer container -*}
 2 {{- block speak(lang="ja-JP") -}}
 3 <speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='{{lang}}'>
 4 {{- yield content -}}
 5 </speak>
 6 {{- end -}}
 7 
 8 {*- voice element -*}
 9 {{- block voice(voice="ja-JP, KeitaNeural") -}}
10 <voice name='Microsoft Server Speech Text to Speech Voice ({{voice}})'>
11 {{- yield content -}}
12 </voice>
13 {{- end -}}
14 
15 {*- prosody element -*}
16 {{- block prosody(pitch="+0Hz", rate="+0%") -}}
17 <prosody pitch="{{pitch}}" rate="{{rate}}">
18 {{- yield content -}}
19 </prosody>
20 {{- end -}}
21 
22 {*- break element -*}
23 {{- block break(time="1s") -}}
24 <break time="{{time}}"/>
25 {{- end -}}
part0.jet
1 {{ import "./base.jet" }}
2 {{ yield speak() content }}
3 {{ yield voice() content }}
4 <p>
5 <s>こんばんは。またお会いできましたね。{{yield break(time="4s")}}</s>
6 <s>{{ yield prosody(rate="-20%", pitch="+110Hz") content }}どうしました?{{ end }}</s>
7 </p>
8 {{ end }}
9 {{ end }}
レンダリング結果(SSML)
1 <speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='ja-JP'>
2 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)'>
3 <p>
4 <s>こんばんは。またお会いできましたね。<break time="4s"/></s>
5 <s><prosody pitch="+110Hz" rate="-20%">どうしました?</prosody></s>
6 </p>
7 </voice>
8 </speak>

うん、めっさ普通。この「普通」がいかに大事で大変なことなのか、てことよな。

微妙に「記述が長い」が解決してないけれど、少なくとも「記述が長過ぎる」ではなくなったし、不自然な考え方もしなくて良くなってマクロ(という言い方はしてないみたいだけど)を臆せずガンガン作れるようになってる、てなわけで、何億倍かは気分が良い。だいたいにして「めっさ読みやすい、わかりやすい」よね。

ちなみにこの SSML を Microsoft Edge TTS サーバに投げて音声に出来る:

Microsoft Edge TTS の Python クライアントが edge-tts。pip でインストール出来るよ。
1 [me@host: ~]$ py -3 -m edge_tts -f 上で作ったSSML.xml -z > part0.mp3

あとはこの CLI ツールの進化をどうしていくか、なのだが、最低でも「テンプレートパス」の扱いと複数ファイルの処理について綺麗に整理すべきなんだろう。が、ひとまずはここまでのものでもかなり実用になるので、その方面のは後回しでいいか、と。

正直 jet で満足しそうな予感がしていたんだけれど、ちょっと冗長性がやはり気になるのと、あとね、ひとつどうしてもやりたいのに出来ていないことがある。こういうことがしたかったの:

たとえば「nl_remove」という機能を自分で追加した想定で。でもこれは出来ない。
 1 {{- block speak(lang="ja-JP") -}}
 2 <speak version='1.0'
 3        xmlns='http://www.w3.org/2001/10/synthesis'
 4        xmlns:mstts='https://www.w3.org/2001/mstts'
 5        xml:lang='{{lang}}'>
 6 {{- yield content | nl_remove -}}
 7 </speak>
 8 {{- end -}}
 9 
10 {* 呼び出す側でやろうとやっぱりダメなものはダメ *}
11 {{ yield speak() content | nl_remove }}
12 {{- end -}}

これは Microsoft Edge’s TTS Server 相手の SSML 記述。何をしたいかというと。要素として「パラグラフ」「センテンス」について、SSML 記述でその区切りを指定出来るわけだけれど、「ワード」の区切りはこれは「英語とかなら自明、日本語だと不明瞭不可解奇っ怪奇々怪々」でしょ? わかるよね。だから、「パラグラフ」「センテンス」の内側に入る「テキスト」に改行が入るか入らないかは、これは英語ならまず支障ないけれど、日本語だと読み方が変わりうるわけ。でもだからといって「SSML 記述の元になるテキスト」を無改行で書き連ねるのは読むのも書くのもツラい、だからテンプレートの機能で改行取っ払いが欲しいぞ、と、まぁそういうわけ。だけど上の書き方は(たとえ「nl_remove」が実在していたとしても)出来ない。

思うに「yield」と「content」のスペシャル扱い過ぎが原因なんじゃなかろうかと思う。構文のパースがこの仕様ではシンプルになるのは理解は出来るんだよなぁ、気持ちはわかるけれども、て感じ。

ひとまず次善策として「ポストプロセス」的な思想の、というよりは「乱暴な変換」だけでも組み込んでみた:

(2022-02-11追記: Execute の戻りをみてない。ポカミス。無念。)
 1 package main
 2 
 3 import (
 4     "os"
 5     "bytes"
 6     "regexp"
 7     "path/filepath"
 8     "io/ioutil"
 9     "encoding/json"
10     "github.com/ogier/pflag"
11     mxj "github.com/clbanning/mxj/v2"
12     "github.com/CloudyKit/jet/v6"
13 )
14 
15 func json_loads(jsoncont string) interface{} {
16     var i interface{}
17     json.Unmarshal(([]byte)(jsoncont), &i)
18     return i
19 }
20 
21 func json_load(fn string) interface{} {
22     jsoncont, err := ioutil.ReadFile(fn)
23     if err != nil { panic(err) }
24     return json_loads(string(jsoncont))
25 }
26 
27 func xml_loads(xmlcont string) interface{} {
28     i, err := mxj.NewMapXml([]byte(xmlcont))
29     if err != nil { panic(err) }
30     return i
31 }
32 
33 func xml_load(fn string) interface{} {
34     xmlcont, err := ioutil.ReadFile(fn)
35     if err != nil { panic(err) }
36     return xml_loads(string(xmlcont))
37 }
38 
39 func main() {
40     var (
41         extravars = pflag.StringP(
42             "extravars", "v", "", "specify json or xml for the variables")
43         subnl = pflag.StringP(
44             "replace_rendered_newline", "n", "\n", "replace newline to this")
45     )
46     ousage := pflag.Usage
47     pflag.Usage = func() {
48         ousage()
49         os.Exit(1)
50     }
51     pflag.Parse()
52 
53     l := jet.NewOSFileSystemLoader(".")
54     tmplfn := pflag.Arg(0)
55     _, err := l.Open(tmplfn)
56     if err != nil {
57         panic(err)
58     }
59     set := jet.NewSet(l)
60     tmpl, err := set.GetTemplate(tmplfn)
61     if err != nil {
62         panic(err)
63     }
64     from_fn := func(fn string) interface{} {
65         if filepath.Ext(fn) == ".xml" {
66             return xml_load(fn)
67         } else {
68             return json_load(fn)
69         }
70     }
71     var context interface{}
72     if len(pflag.Args()) > 1 {
73         context = from_fn(pflag.Arg(1))
74     }
75     var variable = make(jet.VarMap)
76     variable.Set("json_loads", json_loads)
77     variable.Set("json_load", json_load)
78     variable.Set("xml_loads", xml_loads)
79     variable.Set("xml_load", xml_load)
80     if *extravars != "" {
81         outer := from_fn(*extravars)
82         for k, v := range outer.(map[string]interface{}) {
83             variable.Set(k, v)
84         }
85     }
86     nl_remove := func (cont string) string {
87         return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *subnl)
88     }
89     writer := bytes.NewBufferString("")
90     tmpl.Execute(writer, variable, context)
91     os.Stdout.Write([]byte(nl_remove(writer.String())))
92 }

問答無用で改行を置き換える、というものだが、これは「汎用」を考えるならダメなんだけれど、相手が xml (や html など SGML 系)なら問題ないケースが結構多いだろうなと思う。CDATA を駆使してるとダメだけれど、そうでないなら。ただ、これはあとで少しはちゃんとしたいかなぁ。「改行を置き換える」という決め打ちでないのがいいよね、同じ乱暴なんだとしてもね。(xml の場合は例えば「オリジナルの改行を取り除きつつ 「>」 の前に改行を入れる」という(バッド)ノウハウがあるでしょ、そういうの。)

2022-02-11追記:
話としてはふたつ。

ひとつめは「やってて気付いてなくて、あとになってちょっと悶絶した」話。「block」なんだけどね、これ、Go 標準の text/template と同じなんだよね、以下:

つい最初の「block」は「マクロを定義しているだけ」と思いがちだがそうではない、の巻
1 {{block copyright()}}
2     <div>© HOGEME, Inc. 2032</div>
3 {{end}}
4 <footer>
5     {{yield copyright()}}
6 </footer>

これを実行すれば、「素直に」こうなる:

望みの結果ではないわな
1     <div>© HOGEME, Inc. 2032</div>
2 
3 <footer>
4     
5     <div>© HOGEME, Inc. 2032</div>
6 
7 </footer>

ちゃんと説明されてる:

Defining a block in a template that’s being executed will also invoke it immediately. To avoid this, use import or extends.

なんでずっと気付かなかったかって、もちろん import 元でしか block を書いてなかったから。そしてなんで今さら気付いたかって、続くふたつ目の話のための「実験」で簡単に試そうとして block と yield を同じテンプレートファイルに書いちゃったため。なんで二回出るんだぁ、と、10分以上頭掻きむしってたわよ。

ふたつめの話は、ワタシの節穴に関係してて、最後に話が覆るのだが、まぁノンビリ読み進めてくれ、そして笑うがいい。

Python を例にするとキーワードの関係でちょっとややこしいところもあるんだけれど、要はこれ:

1 for i in range(5):
2     print(i)

Go の「range」が Python などで言うところの「for」なのでめっちゃややこしいのだが、欲しいのはこの「Python で言うところの range」に相当する機能である。numpy の arange や linspace のようなもの、と言ってもいい。つまり、回数指定等でシーケンスを生成する機能が欲しい。けれども、どうにもこういう機能が見当たらない。うーん、ないのか?

これで実現したかったのはやはり SSML に関係している。Microsoft Edge TTS Server のエンジンが「break」(読み上げの休止機能)の実装でやらかしちゃってるんだよね。説明は「This value should be set less than 5,000 ms」だ、だから5秒以上を指定したお前が悪い、ということはわかる。けれども「黙って切り捨てる」は一番やっちゃいけない。ライターの指示を勝手に無視して間違ったまま「正しい」フリをして動き続けてしまうと、あとになって気付いた際には影響が大き過ぎて簡単には直せなくなる、なんてことが起こる。そりゃそうであろう。テストしながら書いているのであれば、その間違った結果を一度以上は受け入れてしまっていたのだから。そのテストを全てやり直すハメになる、てことだ。ともあれ、そういうことなので、たとえば「30秒の break」を実現したければ、5秒指定の break を6回呼び出す必要があるわけだ、なので、テンプレートの block として「30秒を引数に与えると5秒指定の break を6回呼び出す」ようなものを書きたい、と。

そして「「Python で言うところの range」に相当する機能がないので」書けない、のか? てハナシ。slice と repeat で誤魔化せないかなとも思ったが、どうもうまくいかなかった。のでもうこれは、追加するしかないかなと:

ここまでやってきてたの、最後の Execute のエラー処理が抜けてたのでその追加もしてる。
  1 package main
  2 
  3 import (
  4     "os"
  5     "bytes"
  6     "regexp"
  7     "path/filepath"
  8     "io/ioutil"
  9     "encoding/json"
 10     "github.com/ogier/pflag"
 11     mxj "github.com/clbanning/mxj/v2"
 12     "github.com/CloudyKit/jet/v6"
 13 )
 14 
 15 func json_loads(jsoncont string) interface{} {
 16     var i interface{}
 17     json.Unmarshal(([]byte)(jsoncont), &i)
 18     return i
 19 }
 20 
 21 func json_load(fn string) interface{} {
 22     jsoncont, err := ioutil.ReadFile(fn)
 23     if err != nil { panic(err) }
 24     return json_loads(string(jsoncont))
 25 }
 26 
 27 func xml_loads(xmlcont string) interface{} {
 28     i, err := mxj.NewMapXml([]byte(xmlcont))
 29     if err != nil { panic(err) }
 30     return i
 31 }
 32 
 33 func xml_load(fn string) interface{} {
 34     xmlcont, err := ioutil.ReadFile(fn)
 35     if err != nil { panic(err) }
 36     return xml_loads(string(xmlcont))
 37 }
 38 
 39 func make_seq(sss ...float64) []float64 {
 40     sta := 0.0
 41     ste := 1.0
 42     sto := 1.0
 43     if len(sss) == 1 {
 44         sto = sss[0]
 45     } else if len(sss) == 2 {
 46         sta = sss[0]
 47         sto = sss[1]
 48     } else if len(sss) == 3 {
 49         sta = sss[0]
 50         sto = sss[1]
 51         ste = sss[2]
 52         if ste == 0.0 {
 53             panic("`step` must not be zero")
 54         }
 55     } else {
 56         panic("require arguments, at least one for `stop`")
 57     }
 58     var res []float64
 59     for {
 60         if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
 61             break
 62         }
 63         res = append(res, sta)
 64         sta += ste
 65     }
 66     return res
 67 }
 68 
 69 func main() {
 70     var (
 71         extravars = pflag.StringP(
 72             "extravars", "v", "", "specify json or xml for the variables")
 73         subnl = pflag.StringP(
 74             "replace_rendered_newline", "n", "\n", "replace newline to this")
 75     )
 76     ousage := pflag.Usage
 77     pflag.Usage = func() {
 78         ousage()
 79         os.Exit(1)
 80     }
 81     pflag.Parse()
 82 
 83     l := jet.NewOSFileSystemLoader(".")
 84     tmplfn := pflag.Arg(0)
 85     _, err := l.Open(tmplfn)
 86     if err != nil {
 87         panic(err)
 88     }
 89     set := jet.NewSet(l)
 90     tmpl, err := set.GetTemplate(tmplfn)
 91     if err != nil {
 92         panic(err)
 93     }
 94     from_fn := func(fn string) interface{} {
 95         if filepath.Ext(fn) == ".xml" {
 96             return xml_load(fn)
 97         } else {
 98             return json_load(fn)
 99         }
100     }
101     var context interface{}
102     if len(pflag.Args()) > 1 {
103         context = from_fn(pflag.Arg(1))
104     }
105     var variable = make(jet.VarMap)
106     variable.Set("json_loads", json_loads)
107     variable.Set("json_load", json_load)
108     variable.Set("xml_loads", xml_loads)
109     variable.Set("xml_load", xml_load)
110     variable.Set("make_seq", make_seq)
111     if *extravars != "" {
112         outer := from_fn(*extravars)
113         for k, v := range outer.(map[string]interface{}) {
114             variable.Set(k, v)
115         }
116     }
117     nl_remove := func (cont string) string {
118         return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *subnl)
119     }
120     writer := bytes.NewBufferString("")
121     err = tmpl.Execute(writer, variable, context)
122     if err != nil {
123         panic(err)
124     }
125     os.Stdout.Write([]byte(nl_remove(writer.String())))
126 }

これによりめでたく:

base.jet
1 {{ block break_s(sec=5) }}
2 {{- range make_seq(sec / 5) }}
3 {{- nxt := ((. + 1) * 5) }}
4 {{- if nxt > sec }}{{ nxt = sec }}{{ end -}}
5 {{- t := nxt - (. * 5) -}}
6 <break time="{{t}}s"/>
7 {{ end -}}
8 {{ end }}
voice.jet
1 {{- import "base.jet" -}}
2 {{ yield break_s(sec=6) }}
3 
4 {{ yield break_s(sec=13) }}
実行結果
1 <break time="5s"/>
2 <break time="1s"/>
3 
4 
5 <break time="5s"/>
6 <break time="5s"/>
7 <break time="3s"/>

うん、よいね。

にしてもこれってかなり基礎的で絶対に必要なもんのような気がするんだけど…。ほかのエンジンってどうなってたかなぁと思い返してみるんだけど、多くのものが実装言語の機能をそのまま使えるものが多かったので、おそらくそれらはその中で吸収出来ているんであろうね、だってこんなん気になったことないもん、ほかのエンジン使ってて。

と、あったら恥ずかしいことになるので改めてリファレンスを熟読………ありやがりまして、辱められておめでとうございます。うわぁ、なんか見つけにくい命名…。致し方ない、のかしらねぇ…?

ワタシのは整数であることを前提としてないので同じものではなくて、そしてワタシのでないとワタシのは素直には書けない。ので、「追加し損」ではない。けれど、ints だって十分な場合は十分なわけだ:

5s固定としてその繰り返し回数だけ考える、という単純化(というかおバカ)バージョン
1 {{ block break5rep(times=5) }}
2 {{- range ints(0, times) }}
3 <break time="5s"/>
4 {{- end -}}
5 {{ end }}
voice.jet
1 {{- import "base.jet" -}}
2 {{ yield break5rep(times=3) }}
実行結果
1 <break time="5s"/>
2 <break time="5s"/>
3 <break time="5s"/>

頭まわらんので出来てないが、頑張れば ints でもやりたいことは出来る、かも。けど整数のシーケンスしか作れない、は、今回のようなのでなくてもいずれは不自由に感じることだろう、無駄ではなかった、と思っておこう…。

ところで「追記:」として上の方にも入れまくった「Execute のエラー処理」。やべーよこういうミス。Go ならではというか。Go って、かなり「原点回帰」なのよ、あえて C 言語に先祖返りしてる仕様がたくさんあって、「機構としての「例外」のような「巻き戻し」はない」のね、だから、必要なときにはいつでも都度 func 等からの戻りをチェックしなければならない。わかってたハズなのに、やっぱほかのモダンな言語に染まりすぎちゃってたわ、「エラー処理をサボるとその報いはデカい」てことをすっかり忘れていた。今のワタシの場合 Microsoft Edge TTS Server が相手なんだけど、だとするならば「テンプレート処理結果で誤りが起こった」なら Microsoft Edge TTS Server との通信は端折っていいし、端折るべきである。壊れた XML を投げて通信時間を損する合理的な理由なぞなかろう。サボって良かった、なんてことにはならない。その小さな時間節約が、何分・何十分・何百分の損失に繋がる。みたいなこと。main エントリの終了コードって、バッチ処理にはとても重大なことなのだよね。

2022-02-12追記:
思ったよりも早く必要になった。「テンプレートパスの追加」。

実際ワタシはこの道具を「自分の個人タスクのために」大活躍させているわけなのだけれど、となれば当然「基底お便利ベース」を「共通置き場」に放り込んでおきたい、という欲が出てくるわけだ。こんなのやだろ、と:

 1 [me@host: ~]$ find . -maxdepth 2 -name 'base*.jet' -ls
 2 1023080    1 -rw-r--r--   1 hhsprings    Administrators       35 Feb  02:43 ./wk1/base.jet
 3 1022877    2 -rw-r--r--  11 hhsprings    Administrators     3648 Feb  21:13 ./wk1/base_common.jet
 4 498716    1 -rw-r--r--   1 hhsprings    Administrators     1587 Feb  01:10 ./wk2/base.jet
 5 1022877    2 -rw-r--r--  11 hhsprings    Administrators     3648 Feb  21:13 ./wk2/base_common.jet
 6 6789960    2 -rw-r--r--   1 hhsprings    Administrators     2301 Feb  09:35 ./wk3/base.jet
 7 1022877    2 -rw-r--r--  11 hhsprings    Administrators     3648 Feb  21:13 ./wk3/base_common.jet
 8 498592    1 -rw-r--r--   1 hhsprings    Administrators     1576 Feb  23:38 ./wk4/base.jet
 9 1022877    2 -rw-r--r--  11 hhsprings    Administrators     3648 Feb  21:13 ./wk4/base_common.jet
10 498521    1 -rw-r--r--   1 hhsprings    Administrators     1895 Feb  02:10 ./wk5/base.jet
11 1022877    2 -rw-r--r--  11 hhsprings    Administrators     3648 Feb  21:13 ./wk5/base_common.jet

rwx フラグ表示の次の数字カラムはハードリンク数で、つまり base_common.jet はハードリンクしているわけね、けどこのハードリンクを作るのも維持するのもメンドイさそりゃ、なので例えば common みたいなフォルダに置いて、その場所をローダが見つけられるようにしたいわけだ。

相変わらずドキュメントのわかりにくさでかなり迷走したけれど、出来た:

  1 package main
  2 
  3 import (
  4     "strings"
  5     "os"
  6     "bytes"
  7     "regexp"
  8     "path/filepath"
  9     "io/ioutil"
 10     "encoding/json"
 11     "github.com/ogier/pflag"
 12     mxj "github.com/clbanning/mxj/v2"
 13     "github.com/CloudyKit/jet/v6"
 14     "github.com/CloudyKit/jet/v6/loaders/multi"
 15 )
 16 
 17 func json_loads(jsoncont string) interface{} {
 18     var i interface{}
 19     json.Unmarshal(([]byte)(jsoncont), &i)
 20     return i
 21 }
 22 
 23 func json_load(fn string) interface{} {
 24     jsoncont, err := ioutil.ReadFile(fn)
 25     if err != nil { panic(err) }
 26     return json_loads(string(jsoncont))
 27 }
 28 
 29 func xml_loads(xmlcont string) interface{} {
 30     i, err := mxj.NewMapXml([]byte(xmlcont))
 31     if err != nil { panic(err) }
 32     return i
 33 }
 34 
 35 func xml_load(fn string) interface{} {
 36     xmlcont, err := ioutil.ReadFile(fn)
 37     if err != nil { panic(err) }
 38     return xml_loads(string(xmlcont))
 39 }
 40 
 41 func make_seq(sss ...float64) []float64 {
 42     sta := 0.0
 43     ste := 1.0
 44     sto := 1.0
 45     if len(sss) == 1 {
 46         sto = sss[0]
 47     } else if len(sss) == 2 {
 48         sta = sss[0]
 49         sto = sss[1]
 50     } else if len(sss) == 3 {
 51         sta = sss[0]
 52         sto = sss[1]
 53         ste = sss[2]
 54         if ste == 0.0 {
 55             panic("`step` must not be zero")
 56         }
 57     } else {
 58         panic("require arguments, at least one for `stop`")
 59     }
 60     var res []float64
 61     for {
 62         if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
 63             break
 64         }
 65         res = append(res, sta)
 66         sta += ste
 67     }
 68     return res
 69 }
 70 
 71 func main() {
 72     var (
 73         extravars = pflag.StringP(
 74             "extravars", "v", "", "specify json or xml for the variables")
 75         subnl = pflag.StringP(
 76             "replace_rendered_newline", "n", "\n", "replace newline to this")
 77         addpath = pflag.StringP(
 78             "addpath", "p", "",
 79             "additional template paths (semicolon separated list)")
 80     )
 81     ousage := pflag.Usage
 82     pflag.Usage = func() {
 83         ousage()
 84         os.Exit(1)
 85     }
 86     pflag.Parse()
 87 
 88     var loaders []jet.Loader
 89     loaders = append(loaders, jet.NewOSFileSystemLoader("."))
 90     for _, p := range strings.Split(*addpath, ";") {
 91         loaders = append(loaders, jet.NewOSFileSystemLoader(p))
 92     }
 93     l := multi.NewLoader(loaders...)
 94     tmplfn := pflag.Arg(0)
 95     set := jet.NewSet(l)
 96     tmpl, err := set.GetTemplate(tmplfn)
 97     if err != nil {
 98         panic(err)
 99     }
100     from_fn := func(fn string) interface{} {
101         if filepath.Ext(fn) == ".xml" {
102             return xml_load(fn)
103         } else {
104             return json_load(fn)
105         }
106     }
107     var context interface{}
108     if len(pflag.Args()) > 1 {
109         context = from_fn(pflag.Arg(1))
110     }
111     var variable = make(jet.VarMap)
112     variable.Set("json_loads", json_loads)
113     variable.Set("json_load", json_load)
114     variable.Set("xml_loads", xml_loads)
115     variable.Set("xml_load", xml_load)
116     variable.Set("make_seq", make_seq)
117     if *extravars != "" {
118         outer := from_fn(*extravars)
119         for k, v := range outer.(map[string]interface{}) {
120             variable.Set(k, v)
121         }
122     }
123     nl_remove := func (cont string) string {
124         return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *subnl)
125     }
126     writer := bytes.NewBufferString("")
127     err = tmpl.Execute(writer, variable, context)
128     if err != nil {
129         panic(err)
130     }
131     os.Stdout.Write([]byte(nl_remove(writer.String())))
132 }

バージョン違いのソースコードを読んでしまって混乱したのも時間がかかった要因の一つだけれど、でもね、やっぱりドキュメントの問題よ、これは。

ちなみに蛇足と言うか。この一つ前までの版、「l」(Loader)の Open がいらなかった。なんで気付かなかったんだろ?

2022-02-13追記:
今したい話は2つあるのだが、2つ目はちょっとややこしそうなので後日として、1つ目だけ。

昨日の、「テンプレートの共通置き場を考えたい」なのだが、よく考えたら json_load、xml_load もこれに追従出来ないと困るんではないかと。ベースとなるテンプレートで json_load をあてにしてたとすると、それっておそらくテンプレートと json が同じ場所にあることを前提にしちゃうハズと思うんだよね、事実ワタシはそうしてた。ちょっと、コードの整理も伴ってやりたかったことよりも大きな改造をしちゃったがこんな感じ:

  1 package main
  2 
  3 import (
  4     "strings"
  5     "os"
  6     "bytes"
  7     "regexp"
  8     "path/filepath"
  9     "io"
 10     "encoding/json"
 11     "github.com/ogier/pflag"
 12     mxj "github.com/clbanning/mxj/v2"
 13     "github.com/CloudyKit/jet/v6"
 14     "github.com/CloudyKit/jet/v6/loaders/multi"
 15 )
 16 
 17 type MyOpt struct {
 18     extravars *string
 19     subnl *string
 20     addpath *string
 21 }
 22 var myOpt MyOpt
 23 
 24 func init() {
 25     ousage := pflag.Usage
 26     pflag.Usage = func() {
 27         ousage()
 28         os.Exit(1)
 29     }
 30     myOpt.extravars = pflag.StringP(
 31         "extravars", "v", "", "specify json or xml for the variables")
 32     myOpt.subnl = pflag.StringP(
 33         "replace_rendered_newline", "n", "\n", "replace newline to this")
 34     myOpt.addpath = pflag.StringP(
 35         "addpath", "p", "",
 36         "additional template paths (semicolon separated list)")
 37 }
 38 
 39 func _Read(path string) ([]byte, error) {
 40     sp := append([]string{"."}, strings.Split(*myOpt.addpath, ";")...)
 41     for _, p := range sp {
 42         if f, err := os.Open(filepath.Join(p, path)); err == nil {
 43             return io.ReadAll(f)
 44         }
 45     }
 46     return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
 47 }
 48 
 49 func json_loads(jsoncont string) interface{} {
 50     var i interface{}
 51     json.Unmarshal(([]byte)(jsoncont), &i)
 52     return i
 53 }
 54 
 55 func json_load(fn string) interface{} {
 56     jsoncont, err := _Read(fn)
 57     if err != nil { panic(err) }
 58     return json_loads(string(jsoncont))
 59 }
 60 
 61 func xml_loads(xmlcont string) interface{} {
 62     i, err := mxj.NewMapXml([]byte(xmlcont))
 63     if err != nil { panic(err) }
 64     return i
 65 }
 66 
 67 func xml_load(fn string) interface{} {
 68     xmlcont, err := _Read(fn)
 69     if err != nil { panic(err) }
 70     return xml_loads(string(xmlcont))
 71 }
 72 
 73 func make_seq(sss ...float64) []float64 {
 74     sta := 0.0
 75     ste := 1.0
 76     sto := 1.0
 77     if len(sss) == 1 {
 78         sto = sss[0]
 79     } else if len(sss) == 2 {
 80         sta = sss[0]
 81         sto = sss[1]
 82     } else if len(sss) == 3 {
 83         sta = sss[0]
 84         sto = sss[1]
 85         ste = sss[2]
 86         if ste == 0.0 {
 87             panic("`step` must not be zero")
 88         }
 89     } else {
 90         panic("require arguments, at least one for `stop`")
 91     }
 92     var res []float64
 93     for {
 94         if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
 95             break
 96         }
 97         res = append(res, sta)
 98         sta += ste
 99     }
100     return res
101 }
102 
103 func main() {
104     pflag.Parse()
105 
106     var loaders []jet.Loader
107     loaders = append(loaders, jet.NewOSFileSystemLoader("."))
108     for _, p := range strings.Split(*myOpt.addpath, ";") {
109         loaders = append(loaders, jet.NewOSFileSystemLoader(p))
110     }
111     l := multi.NewLoader(loaders...)
112     tmplfn := pflag.Arg(0)
113     set := jet.NewSet(l)
114     tmpl, err := set.GetTemplate(tmplfn)
115     if err != nil {
116         panic(err)
117     }
118     from_fn := func(fn string) interface{} {
119         if filepath.Ext(fn) == ".xml" {
120             return xml_load(fn)
121         } else {
122             return json_load(fn)
123         }
124     }
125     var context interface{}
126     if len(pflag.Args()) > 1 {
127         context = from_fn(pflag.Arg(1))
128     }
129     var variable = make(jet.VarMap)
130     variable.Set("json_loads", json_loads)
131     variable.Set("json_load", json_load)
132     variable.Set("xml_loads", xml_loads)
133     variable.Set("xml_load", xml_load)
134     variable.Set("make_seq", make_seq)
135     if *myOpt.extravars != "" {
136         outer := from_fn(*myOpt.extravars)
137         for k, v := range outer.(map[string]interface{}) {
138             variable.Set(k, v)
139         }
140     }
141     nl_remove := func (cont string) string {
142         return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *myOpt.subnl)
143     }
144     writer := bytes.NewBufferString("")
145     err = tmpl.Execute(writer, variable, context)
146     if err != nil {
147         panic(err)
148     }
149     os.Stdout.Write([]byte(nl_remove(writer.String())))
150 }

テンプレートの場所と必ず同じなのか、てのは本当は考える余地ありなのかもしれんけど、当座はあんまし凝ったもんはいらんからね、同じ、とした。

「2つ目」なのだが、予告だけしとくと「intへのキャストとか min/max とかあるいは math」の話。ちょっと前の追記の「make_seq vs ints」に関係してる。が、まだ何も話が見えてない、ので、後日、と。

2022-02-14追記:
昨日保留にした話なのだが、なんというかちょっと微妙な気分になってる。

話としては結局のところ「何が欠けているか? 答えは、色々欠けている」ということなんだけれど、だったらどうするのが正解か、てのもなかなか言いにくい。というのも、これから書く内容はハッキリいってしまえば「jet 自身に組み込んだほうがよくね?」てものたちばかりになるし、であれば、ブログのネタとしてじゃなく PR 挙げちまった方が世のため人のためになると思う。ただ、…、そう、話は「built-ins」の話になるので、これって、まぁ技術者間の好みの問題も含め、論争の種にもなりうるような、そういう繊細な話にもなりうる。言い方を変えるなら「こうしてご自分で自由に追加出来る余地は提供されてるんだから、それでええやん」てことやね。

ほかの言語で実現されているテンプレートエンジンの多くは、実装言語機能を「直接」使えることが多くて、そうである場合は組み込み機能の提供量で不満を感じる機会は少ない。けれども jet はそうではないので、不足機能があればここまでやってきたやり方で追加していくことになる。それをどの程度予めやってあるか、つまり built-ins が不十分であることが不満の元となる可能性がある、という話だが、事実ワタシのやりたいたった一つの実例で既にそれが露呈しつつある、ということ。

make_seq と ints の例で、もう答えは出ているようなもんなんだけど、「当り前のことがしたい場合に足りていない機能が既に結構多い」のだわ。難しいことがしたい場合に欠けている、ではないんだよ、基礎的なことを行う際の部品から既に足りない。上でやった例がわかりやすい:

base.jet
1 {{ block break_s(sec=5) }}
2 {{- range make_seq(sec / 5) }}
3 {{- nxt := ((. + 1) * 5) }}
4 {{- if nxt > sec }}{{ nxt = sec }}{{ end -}}
5 {{- t := nxt - (. * 5) -}}
6 <break time="{{t}}s"/>
7 {{ end -}}
8 {{ end }}

ひとつには make_seq のように整数を前提としないシーケンス生成が必要だったということを意味しているが、そうではなくて例えばここで「sec / 5」を「整数にしたい」と思ったとして、既にその機能がない。つまり「int へのキャスト」のようなことが出来ない、ここでの例で make_seq 自身がそう空気を読もうとしない限りは。さらに、たとえば「{{t}}」部分で数値書式化、たとえば float で「%.1f」で出力したいと思ったとしても、またしてもその機能がない。

どちらも「追加しちゃえる」:

  1 package main
  2 
  3 import (
  4     "fmt"
  5     "strings"
  6     "strconv"
  7     "os"
  8     "bytes"
  9     "regexp"
 10     "reflect"
 11     "path/filepath"
 12     "io"
 13     "encoding/json"
 14     "github.com/ogier/pflag"
 15     mxj "github.com/clbanning/mxj/v2"
 16     "github.com/CloudyKit/jet/v6"
 17     "github.com/CloudyKit/jet/v6/loaders/multi"
 18 )
 19 
 20 type MyOpt struct {
 21     extravars *string
 22     subnl *string
 23     addpath *string
 24 }
 25 var myOpt MyOpt
 26 
 27 func init() {
 28     ousage := pflag.Usage
 29     pflag.Usage = func() {
 30         ousage()
 31         os.Exit(1)
 32     }
 33     myOpt.extravars = pflag.StringP(
 34         "extravars", "v", "", "specify json or xml for the variables")
 35     myOpt.subnl = pflag.StringP(
 36         "replace_rendered_newline", "n", "\n", "replace newline to this")
 37     myOpt.addpath = pflag.StringP(
 38         "addpath", "p", "",
 39         "additional template paths (semicolon separated list)")
 40 }
 41 
 42 func to_num(iv interface{}) interface{} {
 43     v := reflect.ValueOf(iv)
 44     if !v.IsValid() {
 45         panic(fmt.Errorf("invalid value can't be converted to float64"))
 46     }
 47     kind := v.Kind()
 48     if kind == reflect.Float32 || kind == reflect.Float64 {
 49         return v.Float()
 50     } else if kind >= reflect.Int && kind <= reflect.Int64 {
 51         return v.Int()
 52     } else if kind >= reflect.Uint && kind <= reflect.Uint64 {
 53         return v.Uint()
 54     } else if kind == reflect.String {
 55         n, e := strconv.ParseFloat(v.String(), 0)
 56         if e != nil {
 57             panic(e)
 58         }
 59         return n
 60     } else if kind == reflect.Bool {
 61         if v.Bool() {
 62             return 0
 63         }
 64         return 1
 65     }
 66     panic(fmt.Errorf("type: %q can't be converted to float64", v.Type()))
 67 }
 68 func to_int(iv interface{}, base... int) interface{} {
 69     v := reflect.ValueOf(iv)
 70     if !v.IsValid() {
 71         panic(fmt.Errorf("invalid value can't be converted to float64"))
 72     }
 73     kind := v.Kind()
 74     if kind == reflect.String {
 75         var b int
 76         if len(base) > 0 {
 77             b = base[0]
 78         } else {
 79             b = 10
 80         }
 81         s := v.String()
 82         if b == 16 && regexp.MustCompile(`^0[xX]`).MatchString(s) {
 83             s = s[2:]
 84         }
 85         n, e := strconv.ParseInt(s, b, 0)
 86         if e != nil {
 87             panic(e)
 88         }
 89         return n
 90     }
 91     return int(to_num(iv).(float64))
 92 }
 93 
 94 func _Read(path string) ([]byte, error) {
 95     sp := append([]string{"."}, strings.Split(*myOpt.addpath, ";")...)
 96     for _, p := range sp {
 97         if f, err := os.Open(filepath.Join(p, path)); err == nil {
 98             return io.ReadAll(f)
 99         }
100     }
101     return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
102 }
103 
104 func json_loads(jsoncont string) interface{} {
105     var i interface{}
106     json.Unmarshal(([]byte)(jsoncont), &i)
107     return i
108 }
109 
110 func json_load(fn string) interface{} {
111     jsoncont, err := _Read(fn)
112     if err != nil { panic(err) }
113     return json_loads(string(jsoncont))
114 }
115 
116 func xml_loads(xmlcont string) interface{} {
117     i, err := mxj.NewMapXml([]byte(xmlcont))
118     if err != nil { panic(err) }
119     return i
120 }
121 
122 func xml_load(fn string) interface{} {
123     xmlcont, err := _Read(fn)
124     if err != nil { panic(err) }
125     return xml_loads(string(xmlcont))
126 }
127 
128 func make_seq(sss ...float64) []float64 {
129     sta := 0.0
130     ste := 1.0
131     sto := 1.0
132     if len(sss) == 1 {
133         sto = sss[0]
134     } else if len(sss) == 2 {
135         sta = sss[0]
136         sto = sss[1]
137     } else if len(sss) == 3 {
138         sta = sss[0]
139         sto = sss[1]
140         ste = sss[2]
141         if ste == 0.0 {
142             panic("`step` must not be zero")
143         }
144     } else {
145         panic("require arguments, at least one for `stop`")
146     }
147     var res []float64
148     for {
149         if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
150             break
151         }
152         res = append(res, sta)
153         sta += ste
154     }
155     return res
156 }
157 
158 func main() {
159     pflag.Parse()
160 
161     var loaders []jet.Loader
162     loaders = append(loaders, jet.NewOSFileSystemLoader("."))
163     for _, p := range strings.Split(*myOpt.addpath, ";") {
164         loaders = append(loaders, jet.NewOSFileSystemLoader(p))
165     }
166     l := multi.NewLoader(loaders...)
167     tmplfn := pflag.Arg(0)
168     set := jet.NewSet(l)
169     tmpl, err := set.GetTemplate(tmplfn)
170     if err != nil {
171         panic(err)
172     }
173     from_fn := func(fn string) interface{} {
174         if filepath.Ext(fn) == ".xml" {
175             return xml_load(fn)
176         } else {
177             return json_load(fn)
178         }
179     }
180     var context interface{}
181     if len(pflag.Args()) > 1 {
182         context = from_fn(pflag.Arg(1))
183     }
184     var variable = make(jet.VarMap)
185     variable.Set("json_loads", json_loads)
186     variable.Set("json_load", json_load)
187     variable.Set("xml_loads", xml_loads)
188     variable.Set("xml_load", xml_load)
189     variable.Set("make_seq", make_seq)
190     variable.Set("float", to_num)
191     variable.Set("int", to_int)
192     variable.Set("sprintf", fmt.Sprintf)
193     if *myOpt.extravars != "" {
194         outer := from_fn(*myOpt.extravars)
195         for k, v := range outer.(map[string]interface{}) {
196             variable.Set(k, v)
197         }
198     }
199     nl_remove := func (cont string) string {
200         return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *myOpt.subnl)
201     }
202     writer := bytes.NewBufferString("")
203     err = tmpl.Execute(writer, variable, context)
204     if err != nil {
205         panic(err)
206     }
207     os.Stdout.Write([]byte(nl_remove(writer.String())))
208 }
使用例
 1 {{ int("30") }}
 2 {{ int(5.1 / 2) }}
 3 {{ int(3) }}
 4 {{ int("01", 8) }}
 5 {{ int("07", 8) }}
 6 {{ int("10", 8) }}
 7 {{ int("11", 8) }}
 8 {{ int("F", 16) }}
 9 {{ int(0xE) }}
10 {{ int("0xE", 16) }}
11 {{- s := "0xE" }}
12 {{ int(s[2:], 16) }}
13 {{ sprintf("%.9f", float("29.5e-3")) }}

そりゃぁ別になんにも難しいことはないわいよ。to_num、to_int の実装はかなり github.com/CloudyKit/jet/v6/eval.go 内の実装を真似れたしさ。けど「さすがにこれ、built-ins にないのって、あまりにもあんまりだ」と思わんか? 特に sprintf よな。

そして話はさらに「min/max」や、math 関数に及ぶわけだ、当然それらもない。少なくとも min, max があれば、上の break の実現は少しスマートになる。そして「どこまでやれば満足か?」という境界の問題になってくる。そもそも「int化」だけでなく、丸め機能のワンセット(floor, ceil, round)が必要なことも多いしさ。うーん…。ワタシとしてはまずは min/max と math のフルセットがあると嬉しい、と思うんだけどね、strconv フルセット、みたいなのもチラついちゃうんよね、ので、ちょっともうちっと落ち着いて考えたいわ。

ので、今回はひとまずここまで。

2022-02-15追記:
半日寝かせたくらいじゃ、取り立てて考えが整理出来るわけでもないのだよね。たとえば math について「全部だ」と決断しようとする場合ですら「Constants も含めるか」というバリエーションがあってだな。

ひとまず「ループのコントロールにて使いがちなのでとにかく非常に日常的に頻出」という意味で、符号操作・丸め・刈り取り系だけは、と考えるならば、以下の品揃えになるだろうと思う:

mod, remainder は直接使わずとも「%」演算子があるからいい、という考え方もあるけれど、まぁあってもいいだろうとは思う。で、これらをミニマルと考えたとしての「じゃぁその次に最小限は?」の答えは…、まぁあんましないね。どれもいらんとも言えるし、どれもいるとも言えるし。どれも割と基礎的な級数展開の元になるものたちだからね、必要になる時は必要になる。Go 実装の変更はオモロいもんではない:

  1 package main
  2 
  3 import (
  4     "fmt"
  5     "strings"
  6     "strconv"
  7     "os"
  8     "bytes"
  9     "regexp"
 10     "reflect"
 11     "path/filepath"
 12     "io"
 13     "math"
 14     "encoding/json"
 15     "github.com/ogier/pflag"
 16     mxj "github.com/clbanning/mxj/v2"
 17     "github.com/CloudyKit/jet/v6"
 18     "github.com/CloudyKit/jet/v6/loaders/multi"
 19 )
 20 
 21 type MyOpt struct {
 22     extravars *string
 23     subnl *string
 24     addpath *string
 25 }
 26 var myOpt MyOpt
 27 
 28 func init() {
 29     ousage := pflag.Usage
 30     pflag.Usage = func() {
 31         ousage()
 32         os.Exit(1)
 33     }
 34     myOpt.extravars = pflag.StringP(
 35         "extravars", "v", "", "specify json or xml for the variables")
 36     myOpt.subnl = pflag.StringP(
 37         "replace_rendered_newline", "n", "\n", "replace newline to this")
 38     myOpt.addpath = pflag.StringP(
 39         "addpath", "p", "",
 40         "additional template paths (semicolon separated list)")
 41 }
 42 
 43 func to_num(iv interface{}) interface{} {
 44     v := reflect.ValueOf(iv)
 45     if !v.IsValid() {
 46         panic(fmt.Errorf("invalid value can't be converted to float64"))
 47     }
 48     kind := v.Kind()
 49     if kind == reflect.Float32 || kind == reflect.Float64 {
 50         return v.Float()
 51     } else if kind >= reflect.Int && kind <= reflect.Int64 {
 52         return v.Int()
 53     } else if kind >= reflect.Uint && kind <= reflect.Uint64 {
 54         return v.Uint()
 55     } else if kind == reflect.String {
 56         n, e := strconv.ParseFloat(v.String(), 0)
 57         if e != nil {
 58             panic(e)
 59         }
 60         return n
 61     } else if kind == reflect.Bool {
 62         if v.Bool() {
 63             return 0
 64         }
 65         return 1
 66     }
 67     panic(fmt.Errorf("type: %q can't be converted to float64", v.Type()))
 68 }
 69 func to_int(iv interface{}, base... int) interface{} {
 70     v := reflect.ValueOf(iv)
 71     if !v.IsValid() {
 72         panic(fmt.Errorf("invalid value can't be converted to float64"))
 73     }
 74     kind := v.Kind()
 75     if kind == reflect.String {
 76         var b int
 77         if len(base) > 0 {
 78             b = base[0]
 79         } else {
 80             b = 10
 81         }
 82         s := v.String()
 83         if b == 16 && regexp.MustCompile(`^0[xX]`).MatchString(s) {
 84             s = s[2:]
 85         }
 86         n, e := strconv.ParseInt(s, b, 0)
 87         if e != nil {
 88             panic(e)
 89         }
 90         return n
 91     }
 92     return int(to_num(iv).(float64))
 93 }
 94 
 95 func _Read(path string) ([]byte, error) {
 96     sp := append([]string{"."}, strings.Split(*myOpt.addpath, ";")...)
 97     for _, p := range sp {
 98         if f, err := os.Open(filepath.Join(p, path)); err == nil {
 99             return io.ReadAll(f)
100         }
101     }
102     return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
103 }
104 
105 func json_loads(jsoncont string) interface{} {
106     var i interface{}
107     json.Unmarshal(([]byte)(jsoncont), &i)
108     return i
109 }
110 
111 func json_load(fn string) interface{} {
112     jsoncont, err := _Read(fn)
113     if err != nil { panic(err) }
114     return json_loads(string(jsoncont))
115 }
116 
117 func xml_loads(xmlcont string) interface{} {
118     i, err := mxj.NewMapXml([]byte(xmlcont))
119     if err != nil { panic(err) }
120     return i
121 }
122 
123 func xml_load(fn string) interface{} {
124     xmlcont, err := _Read(fn)
125     if err != nil { panic(err) }
126     return xml_loads(string(xmlcont))
127 }
128 
129 func make_seq(sss ...float64) []float64 {
130     sta := 0.0
131     ste := 1.0
132     sto := 1.0
133     if len(sss) == 1 {
134         sto = sss[0]
135     } else if len(sss) == 2 {
136         sta = sss[0]
137         sto = sss[1]
138     } else if len(sss) == 3 {
139         sta = sss[0]
140         sto = sss[1]
141         ste = sss[2]
142         if ste == 0.0 {
143             panic("`step` must not be zero")
144         }
145     } else {
146         panic("require arguments, at least one for `stop`")
147     }
148     var res []float64
149     for {
150         if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
151             break
152         }
153         res = append(res, sta)
154         sta += ste
155     }
156     return res
157 }
158 
159 func main() {
160     pflag.Parse()
161 
162     var loaders []jet.Loader
163     loaders = append(loaders, jet.NewOSFileSystemLoader("."))
164     for _, p := range strings.Split(*myOpt.addpath, ";") {
165         loaders = append(loaders, jet.NewOSFileSystemLoader(p))
166     }
167     l := multi.NewLoader(loaders...)
168     tmplfn := pflag.Arg(0)
169     set := jet.NewSet(l)
170     tmpl, err := set.GetTemplate(tmplfn)
171     if err != nil {
172         panic(err)
173     }
174     from_fn := func(fn string) interface{} {
175         if filepath.Ext(fn) == ".xml" {
176             return xml_load(fn)
177         } else {
178             return json_load(fn)
179         }
180     }
181     var context interface{}
182     if len(pflag.Args()) > 1 {
183         context = from_fn(pflag.Arg(1))
184     }
185     var variable = make(jet.VarMap)
186     variable.Set("json_loads", json_loads)
187     variable.Set("json_load", json_load)
188     variable.Set("xml_loads", xml_loads)
189     variable.Set("xml_load", xml_load)
190     variable.Set("make_seq", make_seq)
191     variable.Set("float", to_num)
192     variable.Set("int", to_int)
193     variable.Set("sprintf", fmt.Sprintf)
194     variable.Set("abs", math.Abs)
195     variable.Set("ceil", math.Ceil)
196     variable.Set("copysign", math.Copysign)
197     variable.Set("dim", math.Dim)
198     variable.Set("floor", math.Floor)
199     variable.Set("max", math.Max)
200     variable.Set("min", math.Min)
201     variable.Set("round", math.Round)
202     variable.Set("roundtoeven", math.RoundToEven)
203     variable.Set("trunc", math.Trunc)
204     variable.Set("mod", math.Mod)
205     variable.Set("modf", math.Modf)
206     variable.Set("remainder", math.Remainder)
207     if *myOpt.extravars != "" {
208         outer := from_fn(*myOpt.extravars)
209         for k, v := range outer.(map[string]interface{}) {
210             variable.Set(k, v)
211         }
212     }
213     nl_remove := func (cont string) string {
214         return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *myOpt.subnl)
215     }
216     writer := bytes.NewBufferString("")
217     err = tmpl.Execute(writer, variable, context)
218     if err != nil {
219         panic(err)
220     }
221     os.Stdout.Write([]byte(nl_remove(writer.String())))
222 }

まぁ自分のためだけ、てことならこれで存分に十分なんだけどね。ベッセル関数関連なんかこの後一生使わん可能性すらあるですもの。

2022-02-19追記:
「Go 言語のおべんきょ」としての「だとしても少しは実用になるネタ」なんてことを考えてた流れで、ほかでやってたネタとこのネタが結び付いてくれた。putclip はよく使うが getclip はあんまし用途ないよなぁなんて言ったけれど、ふと、テンプレートエンジンの機能としてあったら便利ぞな、と気付いた、今更だけど。

てわけで、「Goのおべんきょ」の足しには全然ならんけれど、道具の実用性はぐんと増すてやーつ:

  1 package main
  2 
  3 import (
  4     "fmt"
  5     "strings"
  6     "strconv"
  7     "os"
  8     "bytes"
  9     "regexp"
 10     "reflect"
 11     "path/filepath"
 12     "io"
 13     "math"
 14     "encoding/json"
 15     "github.com/ogier/pflag"
 16     mxj "github.com/clbanning/mxj/v2"
 17     "github.com/CloudyKit/jet/v6"
 18     "github.com/CloudyKit/jet/v6/loaders/multi"
 19     "golang.design/x/clipboard"  // github.com/golang-design/clipboard
 20 )
 21 
 22 type MyOpt struct {
 23     extravars *string
 24     subnl *string
 25     addpath *string
 26 }
 27 var myOpt MyOpt
 28 
 29 func init() {
 30     ousage := pflag.Usage
 31     pflag.Usage = func() {
 32         ousage()
 33         os.Exit(1)
 34     }
 35     myOpt.extravars = pflag.StringP(
 36         "extravars", "v", "", "specify json or xml for the variables")
 37     myOpt.subnl = pflag.StringP(
 38         "replace_rendered_newline", "n", "\n", "replace newline to this")
 39     myOpt.addpath = pflag.StringP(
 40         "addpath", "p", "",
 41         "additional template paths (semicolon separated list)")
 42 
 43     err := clipboard.Init()
 44     if err != nil {
 45         panic(err)
 46     }
 47 }
 48 
 49 func to_num(iv interface{}) interface{} {
 50     v := reflect.ValueOf(iv)
 51     if !v.IsValid() {
 52         panic(fmt.Errorf("invalid value can't be converted to float64"))
 53     }
 54     kind := v.Kind()
 55     if kind == reflect.Float32 || kind == reflect.Float64 {
 56         return v.Float()
 57     } else if kind >= reflect.Int && kind <= reflect.Int64 {
 58         return v.Int()
 59     } else if kind >= reflect.Uint && kind <= reflect.Uint64 {
 60         return v.Uint()
 61     } else if kind == reflect.String {
 62         n, e := strconv.ParseFloat(v.String(), 0)
 63         if e != nil {
 64             panic(e)
 65         }
 66         return n
 67     } else if kind == reflect.Bool {
 68         if v.Bool() {
 69             return 0
 70         }
 71         return 1
 72     }
 73     panic(fmt.Errorf("type: %q can't be converted to float64", v.Type()))
 74 }
 75 func to_int(iv interface{}, base... int) interface{} {
 76     v := reflect.ValueOf(iv)
 77     if !v.IsValid() {
 78         panic(fmt.Errorf("invalid value can't be converted to float64"))
 79     }
 80     kind := v.Kind()
 81     if kind == reflect.String {
 82         var b int
 83         if len(base) > 0 {
 84             b = base[0]
 85         } else {
 86             b = 10
 87         }
 88         s := v.String()
 89         if b == 16 && regexp.MustCompile(`^0[xX]`).MatchString(s) {
 90             s = s[2:]
 91         }
 92         n, e := strconv.ParseInt(s, b, 0)
 93         if e != nil {
 94             panic(e)
 95         }
 96         return n
 97     }
 98     return int(to_num(iv).(float64))
 99 }
100 
101 func _Read(path string) ([]byte, error) {
102     sp := append([]string{"."}, strings.Split(*myOpt.addpath, ";")...)
103     for _, p := range sp {
104         if f, err := os.Open(filepath.Join(p, path)); err == nil {
105             return io.ReadAll(f)
106         }
107     }
108     return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
109 }
110 
111 func json_loads(jsoncont string) interface{} {
112     var i interface{}
113     json.Unmarshal(([]byte)(jsoncont), &i)
114     return i
115 }
116 
117 func json_load(fn string) interface{} {
118     jsoncont, err := _Read(fn)
119     if err != nil { panic(err) }
120     return json_loads(string(jsoncont))
121 }
122 
123 func xml_loads(xmlcont string) interface{} {
124     i, err := mxj.NewMapXml([]byte(xmlcont))
125     if err != nil { panic(err) }
126     return i
127 }
128 
129 func xml_load(fn string) interface{} {
130     xmlcont, err := _Read(fn)
131     if err != nil { panic(err) }
132     return xml_loads(string(xmlcont))
133 }
134 
135 func make_seq(sss ...float64) []float64 {
136     sta := 0.0
137     ste := 1.0
138     sto := 1.0
139     if len(sss) == 1 {
140         sto = sss[0]
141     } else if len(sss) == 2 {
142         sta = sss[0]
143         sto = sss[1]
144     } else if len(sss) == 3 {
145         sta = sss[0]
146         sto = sss[1]
147         ste = sss[2]
148         if ste == 0.0 {
149             panic("`step` must not be zero")
150         }
151     } else {
152         panic("require arguments, at least one for `stop`")
153     }
154     var res []float64
155     for {
156         if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
157             break
158         }
159         res = append(res, sta)
160         sta += ste
161     }
162     return res
163 }
164 
165 func getclip(fmtimg bool) ([]byte) {
166     if !fmtimg {
167         return clipboard.Read(clipboard.FmtText)
168     } else {
169         return clipboard.Read(clipboard.FmtImage)
170     }
171 }
172 
173 func main() {
174     pflag.Parse()
175 
176     var loaders []jet.Loader
177     loaders = append(loaders, jet.NewOSFileSystemLoader("."))
178     for _, p := range strings.Split(*myOpt.addpath, ";") {
179         loaders = append(loaders, jet.NewOSFileSystemLoader(p))
180     }
181     l := multi.NewLoader(loaders...)
182     tmplfn := pflag.Arg(0)
183     set := jet.NewSet(l)
184     tmpl, err := set.GetTemplate(tmplfn)
185     if err != nil {
186         panic(err)
187     }
188     from_fn := func(fn string) interface{} {
189         if filepath.Ext(fn) == ".xml" {
190             return xml_load(fn)
191         } else {
192             return json_load(fn)
193         }
194     }
195     var context interface{}
196     if len(pflag.Args()) > 1 {
197         context = from_fn(pflag.Arg(1))
198     }
199     var variable = make(jet.VarMap)
200     variable.Set("json_loads", json_loads)
201     variable.Set("json_load", json_load)
202     variable.Set("xml_loads", xml_loads)
203     variable.Set("xml_load", xml_load)
204     variable.Set("make_seq", make_seq)
205     variable.Set("float", to_num)
206     variable.Set("int", to_int)
207     variable.Set("sprintf", fmt.Sprintf)
208     variable.Set("abs", math.Abs)
209     variable.Set("ceil", math.Ceil)
210     variable.Set("copysign", math.Copysign)
211     variable.Set("dim", math.Dim)
212     variable.Set("floor", math.Floor)
213     variable.Set("max", math.Max)
214     variable.Set("min", math.Min)
215     variable.Set("round", math.Round)
216     variable.Set("roundtoeven", math.RoundToEven)
217     variable.Set("trunc", math.Trunc)
218     variable.Set("mod", math.Mod)
219     variable.Set("modf", math.Modf)
220     variable.Set("remainder", math.Remainder)
221     variable.Set("getclip", getclip)
222     if *myOpt.extravars != "" {
223         outer := from_fn(*myOpt.extravars)
224         for k, v := range outer.(map[string]interface{}) {
225             variable.Set(k, v)
226         }
227     }
228     nl_remove := func (cont string) string {
229         return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *myOpt.subnl)
230     }
231     writer := bytes.NewBufferString("")
232     err = tmpl.Execute(writer, variable, context)
233     if err != nil {
234         panic(err)
235     }
236     os.Stdout.Write([]byte(nl_remove(writer.String())))
237 }

使用例:

1 {{ getclip(false) | upper }}
2 {* | html エスケープはデフォルトの振る舞いなので例としては相応しくない、
3 ので、クソおもしろくない upper を例にしてる。 *}

これさ、まさに今あなたが目にしてるこのページのね、スニペット部分があるじゃんか。これを「ワタシがあなたに見せるために執筆」してるときには、これってただの html のテキストボックスに書き込んでるわけですよ、ゆえに、html に記述する際にはエスケープしなければならない文字たち(大なり小なりとかよ)は、手作業でエスケープしてるわけ。でも今やったようなのを使えば、まぁちょっと楽出来るんだわ。getclip と組み合わせればなおのこと。うん、我ながらいいこと思いついたわい、とも思うし、今頃気付くなよ、とも思う。もっと早く気付いとけば…とね。

フィルタとして使えるのが今は html くらいしかないけれど、png イメージのために base64 エンコーダとかも組み込むとなお良いよね、これはあとでやろっかなと。やったとすれば使用例はこうなるはず:

想像図
1 {{ getclip(true) | base64encode | raw }}

2022-02-19追記(2):
「png イメージのために base64 エンコーダとかも組み込むとなお良い」:

  1 package main
  2 
  3 import (
  4     "fmt"
  5     "strings"
  6     "strconv"
  7     "os"
  8     "bytes"
  9     "regexp"
 10     "reflect"
 11     "path/filepath"
 12     "io"
 13     "math"
 14     "encoding/json"
 15     "encoding/base64"
 16     "github.com/ogier/pflag"
 17     mxj "github.com/clbanning/mxj/v2"
 18     "github.com/CloudyKit/jet/v6"
 19     "github.com/CloudyKit/jet/v6/loaders/multi"
 20     "golang.design/x/clipboard"  // github.com/golang-design/clipboard
 21 )
 22 
 23 type MyOpt struct {
 24     extravars *string
 25     subnl *string
 26     addpath *string
 27 }
 28 var myOpt MyOpt
 29 
 30 func init() {
 31     ousage := pflag.Usage
 32     pflag.Usage = func() {
 33         ousage()
 34         os.Exit(1)
 35     }
 36     myOpt.extravars = pflag.StringP(
 37         "extravars", "v", "", "specify json or xml for the variables")
 38     myOpt.subnl = pflag.StringP(
 39         "replace_rendered_newline", "n", "\n", "replace newline to this")
 40     myOpt.addpath = pflag.StringP(
 41         "addpath", "p", "",
 42         "additional template paths (semicolon separated list)")
 43 
 44     err := clipboard.Init()
 45     if err != nil {
 46         panic(err)
 47     }
 48 }
 49 
 50 func to_num(iv interface{}) interface{} {
 51     v := reflect.ValueOf(iv)
 52     if !v.IsValid() {
 53         panic(fmt.Errorf("invalid value can't be converted to float64"))
 54     }
 55     kind := v.Kind()
 56     if kind == reflect.Float32 || kind == reflect.Float64 {
 57         return v.Float()
 58     } else if kind >= reflect.Int && kind <= reflect.Int64 {
 59         return v.Int()
 60     } else if kind >= reflect.Uint && kind <= reflect.Uint64 {
 61         return v.Uint()
 62     } else if kind == reflect.String {
 63         n, e := strconv.ParseFloat(v.String(), 0)
 64         if e != nil {
 65             panic(e)
 66         }
 67         return n
 68     } else if kind == reflect.Bool {
 69         if v.Bool() {
 70             return 0
 71         }
 72         return 1
 73     }
 74     panic(fmt.Errorf("type: %q can't be converted to float64", v.Type()))
 75 }
 76 func to_int(iv interface{}, base... int) interface{} {
 77     v := reflect.ValueOf(iv)
 78     if !v.IsValid() {
 79         panic(fmt.Errorf("invalid value can't be converted to float64"))
 80     }
 81     kind := v.Kind()
 82     if kind == reflect.String {
 83         var b int
 84         if len(base) > 0 {
 85             b = base[0]
 86         } else {
 87             b = 10
 88         }
 89         s := v.String()
 90         if b == 16 && regexp.MustCompile(`^0[xX]`).MatchString(s) {
 91             s = s[2:]
 92         }
 93         n, e := strconv.ParseInt(s, b, 0)
 94         if e != nil {
 95             panic(e)
 96         }
 97         return n
 98     }
 99     return int(to_num(iv).(float64))
100 }
101 
102 func _Read(path string) ([]byte, error) {
103     sp := append([]string{"."}, strings.Split(*myOpt.addpath, ";")...)
104     for _, p := range sp {
105         if f, err := os.Open(filepath.Join(p, path)); err == nil {
106             return io.ReadAll(f)
107         }
108     }
109     return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
110 }
111 
112 func json_loads(jsoncont string) interface{} {
113     var i interface{}
114     json.Unmarshal(([]byte)(jsoncont), &i)
115     return i
116 }
117 
118 func json_load(fn string) interface{} {
119     jsoncont, err := _Read(fn)
120     if err != nil { panic(err) }
121     return json_loads(string(jsoncont))
122 }
123 
124 func xml_loads(xmlcont string) interface{} {
125     i, err := mxj.NewMapXml([]byte(xmlcont))
126     if err != nil { panic(err) }
127     return i
128 }
129 
130 func xml_load(fn string) interface{} {
131     xmlcont, err := _Read(fn)
132     if err != nil { panic(err) }
133     return xml_loads(string(xmlcont))
134 }
135 
136 func make_seq(sss ...float64) []float64 {
137     sta := 0.0
138     ste := 1.0
139     sto := 1.0
140     if len(sss) == 1 {
141         sto = sss[0]
142     } else if len(sss) == 2 {
143         sta = sss[0]
144         sto = sss[1]
145     } else if len(sss) == 3 {
146         sta = sss[0]
147         sto = sss[1]
148         ste = sss[2]
149         if ste == 0.0 {
150             panic("`step` must not be zero")
151         }
152     } else {
153         panic("require arguments, at least one for `stop`")
154     }
155     var res []float64
156     for {
157         if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
158             break
159         }
160         res = append(res, sta)
161         sta += ste
162     }
163     return res
164 }
165 
166 func getclip(fmtimg bool) ([]byte) {
167     if !fmtimg {
168         return clipboard.Read(clipboard.FmtText)
169     } else {
170         return clipboard.Read(clipboard.FmtImage)
171     }
172 }
173 
174 func main() {
175     pflag.Parse()
176 
177     var loaders []jet.Loader
178     loaders = append(loaders, jet.NewOSFileSystemLoader("."))
179     for _, p := range strings.Split(*myOpt.addpath, ";") {
180         loaders = append(loaders, jet.NewOSFileSystemLoader(p))
181     }
182     l := multi.NewLoader(loaders...)
183     tmplfn := pflag.Arg(0)
184     set := jet.NewSet(l)
185     tmpl, err := set.GetTemplate(tmplfn)
186     if err != nil {
187         panic(err)
188     }
189     from_fn := func(fn string) interface{} {
190         if filepath.Ext(fn) == ".xml" {
191             return xml_load(fn)
192         } else {
193             return json_load(fn)
194         }
195     }
196     var context interface{}
197     if len(pflag.Args()) > 1 {
198         context = from_fn(pflag.Arg(1))
199     }
200     var variable = make(jet.VarMap)
201     variable.Set("json_loads", json_loads)
202     variable.Set("json_load", json_load)
203     variable.Set("xml_loads", xml_loads)
204     variable.Set("xml_load", xml_load)
205     variable.Set("make_seq", make_seq)
206     variable.Set("float", to_num)
207     variable.Set("int", to_int)
208     variable.Set("sprintf", fmt.Sprintf)
209     variable.Set("abs", math.Abs)
210     variable.Set("ceil", math.Ceil)
211     variable.Set("copysign", math.Copysign)
212     variable.Set("dim", math.Dim)
213     variable.Set("floor", math.Floor)
214     variable.Set("max", math.Max)
215     variable.Set("min", math.Min)
216     variable.Set("round", math.Round)
217     variable.Set("roundtoeven", math.RoundToEven)
218     variable.Set("trunc", math.Trunc)
219     variable.Set("mod", math.Mod)
220     variable.Set("modf", math.Modf)
221     variable.Set("remainder", math.Remainder)
222     variable.Set("getclip", getclip)
223     variable.Set("base64encode", base64.StdEncoding.EncodeToString)
224     variable.Set("base64decode", base64.StdEncoding.DecodeString)
225     if *myOpt.extravars != "" {
226         outer := from_fn(*myOpt.extravars)
227         for k, v := range outer.(map[string]interface{}) {
228             variable.Set(k, v)
229         }
230     }
231     nl_remove := func (cont string) string {
232         return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *myOpt.subnl)
233     }
234     writer := bytes.NewBufferString("")
235     err = tmpl.Execute(writer, variable, context)
236     if err != nil {
237         panic(err)
238     }
239     os.Stdout.Write([]byte(nl_remove(writer.String())))
240 }

まぁなんてことはないよね。なお、これまで何気にやってたけど「base64.StdEncoding.DecodeString」の戻りには error を含むのだが、そこはうまくやってくれる。

ところで上で追記コメント入れたけど、この回でデリミタの変更をトライしたんだけれど、#187 のせいで一旦断念した。やめて欲しいよなこういうの。

2022-03-13追記:
本題とちょっと離れるんだけど、WSL2 の導入で本物の linux を自由に使えるようになったんで、そっちの環境でビルドしてみようと試みたんだけど、ちょっと問題が。

まず、本日時点での「ubuntu on WSL2 マイクロソフト公式」の apt 管理の Go はめっさ古くて 1.13.8、なので、1.17.8 をわざわざ入れた。これはワタシが今ネイティブ Windows 上で使ってる 1.16 より新しい。次に「ubuntu on WSL2 マイクロソフト公式、on Windows 10」ということは必然的に「X Window System、ナニソレタベレンノ」なの。

そういうわけで、まずは「X Window System、ナニソレタベレンノ」が clipboard 部分のビルドで問題を起こす。のだけど、それを回避するためにと C で言うところの「#ifdef」みたいなものはないだろうか、と探してみたんだけれど、…うん、あるにはあるんだ、だけどそれ、Go ではめっちゃ限定的で、コンパイル単位、つまり(// +build tag というディレクティブで)「a.go」というファイル一個単位の制御しか出来ないのよ、「問題を起こす部分だけ取り除く」ことが出来ない。うげぇ。いろいろ考えたが、「オレオレプリプロセス」が一番シンプルよな、と、sed で処理出来る単純なコメント(//ENABLE_CLIP BEGIN と //ENABLE_CLIP END)を突っ込んでおくことにした。で、ビルド用のスクリプトを書いておく:

build.sh
 1 #! /bin/sh
 2 if test -z "${COMSPEC}" ; then
 3     ext=""
 4 else
 5     ext=".exe"
 6 fi
 7 if test "$1"="--disable-clip" ; then
 8     sed '/ENABLE_CLIP BEGIN/,/ENABLE_CLIP END/'d main.go.in > main.go
 9 else
10     cat main.go.in > main.go
11 fi
12 
13 go build && (
14     rm -f go_jettemplate_example${ext}
15     ln tmpljetexpr${ext} go_jettemplate_example${ext}
16     install -c go_jettemplate_example${ext} /usr/local/bin
17 )

go のソースは「main.go.in」にしとく、てことだよ。昔ながらの方法ね。

もう一つは「ReadAll」問題。ワタシが読んだドキュメントは「1.16 になって io が整理されたので、もはや io/ioutil はいらんのぢゃ」という趣旨のことを言っていたと理解していた。けど、1.17 で「io.ReadAll」が消えとる…。これは ioutil に戻すしかないね。

てわけで、こんな感じ:

main.go.in (オレオレプリプロセッサで処理される前提)
  1 package main
  2 
  3 import (
  4     "fmt"
  5     "strings"
  6     "strconv"
  7     "os"
  8     "bytes"
  9     "regexp"
 10     "reflect"
 11     "path/filepath"
 12     "io/ioutil"
 13     "math"
 14     "encoding/json"
 15     "encoding/base64"
 16     "github.com/ogier/pflag"
 17     mxj "github.com/clbanning/mxj/v2"
 18     "github.com/CloudyKit/jet/v6"
 19     "github.com/CloudyKit/jet/v6/loaders/multi"
 20 //ENABLE_CLIP BEGIN
 21     "golang.design/x/clipboard"  // github.com/golang-design/clipboard
 22 //ENABLE_CLIP END
 23 )
 24 
 25 type MyOpt struct {
 26     extravars *string
 27     subnl *string
 28     addpath *string
 29 }
 30 var myOpt MyOpt
 31 
 32 func init() {
 33     ousage := pflag.Usage
 34     pflag.Usage = func() {
 35         ousage()
 36         os.Exit(1)
 37     }
 38     myOpt.extravars = pflag.StringP(
 39         "extravars", "v", "", "specify json or xml for the variables")
 40     myOpt.subnl = pflag.StringP(
 41         "replace_rendered_newline", "n", "\n", "replace newline to this")
 42     myOpt.addpath = pflag.StringP(
 43         "addpath", "p", "",
 44         "additional template paths (semicolon separated list)")
 45 
 46 //ENABLE_CLIP BEGIN
 47     err := clipboard.Init()
 48     if err != nil {
 49         panic(err)
 50     }
 51 //ENABLE_CLIP END
 52 }
 53 
 54 func to_num(iv interface{}) interface{} {
 55     v := reflect.ValueOf(iv)
 56     if !v.IsValid() {
 57         panic(fmt.Errorf("invalid value can't be converted to float64"))
 58     }
 59     kind := v.Kind()
 60     if kind == reflect.Float32 || kind == reflect.Float64 {
 61         return v.Float()
 62     } else if kind >= reflect.Int && kind <= reflect.Int64 {
 63         return v.Int()
 64     } else if kind >= reflect.Uint && kind <= reflect.Uint64 {
 65         return v.Uint()
 66     } else if kind == reflect.String {
 67         n, e := strconv.ParseFloat(v.String(), 0)
 68         if e != nil {
 69             panic(e)
 70         }
 71         return n
 72     } else if kind == reflect.Bool {
 73         if v.Bool() {
 74             return 0
 75         }
 76         return 1
 77     }
 78     panic(fmt.Errorf("type: %q can't be converted to float64", v.Type()))
 79 }
 80 func to_int(iv interface{}, base... int) interface{} {
 81     v := reflect.ValueOf(iv)
 82     if !v.IsValid() {
 83         panic(fmt.Errorf("invalid value can't be converted to float64"))
 84     }
 85     kind := v.Kind()
 86     if kind == reflect.String {
 87         var b int
 88         if len(base) > 0 {
 89             b = base[0]
 90         } else {
 91             b = 10
 92         }
 93         s := v.String()
 94         if b == 16 && regexp.MustCompile(`^0[xX]`).MatchString(s) {
 95             s = s[2:]
 96         }
 97         n, e := strconv.ParseInt(s, b, 0)
 98         if e != nil {
 99             panic(e)
100         }
101         return n
102     }
103     return int(to_num(iv).(float64))
104 }
105 
106 func _Read(path string) ([]byte, error) {
107     sp := append([]string{"."}, strings.Split(*myOpt.addpath, ";")...)
108     for _, p := range sp {
109         if f, err := os.Open(filepath.Join(p, path)); err == nil {
110             return ioutil.ReadAll(f)
111         }
112     }
113     return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
114 }
115 
116 func json_loads(jsoncont string) interface{} {
117     var i interface{}
118     json.Unmarshal(([]byte)(jsoncont), &i)
119     return i
120 }
121 
122 func json_load(fn string) interface{} {
123     jsoncont, err := _Read(fn)
124     if err != nil { panic(err) }
125     return json_loads(string(jsoncont))
126 }
127 
128 func xml_loads(xmlcont string) interface{} {
129     i, err := mxj.NewMapXml([]byte(xmlcont))
130     if err != nil { panic(err) }
131     return i
132 }
133 
134 func xml_load(fn string) interface{} {
135     xmlcont, err := _Read(fn)
136     if err != nil { panic(err) }
137     return xml_loads(string(xmlcont))
138 }
139 
140 func make_seq(sss ...float64) []float64 {
141     sta := 0.0
142     ste := 1.0
143     sto := 1.0
144     if len(sss) == 1 {
145         sto = sss[0]
146     } else if len(sss) == 2 {
147         sta = sss[0]
148         sto = sss[1]
149     } else if len(sss) == 3 {
150         sta = sss[0]
151         sto = sss[1]
152         ste = sss[2]
153         if ste == 0.0 {
154             panic("`step` must not be zero")
155         }
156     } else {
157         panic("require arguments, at least one for `stop`")
158     }
159     var res []float64
160     for {
161         if (ste > 0 && sta >= sto) || (ste < 0 && sta <= sto) {
162             break
163         }
164         res = append(res, sta)
165         sta += ste
166     }
167     return res
168 }
169 
170 //ENABLE_CLIP BEGIN
171 func getclip(fmtimg bool) ([]byte) {
172     if !fmtimg {
173         return clipboard.Read(clipboard.FmtText)
174     } else {
175         return clipboard.Read(clipboard.FmtImage)
176     }
177 }
178 //ENABLE_CLIP END
179 
180 func main() {
181     pflag.Parse()
182 
183     var loaders []jet.Loader
184     loaders = append(loaders, jet.NewOSFileSystemLoader("."))
185     for _, p := range strings.Split(*myOpt.addpath, ";") {
186         loaders = append(loaders, jet.NewOSFileSystemLoader(p))
187     }
188     l := multi.NewLoader(loaders...)
189     tmplfn := pflag.Arg(0)
190     set := jet.NewSet(l)
191     tmpl, err := set.GetTemplate(tmplfn)
192     if err != nil {
193         panic(err)
194     }
195     from_fn := func(fn string) interface{} {
196         if filepath.Ext(fn) == ".xml" {
197             return xml_load(fn)
198         } else {
199             return json_load(fn)
200         }
201     }
202     var context interface{}
203     if len(pflag.Args()) > 1 {
204         context = from_fn(pflag.Arg(1))
205     }
206     var variable = make(jet.VarMap)
207     variable.Set("json_loads", json_loads)
208     variable.Set("json_load", json_load)
209     variable.Set("xml_loads", xml_loads)
210     variable.Set("xml_load", xml_load)
211     variable.Set("make_seq", make_seq)
212     variable.Set("float", to_num)
213     variable.Set("int", to_int)
214     variable.Set("sprintf", fmt.Sprintf)
215     variable.Set("abs", math.Abs)
216     variable.Set("ceil", math.Ceil)
217     variable.Set("copysign", math.Copysign)
218     variable.Set("dim", math.Dim)
219     variable.Set("floor", math.Floor)
220     variable.Set("max", math.Max)
221     variable.Set("min", math.Min)
222     variable.Set("round", math.Round)
223     variable.Set("roundtoeven", math.RoundToEven)
224     variable.Set("trunc", math.Trunc)
225     variable.Set("mod", math.Mod)
226     variable.Set("modf", math.Modf)
227     variable.Set("remainder", math.Remainder)
228 //ENABLE_CLIP BEGIN
229     variable.Set("getclip", getclip)
230 //ENABLE_CLIP END
231     variable.Set("base64encode", base64.StdEncoding.EncodeToString)
232     variable.Set("base64decode", base64.StdEncoding.DecodeString)
233     if *myOpt.extravars != "" {
234         outer := from_fn(*myOpt.extravars)
235         for k, v := range outer.(map[string]interface{}) {
236             variable.Set(k, v)
237         }
238     }
239     nl_remove := func (cont string) string {
240         return regexp.MustCompile(`\r?\n`).ReplaceAllString(cont, *myOpt.subnl)
241     }
242     writer := bytes.NewBufferString("")
243     err = tmpl.Execute(writer, variable, context)
244     if err != nil {
245         panic(err)
246     }
247     os.Stdout.Write([]byte(nl_remove(writer.String())))
248 }

にしても ifdef 相当の細かい制御が出来ないのは痛いな…。昔ながらのプログラマならワタシみたいに手札はいっぱい持ってるけど、若いエンジニアがこの答えに辿り着くのって案外難しい気がするよ。なんとかならんもんかね…。(Go が先祖返り的に意図的に必要以上に単純化しているのは、多くは納得がいくんだけど、これはあまり支持出来ない。)