Tcl」タグアーカイブ

コマンドライン版Everythingを使ってSubversionのワーキングコピーフォルダをリストアップする

最近TortoiseSVNが1.7になって、既存のワーキングコピーを初めて操作するときに自動的に管理ファイルをアップデートするらしい既存のワーキングコピーを手動でアップデートする必要があるらしい ( WC-NG )。未アップデートのワーキングコピーが混在するのは気持ち悪いのでまとめてアップデートしたいと思う。

しかし、僕の場合、ワーキングコピーがいろんなところに散在していて探すのがめんどくさい。それでワーキングコピーのルートフォルダをリストアップするコマンドラインアプリを作りました。

また、複数のプロジェクトをメンテしていてコミットがおろそかになってしまうことが多いんですが、これで列挙すればどこにワーキングセットがあるのか分かりやすくなります。

各ドライブ以下を全検索するのはやってられないので、Everythingの力を借りています。
そのためNTFSフォーマットのドライブ限定になります。

# This script searches all subversion working copy root folders.
# You need Tcl interpreter and Everything command line interface (es.exe).
# Download from this site: http://www.voidtools.com/download.php
# tclsh FindSvnRoots.tcl
array set wcopy {}
set ch [open {|es -i -s -r "^.svn$"}]
 
while {![eof $ch]} {
	if {[gets $ch path] != -1} {
		set parent [file dirname $path]
		set wcopy($parent) 1
	}
}
 
set paths [lsort -dictionary [array names wcopy]]
foreach path $paths {
	array unset wcopy "${path}?*"
}
 
set paths [lsort -dictionary [array names wcopy]]
foreach path $paths {
	puts $path
}

ここからes.exeをダウンロードして、スクリプトの実行フォルダかパスの通ったフォルダに置く必要があります。
http://www.voidtools.com/download.php

※ Tclインタープリタ不要なバージョンも作りました。
これにもes.exeは必要です。

FindSvnRoots 1.0.0.0 (Windows binary)

# 未コミットかどうかも表示できるようにしたい。

2011-10-27 追記

svn status -qコマンドで取得した状態で、" "でないものがあった場合は最初に見つけたものを表示します。

>FindSvnRoots.exe
  C:\src\my\project1
M C:\src\my\project2
A C:\src\my\project2

svnコマンドにパスが通っていない場合は全ての状態が" "になります。

>FindSvnRoots.exe
  C:\src\my\project1
  C:\src\my\project2

FindSvnRoots 2.0.0.0 (Windows binary)

ソースコードは以下。

# This script searches all subversion working copy root folders.
# You need Everything command line interface (es.exe).
# tclsh FindSvnRoots.tcl
# Output format:
# [ ACDIMRX?!~] "Working set folder's full path"
 
set svn_exists [expr {![catch {exec svn --version --quiet}]}]
 
array set wcopy {}
 
set ch [open {|es -i -s -r "^.svn$"}]
 
while {![eof $ch]} {
	if {[gets $ch path] != -1} {
		set parent [file dirname $path]
		set wcopy($parent) 1
	}
}
 
set paths [lsort -dictionary [array names wcopy]]
foreach path $paths {
	array unset wcopy "${path}?*"
}
 
set paths [lsort -dictionary [array names wcopy]]
foreach path $paths {
	set native_path [file nativename $path]
	set s " "
	if {$svn_exists} {
		if {[catch {exec svn status -q $path} str]} {
			puts "E $path : Failed to retrieve local modification : [lindex [split $str \n] 0]"
		}
		set lines [split $str \n]
		foreach line $lines {
			set s [string index $line 0]
			if {$s ne " "} {
				break
			}
		}
	}
	puts "$s $native_path"
}

[Tcl] How to know decimal representation of binary float

I wrote experimental script to convert binary float to decimal.

References:
http://www.h-schmidt.net/FloatApplet/IEEE754.html

http://sandbox.mc.edu/~bennet/cs110/flt/ftod.html

# This script searches all subversion working copy root folders.
# You need Tcl interpreter and Everything command line interface (es.exe).
# Download from this site: http://www.voidtools.com/download.php
# tclsh FindSvnRoots.tcl
array set wcopy {}
set ch [open {|es -i -s -r "^.svn$"}]
 
while {![eof $ch]} {
	if {[gets $ch path] != -1} {
		set parent [file dirname $path]
		set wcopy($parent) 1
	}
}
 
set paths [lsort -dictionary [array names wcopy]]
foreach path $paths {
	array unset wcopy "${path}?*"
}
 
set paths [lsort -dictionary [array names wcopy]]
foreach path $paths {
	puts $path
}

[Tcl] gets or read - which is faster

Usually, I use "gets" to read a text file.
But I tried "read" to know which is really faster.

set exponent 00000001
set mantissa 10001001001101110100110
 
binary scan [binary format B* $exponent] c exponent
set exponent [expr {$exponent - 127}]
 
if {$exponent <= -127} {
	set mantissa "$mantissa"
} else {
	set mantissa "1$mantissa"
}
 
set i 0
set sum 0.0
foreach b [split $mantissa {}] {
	set sum [expr {$sum + pow(2,$exponent-$i)*$b}]
	incr i
}
puts $sum
#=> 1.80555933856e-038

If you read the whole text, "read" is a little faster.
But if you stop reading in the middle of the content, "gets" is more efficient.

"read" uses more memory as the file size increases.

Though "read" is a little bit faster in some situation,
I think "gets" is more efficient in most cases.

Decoding IEEE754 Single Precision Floating Point Number in Tcl - NaN, Infinite etc.

Each version of Tcl recognizes big-endian IEEE754 encoded exceptional single precision floating point numbers like followings.

proc test_gets {f} {
	set ch [open [format {|nkf -s "%s"} $f]]
	while {![eof $ch]} {
	    if {[gets $ch line] != -1} {
	        puts $line
	    }
	}
	close $ch
}
 
proc test_read {f} {
	set ch [open [format {|nkf -s "%s"} $f]]
	set txt [read $ch]
	close $ch
	set lines [split $txt \n]
	foreach line $lines {
	    puts $line
	}
}
 
set filename [file join [pwd] "hoge.txt"]
set bench1 [time {test_gets $filename} 10]
set bench2 [time {test_read $filename} 10]
 
puts $bench1
puts $bench2

# 1162 bytes
# gets => 39963 microseconds per iteration
# read => 27447 microseconds per iteration

# 10350085 bytes
# gets => 6420444 microseconds per iteration
# read => 6246802 microseconds per iteration

ipconfigを呼ばないでMACアドレスのリストを取得する

TclからNICのMACアドレスのリストを取得する場合、ipconfig/allの出力から取り出してたけど、Windowsのバージョンやロケールによって出力が変わるものを使うのはどうも気に食わんかったので、ちゃんとそれ用のWindows APIを使いたいと思っていた。
今回twapiを使えばできることが分かったので、メモしておく。

# IEEE754 Reference
# http://babbage.cs.qc.edu/courses/cs341/IEEE-754references.html
namespace eval IEEE754 {
	variable CONST {
		Quiet-NaN_MIN            FFFFFFFF
		Quiet-NaN_MAX            FFC00001
		Indeterminate            FFC00000
		Signaling-NaN_MIN        FFBFFFFF
		Signaling-NaN_MAX        FF800001
		-Infinity                FF800000
		NegativeNormalized_MIN   FF7FFFFF
		NegativeNormalized_MAX   80800000
		NegativeDenormalized_MIN 807FFFFF
		NegativeDenormalized_MAX 80000001
		-0                       80000000
		+0                       00000000
		PositiveDenormalized_MIN 00000001
		PositiveDenormalized_MAX 007FFFFF
		PositiveNormalized_MIN   00800000
		PositiveNormalized_MAX   7F7FFFFF
		Infinity                 7F800000
		Signaling+NaN_MIN        7F800001
		Signaling+NaN_MAX        7FBFFFFF
		Quiet+NaN_MIN            7FC00000
		Quiet+NaN_MAX            7FFFFFFF
	}
}
 
if {$::tcl_version >= 8.5} {
	proc IEEE754::string_reverse {s} {
		string reverse $s
	}
} else {
	package require struct::list
	package require textutil
	proc IEEE754::string_reverse {s} {
		join [struct::list reverse [textutil::splitn $s 1]] ""
	}
}
 
proc IEEE754::test {} {
	variable CONST
	puts "tcl_patchLevel = $::tcl_patchLevel"
	foreach {name hex} $CONST {
		set name [format %-24s $name]
		binary scan [string_reverse [binary format H* $hex]] f a
		puts "FLOAT: $name $a"
	}
}
 
IEEE754::test

# tcl_patchLevel = 8.4.19
# FLOAT: Quiet-NaN_MIN            -1.#QNAN
# FLOAT: Quiet-NaN_MAX            -1.#QNAN
# FLOAT: Indeterminate            -1.#IND
# FLOAT: Signaling-NaN_MIN        -1.#QNAN
# FLOAT: Signaling-NaN_MAX        -1.#QNAN
# FLOAT: -Infinity                -1.#INF
# FLOAT: NegativeNormalized_MIN   -3.40282346639e+038
# FLOAT: NegativeNormalized_MAX   -1.17549435082e-038
# FLOAT: NegativeDenormalized_MIN -1.17549421069e-038
# FLOAT: NegativeDenormalized_MAX -1.40129846432e-045
# FLOAT: -0                       -0.0
# FLOAT: +0                       0.0
# FLOAT: PositiveDenormalized_MIN 1.40129846432e-045
# FLOAT: PositiveDenormalized_MAX 1.17549421069e-038
# FLOAT: PositiveNormalized_MIN   1.17549435082e-038
# FLOAT: PositiveNormalized_MAX   3.40282346639e+038
# FLOAT: Infinity                 1.#INF
# FLOAT: Signaling+NaN_MIN        1.#QNAN
# FLOAT: Signaling+NaN_MAX        1.#QNAN
# FLOAT: Quiet+NaN_MIN            1.#QNAN
# FLOAT: Quiet+NaN_MAX            1.#QNAN

# tcl_patchLevel = 8.5.9
# FLOAT: Quiet-NaN_MIN            -NaN(7ffffe0000000)
# FLOAT: Quiet-NaN_MAX            -NaN(20000000)
# FLOAT: Indeterminate            -NaN
# FLOAT: Signaling-NaN_MIN        -NaN(7ffffe0000000)
# FLOAT: Signaling-NaN_MAX        -NaN(20000000)
# FLOAT: -Infinity                -Inf
# FLOAT: NegativeNormalized_MIN   -3.4028234663852886e+38
# FLOAT: NegativeNormalized_MAX   -1.1754943508222875e-38
# FLOAT: NegativeDenormalized_MIN -1.1754942106924411e-38
# FLOAT: NegativeDenormalized_MAX -1.401298464324817e-45
# FLOAT: -0                       -0.0
# FLOAT: +0                       0.0
# FLOAT: PositiveDenormalized_MIN 1.401298464324817e-45
# FLOAT: PositiveDenormalized_MAX 1.1754942106924411e-38
# FLOAT: PositiveNormalized_MIN   1.1754943508222875e-38
# FLOAT: PositiveNormalized_MAX   3.4028234663852886e+38
# FLOAT: Infinity                 Inf
# FLOAT: Signaling+NaN_MIN        NaN(20000000)
# FLOAT: Signaling+NaN_MAX        NaN(7ffffe0000000)
# FLOAT: Quiet+NaN_MIN            NaN
# FLOAT: Quiet+NaN_MAX            NaN(7ffffe0000000)

# tcl_patchLevel = 8.6b1.2
# FLOAT: Quiet-NaN_MIN            -NaN(7ffffe0000000)
# FLOAT: Quiet-NaN_MAX            -NaN(20000000)
# FLOAT: Indeterminate            -NaN
# FLOAT: Signaling-NaN_MIN        -NaN(7ffffe0000000)
# FLOAT: Signaling-NaN_MAX        -NaN(20000000)
# FLOAT: -Infinity                -Inf
# FLOAT: NegativeNormalized_MIN   -3.4028234663852886e+38
# FLOAT: NegativeNormalized_MAX   -1.1754943508222875e-38
# FLOAT: NegativeDenormalized_MIN -1.1754942106924411e-38
# FLOAT: NegativeDenormalized_MAX -1.401298464324817e-45
# FLOAT: -0                       -0.0
# FLOAT: +0                       0.0
# FLOAT: PositiveDenormalized_MIN 1.401298464324817e-45
# FLOAT: PositiveDenormalized_MAX 1.1754942106924411e-38
# FLOAT: PositiveNormalized_MIN   1.1754943508222875e-38
# FLOAT: PositiveNormalized_MAX   3.4028234663852886e+38
# FLOAT: Infinity                 Inf
# FLOAT: Signaling+NaN_MIN        NaN(20000000)
# FLOAT: Signaling+NaN_MAX        NaN(7ffffe0000000)
# FLOAT: Quiet+NaN_MIN            NaN
# FLOAT: Quiet+NaN_MAX            NaN(7ffffe0000000)

ちなみにコマンドプロンプト上ではchcpで932以外のコードページを指定してからipconfigすれば英語の出力になる。
これをTclからもできればよかったんだけど、複数のコマンドをパイプ経由で渡すことはできないようだ。

cmd /c "chcp 437 & ipconfig/all"

というのをexecしたりしても、標準出力を受け取ることができなかった。
一時的にバッチファイルを作り、これを実行してもよいが、美しくない。

そういうわけで、上記のテクニックが今のところ一番いいと思う。

In which case vwait never returns

If your Tcl script uses many afters and vwaits, sometimes it happens that vwait never returns even if its target variable is set. I didn't understand why my application get into such a situation.

Here is a sample script that causes first call of vwait never to return.
Run this script in wish.

package require twapi
proc getPhysicalAddresses {} {
	set macs {}
	foreach i [twapi::get_netif_indices] {
		set type [twapi::get_netif_info $i -type]
		array set netif [twapi::get_netif_info $i -type -physicaladdress]
		if {$netif(-type) eq "ethernet"} {
			lappend macs [string toupper $netif(-physicaladdress)]
		}
	}
	return $macs
}
puts [getPhysicalAddresses]

I expect the following output;

cmd /c "chcp 437 & ipconfig/all"

Contrary to my expection, the actual console output is;

enter vwait ::var1
enter vwait ::var2
set var1 go

When I call "set_var2 go" interactively, the output follows,

% set var2 go
go
leave vwait ::var2
leave vwait ::var1
set var2 go

This means that if you call nested vwaits, the first call of a vwait blocks until all of the nested vwait returns.

Even if you understand this characteristics of vwait, you can run into trouble in network programs. I guess this is caused by the following characteristics of a network program.

  • It doesn't know when a response comes.
  • It uses recursive call of vwait when resending a command on a response timeout.
  • A polling loop interrupts normal sequence order.

Sorry to suck at explaining.

I made an application that communicates with multiple I/O devices which have UDP servers.
I designed it in an object-oriented way and encapsulated UDP client sockets in each of the device objects. As the UDP server device doesn't support push-notification, I had to ask their status at a certain intervals: a polling loop for each device. The polling loops are recursive call of afters. They interrupt into the normal command sequence. I guess this causes unexpected vwait call.

My design was successful in the other platforms like Ruby and C# which have built-in preemptive threads. But in Tcl, I had to place a single manager object for highly concurrent parts of the program.

Tcl has many OOP extensions. We can call Tcl as multi paradigm programming language. But as to this kind of thing, Tcl is not truely object-oriented; it forces tight dependencies between objects.

Tcl 8.6 will gain coroutines. I hope it will introduce a true OOP into Tcl.

Tcl/Tk 8.5 Programming Cookbook

暗号化APIを有効にしたSQLite3のTclバインディングをコンパイルする

コンパイルに使用したもの
ActiveTcl 8.4.19.5 http://www.activestate.com/activetcl/downloads
wxSQLite3(wxsqlite3-2.1.2.zip) http://sourceforge.net/projects/wxcode/files/Components/wxSQLite3/
SQLite3ソースコード(sqlite-autoconf-3070701.tar.gz) http://www.sqlite.org/download.html
MinGW(mingw-get-inst-20110530.exe) http://sourceforge.net/projects/mingw/files/Automated%20MinGW%20Installer/mingw-get-inst/

作業フォルダはどこでもいいですが、今回はc:\srcとします。
そこにSQLite3ソースコードとwxSQLite3を展開します。
こんな感じ。

console show
 
rename vwait original_vwait
proc vwait {var} {
	global last_vwait_var
	puts "enter vwait $var"
	set last_vwait_var $var
	original_vwait $var
	puts "leave vwait $var"
}
set last_vwait_var {}
 
proc set_var1 {val} {
	global var1
	puts "set var1 $val"
	set var1 $val
}
 
proc set_var2 {val} {
	global var2
	puts "set var2 $val"
	set var2 $val
}
 
after 1000 {
	after 1000 {
		set_var1 go
	}
	vwait ::var2
}
 
vwait ::var1
set_var2 go

以下のフォルダ内の全てのファイルを
C:\src\wxsqlite3-2.1.2\sqlite3\secure\src\codec-c

ここに上書きコピーします。
C:\src\sqlite-autoconf-3070701\tea\generic

そして、以下のソースコードをテキストエディタで開きます。
C:\src\sqlite-autoconf-3070701\tea\generic\tclsqlite3.c

4行目の"../../sqlite3.c"を

enter vwait ::var1
enter vwait ::var2
set var1 go
leave vwait ::var1
set var2 go
leave vwait ::var2

"sqlite3secure.c"に書き換える。

enter vwait ::var1
enter vwait ::var2
set var1 go

これで、コンパイルの準備はできました。

さて、MinGW shellを起動しましょう。

ディレクトリを移動してコンパイルします。

% set var2 go
go
leave vwait ::var2
leave vwait ::var1
set var2 go

デフォルトではAES128コーデックが使われます。
試してませんが、AES256にしたければ、以下のようにすればよいと思います。

$ ./configure CFLAGS="-DSQLITE_HAS_CODEC -DCODEC_TYPE=CODEC_TYPE_AES256"

wxSQLite3 AES128ならGUIツールのSQLite2009 Proが対応していますが、AES256に対応しているツールはなさそうなので、セキュリティの強化以外の理由でAES256を選択する理由はないと思います。

これで以下のファイルができました。これ単体でTclのパッケージです。

C:\src\sqlite-autoconf-3070701\tea\sqlite3771.dll

直接tclshからloadするか、pkgIndex.tclとともにTcl/libにインストールしてpackage requireすることもできます。

さて、テストしてみましょう。

load sqlite3771.dll Sqlite3

# create plain database file
sqlite3 pdb plain.db

# create encrypted database file
sqlite3 sdb secret.db -key password

# SQL test script
# 1. create table
# 2. populate test data
# 3. execute query
set sql {
	create table users (
		id integer primary key autoincrement not null,
		name text,
		age integer
	);
	insert into users (name, age) values ("山田太郎", 30);
	select * from users;
}
 
pdb eval $sql
#=>; 1 山田太郎 30
pdb close
 
sdb eval $sql
#=>; 1 山田太郎 30
sdb close

# Re-open plain.db
sqlite3 pdb plain.db
pdb eval {
	select * from users;
}
#=>; 1 山田太郎 30
pdb close

# Re-open secret.db without a key
sqlite3 sdb secret.db
sdb eval {
	select * from users;
}
#=>; file is encrypted or is not a database
sdb close

既存のデータベースファイルを暗号化するには、rekeyするか、dumpを取得して
暗号化した新規データベースでrestoreすればOKです。
この作業には、wxSQLite3に付属するコンパイル済みのshellを使用してください。
C:\src\wxsqlite3-2.1.2\sqlite3\secure\aes128\sqlite3shell.exe

追記:Tclからもできます。

load sqlite3771.dll Sqlite3
# Encrypt plain database
sqlite3 pdb plain.db
pdb rekey "password"
pdb close

# Reopen as plain database
sqlite3 pdb plain.db
pdb eval {
	select * from users;
}
#=>; file is encrypted or is not a database

# Decript secret database
sqlite3 sdb plain.db -key password
sdb rekey ""; # specify null string as encryption key
sdb close

# Re-open plain.db
sqlite3 pdb plain.db
pdb eval {
	select * from users;
}
#=>; 1 山田太郎 30
pdb close

最後はちょっとはしょった説明になりましたが、できてしまえば結構簡単です。
tclsqliteの暗号化APIについてのドキュメントはないのですが、ネイティブの関数と基本的に変わりはないと思います。
間違ったコマンドを与えてやるとエラーメッセージに関数リストが出てきたり、
関数に間違った引数を与えてやることで使い方が出てきたりするので、いろいろ試してみるとよいと思います。

(tea) 1 % load sqlite3771.dll Sqlite3
(tea) 2 % sqlite3 sdb secret.db -key password
(tea) 3 % sdb ?
bad option "?": must be authorizer, backup, busy, cache, changes, close, collate, collation_needed, commit_hook, complete, copy, enable_load_extension, errorcode, eval, exists, function, incrblob, interrupt, last_insert_rowid, nullvalue, onecolumn, profile, progress, rekey, restore, rollback_hook, status, timeout, total_changes, trace, transaction, unlock_notify, update_hook, version, or wal_hook

なお、.NET Frameworkから使えるADOプロバイダとしてSystem.Data.SQLite.dllが暗号化に対応していますが、これはRSA-MS Cryptだそうです。名前しか分かりませんがとりあえずAESとは別物です。SQLite Encryption Extension($2,000)は4つのコーデックがコンパイル時に選択できるようです。

SQLite3にformatみたいな関数がないのでTclでやる

1-12みたいな連番文字列を01012に一括変換したかった。別のカラムに分けるのが普通でしょうが、諸事情により。
SQLiteの組み込み関数には日付の操作関数は充実してるけど、文字列操作関数は大したものがない。
それで、Tclでやることにした。

c:\src
├─sqlite-autoconf-3070701
│  └─tea
│      ├─doc
│      ├─generic
│      ├─tclconfig
│      └─win
└─wxsqlite3-2.1.2
    ├─build
    ├─build29
    ├─dbadmin
    │  └─images
    ├─docs
    │  └─html
    ├─include
    │  └─wx
    ├─lib
    ├─samples
    ├─sqlite3
    │  ├─include
    │  ├─lib
    │  └─secure
    │      ├─aes128
    │      ├─aes256
    │      └─src
    │          ├─codec
    │          └─codec-c
    ├─src
    └─website

02012みたいなのは、クォートしないと数値とみなされて2012みたいになってしまうので注意が必要です。
LIKE以外にもGLOBやREGEXPも使えるらしい。

050-で始まる電話番号を持つ人のリストを取得するなど。
select * from users where phone_number REGEXP "^050-.+$"

Tcl/TkでMutexを使った多重起動防止

Tcl単体でやろうとすると、ロックファイルでやりなさいということになるんですけど、その場合Tclの処理系が起動するまでに、別のインスタンスが起動できてしまう可能性があるので、厳密ではないです。たとえば、

package require sqlite3
sqlite3 db ./app.db; # handle and database file
 
db eval {SELECT * FROM squence WHERE field LIKE "%-%"} values {
	set R-V $values(field)
	if {[scan ${R-V} %d-%d R V] == 2} {
		set RRVVV [format %02d%03d $R $V]
		db eval [format {UPDATE squence SET field="%s" WHERE id=%d} $RRVVV $values(id)]
	}
}

とかやると、2重起動してしまいます。

プロセスリストを取得して処理する方法もあります http://goo.gl/K38a 。今まではこれを使っていました。ただ、プロセスリストの取得自体が結構時間かかるので、上記よりましですが確実ではないです。

別の方法として、socketで特定ポートをバインドして、多重起動時にエラーにするという方法があります。singleton application - Tcler's Wiki
ネットワークを使わないのにファイアウォールの例外にするか聞かれたりするのが嫌です。

Mutexを使うと、VBやC#とかでやってるみたいな厳密な多重起動防止対策ができます(Windows限定)。

twapi 2.2.3での実装

package require twapi 2.2.3
twapi::import_commands
 
set appname "My Application"
 
set handle [create_mutex -name $appname]
if {[lock_mutex $handle -wait 1] > 0} {
	tk_messageBox -icon error 
		-title "Startup error" 
		-message "Another instance is running." 
		-type ok
	exit
}
console show

twapi 3.0 正式版での実装

先日twapi3.0の正式版がリリースされました。
3.0では従来通りpackage require するか、dllを1個loadするかが選べるようになりました。
x86版はTcl8.4をサポートしています。
3.0ではAPIの仕様がいろいろ変わって、lock_mutexの返り値がsignalled, timeout, abandonedのいずれかとなっています。

load twapi-x86-3.0.29.dll
twapi::import_commands
 
set appname "My Application"
 
set handle [create_mutex -name $appname]
if {[lock_mutex $handle -wait 1] ne "signalled"} {
	tk_messageBox -icon error 
		-title "Startup error" 
		-message "Another instance is running." 
		-type ok
	exit
}
console show

先に起動してたウインドウを閉じるかユーザに聞く

そういうことをする実験です。実際にはプロセスをkillするとか、ウィンドウの存在を監視して、完全に終了するのを待つとかが必要になると思います。

package require Tk
load twapi-x86-3.0.29.dll
twapi::import_commands
 
set appname "My Application"
 
set handle [create_mutex -name $appname]
if {[lock_mutex $handle -wait 1] ne "signalled"} {
	set ans [tk_messageBox -icon error 
		-title "Startup error" 
		-message "Another instance is running. Kill it?" 
		-type yesno]
	switch $ans {
	yes {
		set hWnds [find_windows -text $appname]
		foreach hWnd $hWnds {
			close_window $hWnd
		}
	}
	no {
		exit
	}
	}
}
 
wm title . $appname
wm protocol . DELETE_WINDOW EXIT
proc EXIT {} {
	after 3000 exit
}

Tcl/Tkのいいところ

半年くらい前まではC#でGUIアプリケーションを作っていたけど、ここ数ヶ月は主にTcl/Tkを使っている。僕がTcl/Tkを使っている理由は、最初にまともなプログラムを書いたのがTcl/Tkからだったからだ。それ以外に必然的な理由はない。以前はプログラマを名乗るのならば、C/C++/Javaあたりは最低やっておかねばという強迫観念にとらわれていた自分だが、最近C#をやって静的型付言語の雰囲気をかじったことで、まあ、必要になったらやったらいいんじゃないかなくらいに思えてきた。

シェアの大きい言語をほとんど使いこなすことなく、Tcl/TkやRubyといったどちらかというとマイナーな言語ばかり使ってきた僕が、Tcl/Tkに客観的な評価を与えられるとは思わないが、5年間というプログラマー生活を共に過ごしてきたこの言語について、実用的観点でよいと思うことを挙げておきたいと思う。

I/Oの抽象化

ファイル、ソケット通信、シリアル通信などがチャネルというハンドルに対する共通のコマンドで操作できる。さらに、VFS(仮想ファイルシステム)といって、たとえばZIPファイルやFTPを普通のディレクトリと同じコマンドで扱えたりする。

wish.exeって便利

Tkアプリケーションで、console showとか書いとくと、アプリケーションを起動したときにシェルウィンドウが開く。ここからなんでもできてしまう。アプリケーションを走らせたまま書き換えることができる。Rubyでもできるけど、インタラクティブなシェルってコマンドライン上で動くirbしかないよね。Ruby/Tkでイベントループ走らせちゃったらもう何もできないし。

マニュアルが読みやすい

MSDNみたいな非人間的な解説じゃない。コマンドのカテゴリ分けが適切で、ひとつひとつのコマンドやオプションに必要かつ十分な説明が正確な英語で書かれている。サンプルスクリプトが少ないのが不満だったが、Tcler's Wikiという膨大なナレッジベースを参照すればいろいろ見つかる。

実用的なライブラリが豊富

およそライブラリというものには言語の機能を組み合わせて複雑な機能を実現してくれるものと、言語ではできないことを可能にしてくれるものがある。前者のバリエーションの豊富さは言語の力を示すものだ。後者は逆かもしれないが、インタープリタ言語には必要なものだ。そしてTclはどちらの種類のライブラリも本当にたくさん揃っていると思う。Perl,PHP,Rubyみたいなパッケージシステムでteapotレポジトリというのがでてきたので、そのうち実用的になっていくことだろう。Windows向けに、ActiveXコントロールをTkに埋め込むライブラリ、COMインターフェース、Win32APIのラッパーライブラリ、DLLのエクスポート関数を利用するライブラリも存在する。

デプロイが簡単

ActiveStateのTclDevKitを使えば、Tclインタープリタ、ライブラリ、ソースコード、フォント、画像など、すべてを単一の実行ファイルに固めて配布でき、ソースコードの隠蔽もできる。僕は複数のファイルからなる合計2万行程度のスクリプトと、ロゴなどの画像、十数種類のパッケージなどを単一ファイルにラップしているが、せいぜい6MB程度にしかならない。.NET Framework 4 client profileはダウンロードするだけで41MBもある。展開したらもっと大きくなるだろう。仕組みはよく分からないが、英語版WindowsXPに日本語フォントがなくてソースコードのコメントが文字化けしていたときも、コンパイルしたTcl/TkのGUIには日本語が表示できていた。

え、これだけ?とか、どれも言語の機能じゃないんですけど、とつっこまれそうなものばかりだな。。。来年あたり8.6が出るみたいだけど、こいつはかなり進化しているらしいですよ。lambda, coroutineに期待。最近は何でも屋さんの傾向は薄れつつあり、言語自体の近代化が進んでいるように思う。JimというTclのアナザー実装はガベージコレクタを持っているらしい。

実はここでは目をつむった悪いところもたくさんあるんですけど、デプロイがここまで簡単なスクリプト言語ってほかに知らないなあ。あと、目が悪いせいか、美しいとか言われてるRubyのソースコードより、Tclのほうが美しく見えてしまうのですよね。

なんか思いついたら追加するかも。