Tcl による CGI プログラミング

出力

Hello World

出力は他の言語で CGI を書く場合と同様に HTTP ヘッダ、空の改行、本体の順で標準出力に送ればよい。 Tcl の CGI の Hello World は次のようなものになる

#!/usr/bin/tclsh
puts {Content-Type: text/html}
puts {}
puts {<html>
<head><title>Hello World!</title></head>
<body><h1>Hello World!</h1><body>
</html>}

HTTP ヘッダ

上の例では HTTP ヘッダを直接書いて出力したが、tcllib の ncgi に入っている ncgi::header コマンドを代わりに使うことも出来る。

ncgi::header ?type? args

type は Content-Type の値を指定する引数で省略すると text/html になる。 他のフィールドを追加するにはこれに続けてフィールド名、フィールド値の順で引数を追加する。

ncgi::header {text/html; charset=euc-jp}\
  Content-Language ja\
  Expires {Thu, 01 Dec 1994 16:00:00 GMT}

上のコマンドは次のような HTTP ヘッダを送ることになる。

Content-Type: text/html; charset=euc-jp
Content-Language: ja
Expires: Thu, 01 Dec 1994 16:00:00 GMT

なお ncgi は CGI プログラミングに使えるコマンドを一通りそろえたもので、 ポピュラーな拡張セットの tcllib の一部である。 tcllib は ActiveTcl のディストリビューションには最初から含まれている。 CGI 支援の拡張パッケージには他に cgi.tcl があるが、 ここでは主に ncgi を使う方法を取り上げていく。

HTML の生成

HTML の生成にも直接 HTML コードを書く方法以外に tcllib の htmlxmlgen/htmlgen を使う方法もある。 また先述の cgi.tcl も HTML 生成コマンドのセットを含んでいる。

これらの方法はここでは詳しくは取り上げないが、 例として示すと htmlgen で HTML を生成する Tcl コードは以下のような感じである (htmlgen のドキュメントから引用)。

set Title "A Simple Example"
html ! {
  head ! {
    title - $Title
  }
  body ! {
    h1 - $Title
    p - As you can see, title and major headline agree.
  }
}

バイナリデータの出力

CGI の出力が HTML のようなテキストでなく、 GIF 画像などのバイナリデータになる場合がある。 例えばアクセスカウンタやウェブリングの CGI は img タグによって呼び出され、画像を出力する。

Tcl では標準出力を含むファイル入出力のコマンド (puts, gets, read など)は、 入力時には外部エンコーディングから内部エンコーディング(UTF-8)へ、 出力時には内部エンコーディングから外部エンコーディングへと、 文字コードの変換を行うようになっている。

しかしバイナリデータの場合こうした変換は不要なので、 fconfigure コマンドで変換を無効にする必要がある。

カレントディレクトリに存在する img.gif を出力する CGI プログラムは以下のようになる。

#!/usr/bin/tclsh
package require ncgi

set f [open img.gif]
fconfigure $f -translation binary
set image [read $f]
close $f

fconfigure stdout -translation binary
ncgi::header image/gif
puts -nonewline $image

フォーム入力の処理

ncgi::query はフォームから送信されたクエリ (または URL のクエリ部分)をそのまま返す。 メソッドが GET でも POST でもここに値が入る。 独自形式のクエリを解析したい場合はこのコマンドの戻り値を使う。

標準的な key-value ペアを切り出すには ncgi::parse を実行する。 これを実行した後は ncgi::value key でキーから値を得ることが出来る。 ncgi::parse は key-value ペアの切り出しだけでなく URL エンコーディングのデコードも行っている。 URL エンコーディングのデコードには単独のコマンド ncgi::decode もあるが、 [ncgi::decode [ncgi::value q]] などとする必要はない。

文字コードの変換

既に述べたように putsgets などのファイル入出力コマンドは、 明示的に指定しない限り内部エンコーディングと外部エンコーディングの間の変換を自動的に行う。

しかし ncgi::parse で取り出した入力には内部エンコーディングへの自動変換はなされていないので、 明示的に変換を行う必要がある。

明示的な文字コードの変換は encoding コマンドを使う。

encoding convertfrom ?encoding? data

このコマンドは引数 data で与えられた文字列を内部エンコーディングに変換するが、 あらかじめ変換元になる文字コードを引数 encoding で指定する必要がある。

文字コードの判定

フォームから送信されるテキストは、 どの文字コードを使って送らなければならないということが決まっているわけではない。 ブラウザによってはフォームが書かれた HTML と同じ文字コードを使って送信してくれるが、 常にそうであることを期待してはならない。

さらに送信内容の文字コードがなんであるかという情報を与えるメタ情報が HTTP ヘッダから与えられるわけでもない(註)。

しかし Tcl での文字コードの変換はあらかじめ変換元の文字コードを知っている必要があるので、 なんらかの方法でこれを補う必要がある。

一つの方法は受け取った内容の中身を解析して文字コードを自動判定する方法である。 Perl の場合、jcode.pl や Jcode.pm に日本語文字コードの自動判定の機能がついているため、 多くの CGI でこの自動判定の方法が採られている。 Tcl の場合、日本語文字コードの自動判定機能を提供する拡張には DPU がある。

もう一つの方法は、あらかじめ決めておいた日本語文字列をフォームから送ってもらうという方法である。 ブラウザがその文字列をどの文字コードで送信したかを判別することによって (決めておいた文字列なのでこの判別は可能なのである) 送信内容の文字コードが分かる。 例えば下のようにフォームの隠し要素として「日本語」という文字列をフォームに埋め込んでおく

<input type="hidden" name="ja" value="日本語">

「日本語」という文字列は各文字コードで次のように表現される。

shiftjis %93%FA%96%7B%8C%EA
euc-jp %C6%FC%CB%DC%B8%EC
iso2022-jp %1B%24BF%7CK%5C8l%1B%28B
utf-8 %E6%97%A5%E6%9C%AC%E8%AA%9E

以下のコードは送信内容の文字コードを判定して変数 charset にエンコーディング名を入れる。 ここで得られた値を encoding コマンドに与えてやればよい。

switch [ncgi::value ja] {
  "\x93\xFA\x96\x7B\x8C\xEA" {set charset shiftjis}
  "\xC6\xFC\xCB\xDC\xB8\xEC" {set charset euc-jp}
  "\x1B\x24BF\x7CK\x5C8l\x1B\x28B" {set charset iso2022-jp}
  "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" {set charset utf-8}
}

註(POST リクエストの HTTP ヘッダについて)

フォームから POST メソッドでデータが送信される場合、 通常 HTTP ヘッダの Content-Type フィールドは以下のようになり、 送信内容の文字コードに関する情報は含まれない。

Content-Type: application/x-www-form-urlencoded

しかし、ブラウザによっては文字コードに関する情報を Content-Type フィールドに含めて送信するものがある。 例えば Lynx では日本語を含む内容を POST で送信すると以下のような Content-Type を送る。

Content-Type: application/x-www-form-urlencoded; charset=euc-jp

こちらでは確認していないが一部の携帯端末にもこのようなヘッダを送るものがあるようだ。 注意すべきことは tcllib の ncgi はこのような文字コード情報つきの Content-Type に対応しておらず、 これを受け取ると未知のヘッダとしてエラーを返すということである。

ファイルのロック

掲示板やアクセスカウンタなどサーバ上にデータを保持する CGI では、 同時に実行された複数の CGI プロセスがぶつかってファイルを壊したりしないように、 個々のプロセスがファイルをロックして占有する必要がある。 Perl では flock 関数によってこれを行う。 Tcl では標準のコマンドとしてファイルのロックは用意されていないが、 拡張の TclX の中に flock コマンドが用意されている。 この拡張は標準ではないがポピュラーな拡張の一つである。 ActiveTcl のディストリビューションには最初から含まれている。

Tcl の flock コマンドは以下のように使用する。

set f [open lockfile r+]
flock -write $f

上の例は書き込みロックである。読み込みロックは次のとおり。

flock -read $f

ファイルのロックを解除するには funlock コマンドを使用する。

funlock $f

しかし明示的に funlock を使用しなくても、 ファイルを閉じたときにロックは外れるようである。

TclX の flock コマンドは使えない OS もある。 例えば Windows 9x がそうである(Windows NT 系列では使用できる)。 flock が使えるかどうかは infox have_flock で確かめられる。 これは flock が定義されているときに 1、定義されていないときに 0 を返す。

ここでは取り上げないが Perl の CGI でよく行われるのと同様に mkdir や rename などのアトミックなファイル操作を利用してロックを実装することも可能だろう。 この方法の利点はプラットフォーム依存性が低いことと、Pure Tcl のみで実装できることである。 プラットフォーム非依存のファイルロックというテーマに関しては The Tcler's Wiki の How do I manage lock files in a cross platform manner in Tcl でいくつかの議論を読むことが出来る。

リモートホスト情報の取得

CGI プログラミングをする上でクライアントのリモートホストの情報を利用したい場合がある。 例えば特定のホストからのアクセスを規制したい場合などである。 サーバに Apache を使用していれば IP アドレスの値が環境変数 REMOTE_ADDR から取得できる。 Tcl では $env(REMOTE_ADDR) のようにして環境変数を取り出す。 さらに Apache の設定次第では環境変数 REMOTE_HOST にホスト名が与えられているかもしれない。 そうでない場合は IP アドレスからホスト名を取り出す必要がある。 このためには TclX に含まれている host_info コマンドを使えばよい。 このコマンドは Perl での gethostbyaddr 関数や gethostbyname 関数に相当する機能を提供する。

ホスト名を取り出す場合はサブコマンド official_name を以下の書式で使用する。 引数 host には $env(REMOTE_ADDR) で得た値などを与える。

host_info official_name host
http://nul.jp/2002/tcl_cgi
文書作成: 2002-12-18
最終更新: 2003-03-02