TclX (Extended Tcl)
TclX(Extended Tcl)はNetSoftの Karl LehenbauerさんとMark Diekhansさんによって開発された、 Tcl言語の最も有名な拡張ライブラリのひとつです。 現在はTcl 8.4以降に対応した版が公開されており、 TclX自身のバージョンもまた8.4です。

TclXはTcl言語のみの拡張で、Tkに関する拡張はありません。 次のような機能が追加されています。

  1. 再帰グロブなどの便利なコマンド
  2. コマンドトレースなどのデバッグ機構
  3. 便利なオンラインヘルプならぬランタイムヘルプ
  4. シグナル、ファイル属性の操作などのUNIXシステムプログラミング
  5. キーつきリスト
他にもまだまだありますが、TclXのコマンドを使うと、 TclXをインストールしていない環境では(当然)動作しなくなります。 あえてそのデメリットを我慢してなお使う気になるような機能だけに絞ってみることにします。

●インストールしよう
LinuxのSlackwareなどではOSのCD-ROMに入っていて、 簡単にインストールすることができます。 MS-Windows用にはダウンロードしてすぐに使えるバイナリとして配布されているようですが、そちらは当サイトではまだ試していません。 その他のUNIXマシンの場合は、 TclXのソースプログラムを入手してコンパイル→インストール作業が必要になります。 その際、同じバージョンのTclのソースプログラムの一部が必要になるので、 Tclのソースアーカイブを展開した同じディレクトリから、 TclXのソースアーカイブも展開しておきます。
コンパイル作業自体は下のように簡単です。

% sh configure
% make
% su root
# make install

TclXの拡張が入ったtclshは「tcl」、wishは「wishx」という名前です。 どちらもTclXのコマンドが追加された他は普通のtclshやwishとして使えます。 ただ、私がインストールしようとしたときには、 make installの途中で「/pub/tcl が見つかりません」 という不可解なエラーで異常終了してしまいました。 一応、それでも使えていますけど…

●再帰グロブ
recursive_globは、UNIXのfindコマンドのように、 ディレクトリ構造を下へ下へと再帰的にファイルを探すコマンドです。 また、探しながら何かをするために for_recursive_globというforeachようの制御構造も用意されています。

recursive_globは、

    recursive_glob startdir_list glob_list 
このように使います。例えば、現在のディレクトリから下を検索し、 *.tclという名前のファイルを全部リストにして得るには、
    set files [recursive_glob . *.tcl]
とします。

for_recursive_globはその制御構造版です。 例えば、現在のディレクトリから下を検索し、*.tclという名前のファイルを全部調べ、 その中の 'require' という文字列が含まれる行を行番号つきで全部出力するには、 このようにします。これは便利ライオン。

    for_recursive_glob a {.} {*.tcl} {
	set fp [open $a r]
	for {set n 1} {! [eof $fp]} {incr n} {
	    set buf [gets $fp]
	    if {[regexp {require} $buf]} {
		puts "$a\[$n\]:$buf"
	    }
	}
	close $fp
    }

●コマンドのトレースとプロファイリング
プロファイリングといってもFBIの犯罪捜査ではないので大丈夫。 コマンドのトレースというのは、 Tclコマンドが何か実行されるたびにそのコマンドをログファイルに記録していく機能です。 またプロファイリングというのは、 Tclスクリプトを実行したさい、どのコマンドが何回実行されたか、 どの程度時間がかかったかを記録し、 レポートみたいな形でファイルに出してくれる機能です。 gprofなどのプロファイラを使ったことがある方にはおなじみですね。 これらはスクリプトのデバッグに役立ちます。
では何か既存のTclスクリプトに対してこいつらをかけてみます。

cmdtrace on [open cmd.log w]
profile -commands -eval on

source "freecell.tcl"; # 何か好きなファイルを

cmdtrace off
profile off ProfResult
profrep ProfResult calls prof.log
cmdtraceがコマンドのトレースをするコマンドで、 'on' を指定してから 'off' で中止するまでが記録されます。 'on' を指定するときに、Tcl標準のopenコマンドでファイルを開いたときのid を一緒につけると、結果はそのファイル(上ではcmd.log)に書き出されます。 省略するとstdoutに書き出されます。 profileはプロファイリングをするコマンドで、 使い方はほぼ同様です。'on' のときに -commands や-evalをつけるとチェックが細かくなります。 結果は'off'のときに引数で指定した名前の変数にハッシュ配列の形で格納されます。 こちらは、結果を整形してファイルに出力するのに profrepという専用のコマンドがあります。 2番目の引数は整形の際のソート基準で、「cpu」「calls」「real」 から選べます。最後の引数が出力ファイル名です。

コマンドのトレース結果はcmd.logにこのように書き出されます。

 1:  profile -commands -eval on
 1:  lindex freecellunix.tcl 0
 1:  source freecellunix.tcl
 2:    namespace eval FreeCell {\n\nset ImageData(spade) { R0lGODdhEwATAPc...}
 3:      set ImageData(spade) { R0lGODdhEwATAPcAAAAAAAAAVQAAqgAA/wAkAAA...}
 3:      set CANVASWIDTH 380
 3:      set CANVASHEIGHT 380
 3:      clock seconds
 3:      expr srand(930965642)
 3:      proc ProgramInit {} {\nvariable Score\nset Score(win) 0; set ...}
一番左はプロシージャの階層の番号です。 「expr srand([clock seconds])」のような入れ子のコマンドが、 「clock seconds」と「expr srand(930965642)」 という、2段階で記録されているのがわかります。

プロファイリングの結果はprof.logにこのように書き出されます。

-----------------------------------------------------------------------------
Procedure Call Stack                              Calls  Real Time   CPU Time
-----------------------------------------------------------------------------
FreeCell::random                                    380        158        157
    FreeCell::Init
    source
return                                              380          0          0
    FreeCell::random
    FreeCell::Init
    source
llength                                             175         10         11
    FreeCell::Init
    source
list                                                156          0          0
    FreeCell::DrawCardAtCanvasLocationOf
    FreeCell::DrawAll
    FreeCell::Init
    source
...
...
つまり呼ばれた回数が一番多かったのが FreeCell::random というプロシージャで、それと同じ回数だけ、そのプロシージャから戻る時の returnコマンドが使われるので、この2つがトップに並んでいるわけです。 以下、ここでは呼ばれた回数順にソートされて結果が報告されています。 文字列を空または空白だけからなる文字列と比較するとき、 llengthの値が零かどうかで比べるという、私のクセがよく出ています。

●ランタイムヘルプ
TclXシェルの対話入力モードでは、 オンラインヘルプならぬランタイムヘルプが使えます。 helpというコマンドを使って、おもむろに

  tcl>help lappend
などとTclコマンド、またはTclXコマンドの名前を入力すると、 ヘルプが表示されます。TclXコマンドの使い方がわからないときに非常に役立ちます。

●UNIXシステムプログラミング
TclXには、ファイルの属性操作など高度なUNIXシステムプログラミング用のコマンドが大量に投入されていますが、 全部を使うことはないでしょう。スクリプト言語で頻繁に chrootやselectを使う人はあまりいないでしょうし。 そんな中でも若干使うか、 というのがsignalコマンドを使ったシグナル操作です。 シグナルを飛ばしたり、 シグナルの一種であるalarm機能を使ったりすることもできますが、 とりあえずシグナルの受け取りかたはこんな感じです。

# シグナルの番号は「SIGINT」「INT」「2」のどの形式でもOK
signal trap SIGINT trapINT
signal trap 15     trapTERM

proc trapINT {args} {
    tk_messageBox -message "SIGINT Trapped."
}
proc trapTERM {args} {
    tk_messageBox -message "SIGTERM Trapped."
}

label  .laba -text Hello
button .cmda -text {Send Any Signal} -command exit
pack .laba -side top
pack .cmda -side top

●ハローミスターパイプマン
TclXのUNIX版では、 UNIXで使えるシステムコールが同名のTclコマンドとして提供されています。 それらを(ちょっとだけ)駆使した例として、子プロセスを起動し、 標準入出力を相互につないでデータをやりとりするスクリプトをご紹介します。 せっかくなので、多少は実用になるスクリプトを作りたい、 ということで、子プロセスは下のようなPerlスクリプトにしてみました。 これは標準入力からサーバーマシンのホスト名またはIPアドレスを読み取り、 Perlのgethostbyaddr関数または gethostbyname関数を使って、 ホスト名ならIPアドレスに、 IPアドレスならホスト名に変換して標準出力に出力します。


#! /usr/local/bin/perl
chomp($s = <STDIN>);
if(/^\d/){
    ($a) = gethostbyaddr(pack("C*", split(/\./, $s)), 2);
} else {
    ($a) = join(".", unpack("C*", gethostbyname($s)));
}
print "[$a]\n";
# end.

…えーとこれは本題ではないわけで、 今度は親プロセスたるTclスクリプトのほうですが、
#! /usr/local/bin/tclsh
package require Tclx

pipe fdin1 fdout1
pipe fdin2 fdout2

if {[fork] == 0} {
    dup $fdin1  stdin
    dup $fdout2 stdout
    set cmd "perl ./gethost.pl"
    eval execl $cmd
} else {
    set alt_stdout [dup stdout]
    dup $fdin2  stdin
    dup $fdout1 stdout
    fcntl stdout NONBLOCK 1
    fcntl stdout NOBUF    1

    puts "www.kantei.go.jp" ; # 話題の首相官邸をセレクト(ホントは適当)
    set buf [gets stdin]
    puts $alt_stdout "*** $buf ***"
    puts $alt_stdout "salvaged: [wait]"
}
# end.

このスクリプトはpipe、dup、fork、execl、fcntl の5つのシステムコールを使って、子プロセスを起動してその標準入力、 標準出力をそれぞれ親プロセスの標準出力、標準入力と接続しています。 この方法でなぜ標準入出力が接続できるのかは割愛しますが、 これらのシステムコールと同名のTclコマンドのおかげで、PerlやPythonと同様、 低レベルなシステムプログラミングもこのように可能というわけですね。

●DNSによる名前解決(円満に)
そういえば、Perlにはgethostbyname やgethostbyaddrのような DNS(Domain Name Service)を利用してホスト名とIPアドレスの対応付け (名前解決)を行う関数がありますが、Tclにはその機能はないのでしょうか? おお、ちょうどよいところにいらっしゃった!(仕組んだように) TclXにはそのようなコマンドが提供されているのでした。 すっかり忘れていましたな(やっぱり仕組んだように)


#! /usr/local/bin/tclsh
package require Tclx

while 1 {
    puts -nonewline ">"; flush stdout
    set a [gets stdin]
    if {[eof stdin]} break
    if {[regexp {^[[:digit:]]} $a]} {
        puts [host_info official_name $a]
    } else {
        puts [host_info addresses $a]
    }
}
# end.

これはnslookupコマンドの簡易版みたいな対話型のコマンドラインツールで、 入力されたホスト名とIPアドレスを相互に変換して表示してくれます。 IPアドレスからそれが指すサーバ機の正規のホスト名を知るには、 host_info official_name コマンドを使います。また、そのマシンが持っている別名のリストを知るには、 host_info aliasesコマンドを使います。 逆にホスト名からIPアドレスを求めるには、 host_info addressesコマンドを使います。

拡張レビュー分室 top
(first uploaded 1999/07/03 last updated 2012/05/27, KAZUHISA ESHI - MISUMI URANO)