[Common Lisp] ros templateの紹介

cl-web-2d-game *1 のようなWebアプリ向けのライブラリなどを作っていると、使うまでに色々とサーバ側の設定コードが必要で、中々気軽にプロジェクトを起こせなかったりします(単にインタフェースが悪いのではという議論は置いておきます)。そうした課題の解決方法としてプロジェクトテンプレートがあると思います。Common Lispにも汎用的なものとしてはCL-Projectがありますし、WebフレームワークであるCaveman2にもテンプレート(skelton)が用意されています(CL-Projectベース)。ただ、自分で気軽にテンプレートを量産したり、もしくは人の書いた色々なテンプレートを使ったりしたいと思うと、テンプレートを統一的に管理する仕組みが欲しくなります。

そういうことをするのであればRoswellの周りだろう…と思って、まずは既存のテンプレートシステムがないのかと見てみると、ros templateというサブコマンドがあることが分かりました。ただ、ほぼアンドキュメントな状態で、ソースを見ながら使い方を探る必要がありました。また、使うにあたってこういう機能も欲しいというものもあったので、ポツポツとプルリク出したりしてました。その辺りも含めて使い方の紹介をする記事です。

目次

コマンドの一覧

ros templateコマンドの一覧は空でコマンドを打てば(もしくはros template helpで)下記のように見ることができます。他、ドキュメントとしてはdocuments/ros-template.mdがあります。プルリク出してクイックスタートを追加したので少し充実しました。

$ ros template
Usage: ros template [subcommand]
init            Create new template
deinit          Remove a template
list            List the installed templates
checkout        Checkout default template to edit.
add             Add files to template.
cat             Show file contents
edit            Edit file contents
rm              Remove (delete) files from template.
delete          Remove (delete) files from template.
type            Set template type for a file.
chmod           Set mode for a file.
rewrite         Set path rewrite rule for a file
export          Export template to directory
import          Import template
help            Print usage and subcommands description

なお、deinit, export, importは最近プルリクを入れてもらったものです。またeditも最近入ったものなので、これらの利用には最新版(master)が必要です。

基本的な利用方法

テンプレートの作成やテンプレートエンジンの適用方法など基本的な使い方を見ていきます。

テンプレートを作成する

まずはinitサブコマンドで空のテンプレートを作成します。

$ ros template init sample

特に出力はありませんが、checkoutサブコマンドを空で打ってみると確かに作成されていることが分かります。

$ ros template checkout
current default is "default"

candidates:
default
sample # ← これ

作成したテンプレートはros init <template名> <プロジェクト名>のようにして利用することができます。ただし、まだ空なので何もできません。

$ ros init sample some-project
; compiling file "/root/dev/roswell/lisp/util-template.lisp" (written 07 FEB 2018 11:55:59 AM):
# ~以下略~
$ ls
# まだ何もない

ファイルの追加

空のままではしょうがないので、ファイルの追加を行っていきます。

ほとんどのサブコマンドは第1引数に対象とするテンプレート名をとります。ただし、事前にcheckoutサブコマンドでテンプレートを指定しておくと第1引数を省略できます。例えば下記のようにすると、以降ファイルの追加や削除、その他の操作はsampleテンプレートに対してなされるようになります(なお、defaultテンプレートに実体はありません。したがってcurrent default is "default"は未選択と同義です)。

$ ros template checkout sample
$ ros template checkout # デフォルトがsampleになっていることを確認
current default is "sample"

candidates:
default
sample

次にファイルの追加ですが、ひとまず追加するファイルtestを適当に作っておきます。

$ echo "Hello Template!!" > test

addサブコマンドで今作ったファイルをテンプレートに追加します(フォルダの指定はできません)。

$ ros template add test

listサブコマンドでテンプレートの内部を概観してみます。すると、先ほど指定したtestが無事追加されていることが分かります。copyについては後で触れます。

$ ros template list
      copy  test

また、catサブコマンドで中を見てみると、確かに先ほど作成したファイルと同じ内容であることが分かります。

$ ros template cat test
Hello Template!!

さて、ここで改めてros initコマンドでテンプレートを起こしてみます。

$ mkdir sample
$ cd sample/
$ ros init sample some-project
$ ls
test
$ cat test
Hello Template!!

以上で、ひとまず自作テンプレートからプロジェクトを作成することができました。

テンプレート変数の利用

ファイルを追加して取り出せるようにはなったものの、変数を利用した部分的な書き換えることができないことにはテンプレート機構としては不十分です。

ros templateではDjulaをテンプレートエンジンとして利用した書き換えをサポートしています。Djulaは中々機能が豊富でほとんど把握できていないのですが…ここでは"{{ variable }}"といった形式で変数を埋め込むことができるという所だけ抑えておけば十分です。

ファイル名とファイル内容では変数の適用方法が異なるのでそれぞれ見ていきます。

ファイル名への変数適用:ros template rewrite

ファイル名については、rewriteサブコマンドを使うことで、リライトルールに変数を埋め込むことができます。

例えば、先ほどのtestというファイルを<プロジェクト名>.txtという形式で出力したい場合は次のように設定します。

$ ros template rewrite test "{{ name }}.txt"
$ ros template list # ルールが設定されていることの確認
      copy  test -> "{{ name }}.txt"

nameはデフォルトで利用可能な変数でros init <template> <project name>としたときに<project name>が入ります。デフォルトで利用できる変数は下記の4つです。

  • name: プロジェクト名
  • author: 作者名。git configから拾われます(なければ$(whoami)
  • email: メールアドレス。git configから拾われます(なければ$(whoami)@$(hostname)
  • get_universal_time: 実行時点の時間。[get-universal-time`](http://clhs.lisp.se/Body/f_get_un.htm)関数の結果です

さて、実際にros initすると、リライトルールに沿ってファイル名の書き換えが行われていることが分かります。なお、リライトルールにdoc/{{ name }}.txtのようにフォルダパスを含めると、フォルダを作成した上でその配下に置いてくれます。

# ※以降、ros initは適当な空ディレクトリで実行しているものとします
$ ros init sample sample-project
$ ls
sample-project.txt
$ cat sample-project.txt
Hello Template!!

ファイルの中身に対する変数適用:ros template type

次にファイルの中身での変数適用を見るため、まずはtestファイルを次の内容に更新しておきます。

$ cat<<EOF  > test
Hello {{ sample }}!!
name: {{ name }}
author: {{ author }}
email: {{ email }}
universal time: {{ universal_time }}
EOF
$ ros template add test # testファイルを上書き
$ ros template cat test
Hello {{ sample }}!!
name: {{ name }}
author: {{ author }}
email: {{ email }}
universal time: {{ universal_time }}

name, author, email, universal_time上記で触れたようにデフォルトの変数として利用できます。sampleのような独自の変数はros initの引数として--sample value(間に"="を入れるのはNG)のようにして指定できます(ファイルのリライトルールでも同じように独自の変数を利用できます)。

実際に試してみますが・・・

$ ros init sample some-project --sample Ikaruga
$ ros template cat test
Hello {{ sample }}!!
name: {{ name }}
author: {{ author }}
email: {{ email }}
universal time: {{ universal_time }}

このままでは変数は適用されません。ここで関係してくるのが、先ほどlistサブコマンドの表示の中で説明を飛ばしたcopyです。

$ ros template list
      copy  test -> "{{ name }}.txt"

これはtypeサブコマンドで指定できるもので、copydjulaの2種類があります。デフォルトのcopyはその名の通りファイルをそのままコピーします。一方のdjulaは、単にコピーするのではなくDjulaで処理をしたものを書き出します*2

$ ros template type djula test
$ ros template list
      djula test -> "{{ name }}.txt"

この状態で改めてros initしてみると、意図通り変数の書き換えが行われました。

$ ros init sample some-project --sample Ikaruga
$ ros template cat test
Hello Ikaruga!!
name: some-project
author: eshamster
email: hamgoostar@gmail.com
universal time: 3727259379

なお、デフォルトのタイプはcopyになっていますが、テンプレートごとに変更することもできます。そのためには、typeサブコマンドにファイル名を与えずに実行します。設定したデフォルトタイプは、以降に新規追加するファイルに影響します*3

$ ros template type # 引数なしで現在の設定を確認
current default type is "copy"
$ ros template type djula
$ ros template type
current default type is "djula"

テンプレートのエクスポート・インポート

ros templateは基本的にテンプレートの情報を内部的に管理するように作られています。そのため、Gitでテンプレートを管理したかったり、それを人に配布したかったりといった用途では少々不便です。そこで、テンプレートの実体をローカルに取り出したり、逆にローカルのテンプレートを一式取り込むためのサブコマンドが、exportimportになります。

まず、exportサブコマンドは指定した(もしくはチェックアウトしている)テンプレートを一式ローカルフォルダに持ってきます。なお、同名のファイルが存在する場合は上書きします。一方で、テンプレート内に存在しないファイルがあった場合は単に無視します。

# 適当な空のディレクトリ
$ ros template checkout sample
$ ros template export
$ ls
roswell.init.sample.asd  test
$ cat test
Hello {{ sample }}!!
name: {{ name }}
author: {{ author }}
email: {{ email }}
universal time: {{ universal_time }}

sampleテンプレート内のファイルtestを取り出せたことが分かります。なお、roswell.init.sample.asdはテンプレートの管理実体 兼 作成スクリプト片です。中身は次のようになっていて、*params*内でパラメータリストとして様々な情報を管理しています。

$ cat roswell.init.sample.asd
(DEFPACKAGE :ROSWELL.INIT.SAMPLE
  (:USE :CL))
(IN-PACKAGE :ROSWELL.INIT.SAMPLE)
(DEFVAR *PARAMS*
  '(:COMMON (:DEFAULT-METHOD "djula") :FILES
    ((:NAME "test" :METHOD "djula" :REWRITE "{{ name }}.txt"))))
(DEFUN SAMPLE (_ &REST R)
  (ASDF/OPERATE:LOAD-SYSTEM :ROSWELL.UTIL.TEMPLATE :VERBOSE NIL)
  (FUNCALL (READ-FROM-STRING "roswell.util.template:template-apply") _ R
           *PARAMS*))

そして、importサブコマンドは指定されたフォルダ内のroswell.init.xxx.asdに従ってテンプレートを作成します。このとき、同名(名称はroswell.init.xxx.asdxxxを抽出)のテンプレートがあった場合は上書きされるので注意が必要です。

# まだテンプレートがない別のマシンにexportしたファイルを持ってきた想定
$ ros template checkout
current default is "default"

candidates:
default
$ ls downloaded/
roswell.init.sample.asd  test
$ ros template import downloaded/
$ ros template list sample
0600  djula test -> "{{ name }}.txt"

このように、export, importサブコマンドを使うことで、自分で作ったテンプレートをGitで管理したり、人の作ったテンプレートを落としてきて試してみる、ということが(それなりに)気楽にできるようになりました。

その他

その他のコマンド

ここまでで説明していないコマンドには下記があります…が名前と説明からおおむね推測がつくと思いますので割愛します。

deinit          Remove a template
edit            Edit file contents
rm              Remove (delete) files from template.
delete          Remove (delete) files from template.
chmod           Set mode for a file.

余談:テンプレートの実体の管理場所

余談ですが、作成したテンプレートは~/.roswell/local-projects/templates/<テンプレート名>というフォルダで管理されます。

追加したファイルは同フォルダ内の<テンプレート名>-templateフォルダに入っています。下記のようにファイル名はエンコーディングされています。

$ ls -F ~/.roswell/local-projects/templates/sample/
roswell.init.sample.asd  sample-template/
$ ls ~/.roswell/local-projects/templates/sample/sample-template/
%38%2T%37%38
$ cat ~/.roswell/local-projects/templates/sample/sample-template/%38%2T%37%38
Hello {{ sample }}!!
name: {{ name }}
author: {{ author }}
email: {{ email }}
universal time: {{ universal_time }}

*1:まだREADMEすら書いていない…

*2:rewriteサブコマンドとは引数が逆順で少々戸惑いますが、rewriteは常に1ファイルずつ処理する、一方typeは複数を同時に処理する場合がある、ということによる差だと思われます

*3:デフォルトタイプの変更機能は最近プルリクしたものなので最新版が必要です