miniblog3 は100行強のプログラムで小規模の部類と言えるが、CGI という性格上そのうちの一定の割合が HTML 出力に費やされている。本システムでは簡素なデザインのものを例示しているため あまり長いHTMLではないが、凝った装飾を施したい場合は それなりに長いHTML文を出力することになる。 また、シェルスクリプトの中に違う文法のものが交じるのは、 文法確認の見通しがよくない。
そこで、一定量以上のHTML文はテンプレート化して外部ファイルに納め、 テンプレートの必要な部分のみを置換して出力するような構造に変えて、 プログラムとHTMLのデザインの独立性を高める工夫の方法を示す。
テンプレート化したものから実際の文書テキストを生成するためのフィルタとして m4 を利用する方法を示す。
m4は汎用的な使用を前提としたマクロプロセッサで、 元となるテキストファイルの中から、指定したキーワードを 対応する置換語に変換する機能を持つフィルタプログラムである。 単語置換はマクロ展開として処理されるもので、マクロの定義はコマンドラインでは -Dname=value の形式で指定する。 この指定により元テキスト中で単語として独立して存在している name すべてを value に置換する。簡単な例を見てみよう。 元テキストとして以下の m4sample.txt を用意する。
The quick brown fox jumps over the lazy dog.
酒田 さかた
象潟 きさかた
このテキストファイルを入力として以下のように m4 を起動する。
m4 -Dthe=ざ -Djump=じゃんぷ -Dさかた=sakata m4sample.txt
The quick brown fox jumps over ざ lazy dog.
酒田 さかた
象潟 きさかた
3つのマクロを定義しているが、これによって置き換えが起こっているのは英単語 the だけである(The には反応していない)。GNU m4、BSD m4 いずれもマルチバイト文字に対しては置換は発生しない。 マクロとして認められているのはシェルパターン「[a-zA-Z_][a-zA-Z0-9_]*」 にマッチする文字並びである。 また、 -Djump=じゃんぷ からは jumps の置き換えが発生しないように単語としての一致が求められる。
m4 に展開させたいマクロ定義は入力テキスト自身にも埋め込むことができるほか、 テキスト中で関数的に機能するマクロが標準で定義されている。 たとえばコマンドラインで -Dfoo=bar と指定する代わりに、組み込みマクロ define を用いて入力テキスト中に「define(`foo',`bar')」と記述してもよい。 その他、テンプレートファイルからの置換に使えそうな組み込みマクロを以下に示す。
マクロ | 働き |
---|---|
dnl | その位置から行末までと改行文字を削除する。 |
define(name, value) | name というマクロを value に展開するものとして定義する。マクロは関数的に呼び出すこともでき、 name(a,b,c) のように引数つきで呼び出すと、value では $1, $2, $3 でそれぞれ置き換えることができる。 |
ifdef(name, yes, no) | name マクロが定義されていたら yes に置換、 未定義なら no に置換する。 |
ifelse(a, b, yes, ...) | aと b が同じ文字列なら、yes に置換、 そうでなければ最初の3つの引数を捨てて同じ処理を繰り返す。 残った引数が1個ならその値を返す。 |
include(name) sinclude(name) |
name で指定されるファイルの内容に置換する。 ファイル内に含まれるマクロも同様に展開する。sinclude はエラーが出ても無視する。 |
esyscmd(cmd) | cmd をシェル経由で実行して得られた出力に置換する。 出力中に含まれるマクロは置換される。 |
syscmd(cmd) | cmd をシェル経由で実行し何にも置換しない。 実行したコマンドが出力したものは m4 の処理を経ることなくそのまま垂れ流される。 |
m4 のマクロ展開は、展開するものがなくなるまで繰り返し行なわれる。 以下の例を見ると分かるだろう。
echo foo abcfoo | m4 -Dfoo=bar -Dbar=baz -Dbaz=bazzz
bazzz abcfoo
echo に与えた最初の単語 foo は -Dfoo=bar によってまず bar に置換され、その bar が次の -Dbar=baz で baz に置換され、さらに baz が最後の -Dbaz=bazzz によって bazzz に置換される。
マクロ展開されたくない単語は m4 の規則に則ってクォートする必要がある。 デフォルトではバッククォート(`)からシングルクォート(') までがクォートされる(以下の実行例の出力結果の出てくるタイミングは起動する m4 の種別によって多少異なる)。
m4 -Dfoo=BAR
foo `foo'
BAR foo
foo `abc
BAR abc
foo bar' foo 'foo
foo bar BAR 'BAR
クォートは行を越えられる。バッククォートが m4 に食われるので、もしバッククォートが必要なテキストを処理したい場合には m4 用のクォート文字列を変えることで対処する。組み込みマクロ changequote(begq, endq) でそれぞれクォート開始文字列、 クォート終端文字列を指定する。以下のようなテキストファイル chqoute.txt を用意する。
define(`foo', `bar')
changequote(`..(', `..)')dnl
foo `foo' .(foo.) ..(foo..)
m4 chquote.txt
bar `bar' .(foo.) foo
最初の define で foo を bar に展開するよう定義し、 次の changequote でクォート文字列を ..( と ..) に変更している。 3行目は組み込みマクロは何もなく `foo' もクォートの意味が消えてマクロ展開が発生している。 なお、出力の1行目が空行となっているのは chquote.txt の1行目の末尾にある改行が出力されているためで、 このような出力を抑止するために dnl を利用しているのが2行目である。
シェルスクリプトからHTML文書を出力するときに、 見本となるテンプレートファイルを作成しておき、m4 で内容を変更しつつ生成するための注意点を示す。
<!DOCTYPE html>
<html lang="ja">
<head><title>_TITLE_</title></head>
<body>
<h1>_TITLE_</h1>
_CONTENT_
</body>
</html>
アンダースコアに囲まれた単語がマクロ展開される予定のもので、 上記の例では _TITLE_ と _CONTENT_ がそれに該当する。 ここでは単純に以下の規則で置き換えるものとする。
実用時には _CONTENT_ を生成するコマンドは ユーザの入力を元にbodyとなるHTML文を生成するものになろうが、 ここでは簡単に read で読み取った1行を <p> と </p> で括ったパラグラフにするだけのものを作った例を示す。
#!/bin/sh
templ=basic.m4.html
title=`date +%F`
echo "何か一言どうぞ(最後にC-d): " 1>&2
message=`cat`
content="<p>$message</p>"
m4 -D_TITLE_="$title" -D_CONTENT_="$content" $templ
_TITLE_ は date コマンドの出力に置き換えられ、_CONTENT_ は標準入力で読み取ったすべてを p 要素で括ったものに置き換えられるように作ったつもりのものである。 まずは「素直な」実行例を示す。
./m4html.sh
何か一言どうぞ(最後にC-d):
やっほー
foo bar
<!DOCTYPE html>
<html lang="ja">
<head><title>2017-10-24</title></head>
<body>
<h1>2017-10-24</h1>
<p>やっほー
foo bar</p>
</body>
</html>
この実行例のように改行を含む文字列も期待どおりにマクロ展開されるので 長さは心配しなくてもよい。しかし m4 の仕様上次のような「イタズラ」も可能である。
./m4html.sh
何か一言どうぞ(最後にC-d):
世界まる見え!
syscmd(ls /)
<!DOCTYPE html>
<html lang="ja">
<head><title>2017-10-24</title></head>
<body>
<h1>2017-10-24</h1>
<p>世界まる見え!
altroot etc media opt sys
archive ext mnt proc tmp
bin home mnt2 r usr
boot include netbsd rescue var
boot.cfg kern netbsd.altq root work
cdrom lib netbsd.generic sbin x
dev libdata netbsd7 service y
emul libexec onetbsd stand
</p>
</body>
</html>
入力データに m4 のマクロを含む文「syscmd(ls /)」を入れると、 それもマクロ展開される。 マクロの展開先にユーザが入力した文字列をそのまま入れることは 任意のコマンド実行を許すことを意味する。それ以上の展開をさせないためには syscmd のようにそれ以上展開の起きないマクロを介すようにする。 たとえば、m4 呼び出しの部分:
m4 -D_TITLE_="$title" -D_CONTENT_="$content" $templ
これを以下のように変える。
syscmd経由の場合
echo "$content" \
| m4 -D_TITLE_="$title" -D_CONTENT_="syscmd(cat)" $templ
このようにすることで、マクロ展開を1段のみに制限できる。 上記の「イタズラ」例の入力をこれに与えたときの出力は以下のようになる(body 部分のみ抜粋)。
<body>
<h1>2017-10-24</h1>
<p>世界まる見え!
syscmd(ls /)
</p>
</body>
SQLite3データベースから取り出したものをテンプレート化した HTML で出力する流れに翻って考察すると、データベースから文字列を取得するときは sqlite3 の出力モードをHTML(.mode html)にして文字エスケープを施したうえで、m4 のマクロ展開が1段になるようにする。以下のCSVファイルを用意して確認しよう。
number,string
1,普通の文字列
2,"m4で意味を持つ syscmd(ls /)"
3,"HTMLで意味を持つ<small>あいう</small>"
これをSQLite3のデータベースに取り込む。
sqlite3 -csv -header itazura.sq3
.import itazura.csv ita
select * from ita;
number,string
1,"普通の文字列"
2,"m4で意味を持つ syscmd(ls /)"
3,"HTMLで意味を持つ<small>あいう</small>"
実行例のようにテーブルを作成する前に CSV インポートすると、CSV ファイルの1行目がカラム名に採用される。
これをHTML文書として出力するために以下のテンプレートファイル itazura.m4.html を利用する。
<!DOCTYPE html>
<html lang="ja">
<head><title>_TITLE_</title>
<meta charset="utf-8">
<style type="text/css">
<!--
table, tr, td {
border: navy solid 2px; padding: 0 1ex; border-collapse: collapse;}
-->
</style></head>
<body><h1>_TITLE_</h1>
<table>_CONTENT_</table>
</body></html>
これをベースに、_TITLE_ と _CONTENT_ を置換して HTML 文書を作成するシェルスクリプトを以下のように作成する。
#!/bin/sh
db=itazura.sq3
templ=itazura.m4.html
sqlite3 -html $db "SELECT * from ita;" \
| m4 -D_TITLE_="データ一覧" -D_CONTENT_="syscmd(cat)" $templ
実際に起動して生成されるHTML文書を確認する。
./itazura-dump.sh
<!DOCTYPE html>
<html lang="ja">
<head><title>データ一覧</title>
<style type="text/css">
<!--
table, tr, td {
border: navy solid 2px; padding: 0 1ex; border-collapse:collapse}
-->
</style>
<meta charset="utf-8">
</head>
<body>
<h1>データ一覧</h1>
<table>
<TR><TD>1</TD>
<TD>普通の文字列</TD>
</TR>
<TR><TD>2</TD>
<TD>m4で意味を持つ syscmd(ls /)</TD>
</TR>
<TR><TD>3</TD>
<TD>HTMLで意味を持つ<small>あいう</small></TD>
</TR>
</table>
</body>
</html>
: これを html ファイルに落とし、ブラウザで確認してみる。
./itazura-dump.sh > output.html
firefox output.html
ブラウザ出力は以下のようになる。
ブラウザで確認した出力
このように m4 マクロの展開の深さの制御と SQLite3 の HTML 出力モードの組み合わせで、 利用者からの入力文字列「そのまま」でのWebページ出力が可能となる。 今後の作成スクリプトではこのような方式で、まとまった量の HTML 記述をテキストファイルに別保存する形式としていく。
m4でのマクロ展開を1段に制限しつつ、テンプレートファイルを 最終出力に変換する流れをおさらいしよう。
以下のようなテンプレートファイル msg.m4 がある。
_WHO_ さんからのメッセージ
-------------------------------------------------------
_MESSAGE_`'dnl
-------------------------------------------------------
標準入力から、「名前」、「メッセージ」に相当する文字列を読み取り、 それぞれを msg.m4 の _WHO_、_MESSAGE_ の置換先としつつ msg.m4 をマクロ展開処理するようなスクリプト msg.sh を作成せよ。 ただし、入力値にどんなm4マクロがあっても展開されないようにせよ。
実行例を1つ示す。
./msg.sh
お名前を入力してください: たろう/Tallow
メッセージを入力してください(行頭 C-d で終端)
ゴールの次にはスタートがあります。
倦まず弛まず元気に進んでください。
include(`/etc/passwd')
[C-d]
たろう/Tallow さんから 愛 のメッセージ
-------------------------------------------------------
ゴールの次にはスタートがあります。
倦まず弛まず元気に進んでください。
include(`/etc/passwd')
-------------------------------------------------------
マクロの多重展開抑止には本文にあるように syscmd マクロを利用する。
名前の入力は1行なので read で、メッセージの入力は EOF までなので cat で行なう。
2つの入力値をテンポラリファイルに落として、 そのファイルを cat するような syscmd マクロ(m4)を呼べばよい。 テンポラリファイルを安全に作成し削除することにも注意する。
#!/bin/sh
tmp1=`mktemp /tmp/msg-1-$$.XXXXXX` # テンポラリファイル作成と
tmp2=`mktemp /tmp/msg-2-$$.XXXXXX` # 削除トリガの登録(trap)
trap "rm -f $tmp1 $tmp2" INT HUP QUIT TERM EXIT
m4src=msg.m4 # m4用テンプレートファイル
printf 'お名前を入力してください: '
read name
printf '%s' "$name" > $tmp1
echo 'メッセージを入力してください(行頭 C-d で終端)'
cat > $tmp2
m4 -D_WHO_="syscmd(\`cat $tmp1')" -D_MESSAGE_="syscmd(\`cat $tmp2')" $m4src
置換単語が1個の場合はテンポラリファイル不要で、標準入出力を利用して
echo 置き換え後の単語 | m4 -D_MACRO_="syscmd(\`cat')" source.m4
とできるが、2個以上の場合は標準入出力では足りないため、 テンポラリファイルが必要となる。もっとも、置き換え後の単語が ユーザからの入力値でなく、m4マクロ展開が起きないことが確実なら syscmd+cat 経由でなく、m4 -Dマクロ=`値' で直接定義して構わない。 たとえば、名前入力で得たシェル変数 name から特殊記号を除去すれば以下のように m4 にそのまま渡せる。
read name
name=$(echo "$name" | tr -d "\`'")
: ${name:?"英数字日本語を正しく入れてください"} # "" ならエラー終了
cat | m4 -D_WHO_="\`$name'" -D_MESSAGE_="syscmd(\`cat')" $m4src