Tclの使い方

最新版は以下に記載しています。
https://hana-shin.hatenablog.com/entry/2023/04/07/212030

#1 はじめに
Expectを使って業務を自動化したいと思っています。
Expectは、内部でTcl(Tool Command Language)を呼び出すことができます。
[Wiki](https://en.wikipedia.org/wiki/Expect)によると、ExpectはTclの拡張だそうです。
そのため、まず、Tclの使い方を理解するところから始めました。
Tclはスクリプト言語です。Tclは"ティクル"と発音するようです。

#2 環境
VMware Workstation 15 Player上の仮想マシンを使いました。
仮想マシンの版数は以下のとりです。

```console:CentOSの版数
[root@server ~]# cat /etc/redhat-release
CentOS Linux release 7.6.1810 (Core)
```

```console:カーネル版数
[root@server ~]# uname -r
3.10.0-957.el7.x86_64
```

```console:tclの版数
[root@server ~]# rpm -qa|grep tcl
tcl-8.5.13-8.el7.x86_64
```


#3 Hello world

##3.1 標準出力への出力方法(puts)
puts関数を使って、指定した文字列を標準出力に出力してみます。
なお、`stdout`は省略可能です。

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh
puts "Hello world!"
puts stdout "Hello world!!"
```

```console:実行権付与
[root@server ~]# chmod 700 test.exp
```

```console:実行結果
[root@server ~]# ./test.exp
Hello world!
Hello world!!
```


##3.2 標準入力からの入力方法(gets)

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

while {1} {
  set str [gets stdin]
  puts stdout $str
}
```

```console:実行結果
[root@server ~]# ./test.exp
111   <= キーボードから111を入力する
111
222   <= キーボードから222を入力する
222
^C^C^C
```
`^C`を押下しても終了しない。。。
`^C`押下のあと`Enter`キーを押下すると終了する。
なんでだろう。。

#4 変数の使い方(set)

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set var 123
puts $var

set var abc
puts $var
```

```console:実行結果
[root@server ~]# ./test.exp
123
abc
```


#5 制御文の使い方(if, elseif, else文)
##5.1 if文の使い方

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set var 1

if {$var == "1"} {
  puts $var
}
```

```console:実行結果
[root@server ~]# ./test.exp
1
```


##5.2 else文の使い方
サンプルスクリプト内のlindexコマンドは、リストから要素を取り出すためのコマンドです。以下の例では、`lindex $argv 0`は、最初の引数を意味しています。


```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set var [lindex $argv 0]

if {$var == "1"} {
  puts "1"
} else {
  puts "other than 1"
}
```



```console:実行結果
[root@server ~]# ./test.exp 1
1
[root@server ~]# ./test.exp 2
other than 1
[root@server ~]# ./test.exp 3
other than 1
```

##5.3 elseif文の使い方

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set var [lindex $argv 0]

if {$var == "1"} {
  puts "1"
} elseif {$var == "2"} {
  puts "2"
} else {
  puts "other than 1 or 2"
}
```

```console:実行結果
[root@server ~]# ./test.exp 1
1
[root@server ~]# ./test.exp 2
2
[root@server ~]# ./test.exp 3
other than 1 or 2
[root@server ~]# ./test.exp 4
other than 1 or 2
```


#6 繰り返しの使い方
##6.1 無限ループ
afterコマンドは、スリープする時間を指定できます。
ここでは、afterコマンドを使って、1秒間隔で現在時刻を表示してみます。

```console:スクリプト(`while{1}`)
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

while {1} {
  set x [exec date]
  puts $x
  after 1000;
}
```

```console:実行結果
[root@server ~]# ./test.exp
2020年  7月 16日 木曜日 16:46:08 JST
2020年  7月 16日 木曜日 16:46:09 JST
2020年  7月 16日 木曜日 16:46:10 JST
-snip-
```

##6.2 forを使った繰り返し

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

for {set x 0} {$x<3} {incr x} {
    puts "x is $x"
}
```

```console:実行結果
[root@server ~]# ./test.exp
x is 0
x is 1
x is 2
```

##6.3 foreachを使った繰り返し

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

foreach i {xx yy zz} {
    puts $i
}
```

```console:実行結果
[root@server ~]# ./test.exp
xx
yy
zz
```



#7 スクリプトの引数の使い方
`$argv0`はスクリプトの名前(ここではtest.exp)、`$argc`は引数の個数を表します。
`$argv`は、スクリプトへの引数を表しています。

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set name $argv0
set num $argc
set x [lindex $argv 0]
set y [lindex $argv 1]
puts $name
puts $argc
puts $x
puts $y
```
スクリプトの名前は`test.exp`、引数の個数は2、
第1引数は`111`、第2引数は`aaa`であることがわかります。


```console:実行結果
[root@server ~]# ./test.exp 111 aaa
./test.exp
2
111
aaa
```

#8 関数の使い方
サンプルスクリプト中の`[ ]`引用符はコマンド置換子です｡
`[ ]`で挟まれたリストはコマンドラインとみなされ､その実行結果がひとつのアーギュメントとしてコマンドに渡されます｡

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x 1
set y 2

proc add {x y} {
  puts x=$x,y=$y
  return [expr $x + $y]
}

set ret [add 1 2]
puts x+y=$ret
```

```console:実行結果
[root@server ~]# ./test.exp
x=1,y=2
x+y=3
```

#9 stringコマンドの使い方
##9.1 小文字、大文字を区別する場合

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set var [lindex $argv 0]

if ![string compare $var "aaa"] {
  puts "equal"
} else {
  puts "not equal"
}
```

```console:実行結果
[root@server ~]# ./test.exp aaa
equal
[root@server ~]# ./test.exp AAA
not equal
[root@server ~]# ./test.exp bbb
not equal
```

##9.2 小文字、大文字を区別しない場合(-nocase)

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set var [lindex $argv 0]

if ![string compare -nocase $var "aaa"] {
  puts "equal"
} else {
  puts "not equal"
}
```


小文字、大文字が区別されないことがわかります。

```console:実行結果
[root@server ~]# ./test.exp aaa
equal
[root@server ~]# ./test.exp AAA
equal
[root@server ~]# ./test.exp bbb
not equal
```


##9.3 ファイルから文字列を読み込む方法

```console:サンプルファイル
[root@server ~]# vi sample.txt
[root@server ~]# cat sample.txt
192.168.3.10 server
192.168.3.20 client
```

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set filename "sample.txt"
set fd [open $filename]

while {! [eof $fd]} {
  gets $fd line
  puts stdout $line
}

close $fd
```

```console:実行結果
[root@server ~]# ./test.exp
192.168.3.10 server
192.168.3.20 client
```

##9.4 文字列を分割する方法

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set src "000 111 222"
set dst [split $src " "]

puts [lindex $dst 0]
puts [lindex $dst 1]
puts [lindex $dst 2]
```

```console:実行結果
[root@server ~]# ./test.exp
000
111
222
```

#10 日時を表示する方法

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set systemTime [clock seconds]

puts "Year with 4 digits is [clock format $systemTime -format %Y]"
puts "Month number(01-12) is [clock format $systemTime -format %m]"
puts "Day of month is [clock format $systemTime -format %d]"

puts "Hour(00-23) is [clock format $systemTime -format %H]"
puts "Minutes(00-59) is [clock format $systemTime -format %M]"
puts "Seconds(00-59) is [clock format $systemTime -format %S]"

puts "Now is [clock format $systemTime -format %Y/%m/%d,%H:%M:%S]"
```

```console:現在時刻の確認
[root@server ~]# date
2020年  7月 16日 木曜日 16:17:19 JST
```

```console:実行結果
[root@server ~]# ./test.exp
Year with 4 digits is 2020
Month number(01-12) is 07
Day of month is 16
Hour(00-23) is 16
Minutes(00-59) is 17
Seconds(00-59) is 21
Now is 2020/07/16,16:17:21
```


#11 外部コマンドを実行する方法(exec)
execを実行することで、子プロセスを起動します。
ここでは、execによって、`date`,`ls`,`cal`コマンドを実行してみます。

##11.1 時刻を表示する方法

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x [exec date]
puts $x
```

```console:実行結果
[root@server ~]# ./test.exp
2020年  7月 16日 木曜日 16:26:02 JST
```

##11.2 ファイル、ディレクトリを表示する方法
ディレクトリのファイル一覧を表示してみます。

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x [exec ls -l]
puts $x
```

```console:実行結果
[root@server ~]# ./test.exp
合計 615664
-rw-------.  1 root root      1828  2月 14 17:48 anaconda-ks.cfg
-rw-r--r--.  1 root root    117272  7月  4  2014 bc-1.06.95-13.el7.x86_64.rpm
drwxr-xr-x.  5 root root        42  4月 11 11:47 bin
-snip-
```

##11.3 カレンダーを表示する方法
カレンダーを表示してみます。

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x [exec cal]
puts $x
```

```console:実行結果
[root@server ~]# ./test.exp
      7月 2020
日 月 火 水 木 金 土
          1  2  3  4
 5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
```


#12 配列の使い方
##12.1 配列に値を代入／配列要素の値を表示する方法
`x`という名前の配列を作成します。要素数は3です。

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x(0) aaa
set x(1) bbb
set x(2) ccc

puts $x(0)
puts $x(1)
puts $x(2)
```

```console:実行結果
[root@server ~]# ./test.exp
aaa
bbb
ccc
```

##12.2 配列の要素数を求める方法(`array size`)
要素数3の配列を作成します。

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x(0) aaa
set x(1) bbb
set x(2) ccc

set n [array size x]
puts "array has $n elements"
```

配列xの要素数が3であることがわかります。

```console:実行結果
[root@server ~]# ./test.exp
array has 3 elements
```


##12.3 配列が存在するかどうかを確認する方法(`array exists`)
配列が存在するかどうかは、`array exists`コマンドを使って確認することができます。
配列が存在する場合は1,配列が存在しない場合は0を返します。

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x(0) aaa
set x(1) bbb

puts $x(0)
puts $x(1)

puts "Does array exist? [array exists x]"
array unset x
puts "array is deleted by unset command"
puts "Does array exist? [array exists x]"
```


```console:実行結果
[root@server ~]# ./test.exp
aaa
bbb
Does array exist? 1
array is deleted by unset command
Does array exist? 0
```

##12.4 指定した添字の要素を返す方法(`array get`)

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x(0) aaa
set x(1) bbb
set x(2) ccc

puts [array get x]
puts [array get x 0]
puts [array get x 1]
puts [array get x 2]
```

```console:実行結果
[root@server ~]# ./test.exp
0 aaa 1 bbb 2 ccc
0 aaa
1 bbb
2 ccc
```

##12.5 2次元配列の使い方

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x(0,0) 10
set x(0,1) 20
set x(1,0) aa
set x(1,1) bb

for {set i 0} {$i < 2} {incr i} {
  for {set j 0} {$j < 2} {incr j} {
    puts $x($i,$j)
  }
}
```

```console:実行結果
[root@server ~]# ./test.exp
10
20
aa
bb
```

##12.6 連想配列の使い方


```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x("tokyo") 14000000
set x("osaka")  8900000

puts $x("tokyo")
puts $x("osaka")
```


```console:実行結果
[root@server ~]# ./test.exp
14000000
8900000
```




#13 リストの使い方
リストとは、中括弧あるいはダブルクォートを使ってスペースで区切られた文字列のことです。

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x {10 20 30}

puts [lindex $x 0]
puts [lindex $x 2]

puts "The length of list x is [llength $x]"
```

```console:実行結果
[root@server ~]# ./test.exp
10
30
The length of list x is 3
```



#14 ファイル操作
##14.1 ファイルが存在するかどうかを確認する方法
カレントディレクトリに`sample.txt`が存在するかどうかを確認する
スクリプトを作成します。

```console:テスト用ファイルの作成
[root@server ~]# touch sample.txt
[root@server ~]# ls sample.txt
sample.txt
```

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

if {[file exists sample.txt] == 1} then {
        puts "FOUND"
} else {
        puts "NOT FOUND"
}
```

```console:実行結果(ファイルが存在する場合）
[root@server ~]# ls sample.txt
sample.txt
[root@server ~]# ./test.exp
FOUND
```

```console:実行結果(ファイルが存在しない場合）
[root@server ~]# ls sample.txt
ls: sample.txt にアクセスできません: そのようなファイルやディレクトリはありません
[root@server ~]# ./test.exp
NOT FOUND
```


##14.2 ファイル名のドットまでの名前を返す方法
最後のドット（最後のドットを含まない）までのファイル名を返します。

```console:テスト用ファイルの作成
[root@server ~]# touch sample.tar.gz
[root@server ~]# ls sample.tar.gz
sample.tar.gz
```

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x [file rootname sample.tar.gz]
puts $x

set y [file rootname $x]
puts $y
```

```console:実行結果
[root@server ~]# ./test.exp
sample.tar
sample
```


##14.3 ディレクトリを作成する方法
`test`ディレクトリが存在しないことを確認します。

```console:ディレクトリの確認
[root@server ~]# ls -ld test
ls: test にアクセスできません: そのようなファイルやディレクトリはありません
```

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set test_dir "/root/test"
file mkdir $test_dir
```

`test`ディレクトリが作成されたことを確認します。

```console:実行結果
[root@server ~]# ./test.exp
[root@server ~]# ls -ld test
drwxr-xr-x. 2 root root 6  7月 16 21:50 test
```


#15 変数の有効範囲
globalコマンドを使うことで、関数外の変数にアクセスすることができます。
globalコマンドは、関数内で実行する必要があります。
ここでは、関数test1内で、変数xをグローバル変数として定義しています。

```console:スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/tclsh

set x 10

proc test1 {} {
  global x
  return $x
}

proc test2 {} {
  set x abc
  return $x
}

puts "The return value of test1 is [test1]"
puts "The return value of test2 is [test2]"
```

```console:実行結果
[root@server ~]# ./test.exp
The return value of test1 is 10
The return value of test2 is abc
```


#16 sourceの使い方
sourceを使って、外部のtclソースファイルを読み込んでみます。
[source - Evaluate a file or resource as a Tcl script](https://www.tcl.tk/man/tcl8.5/TclCmd/source.htm)

##16.1 サンプルスクリプト

```console:呼び出し元スクリプト
[root@server ~]# cat test.exp
#!/usr/bin/expect
source proc.exp
test
```

```console:読み込まれる側のソースコード
[root@server ~]# cat proc.exp
proc test {} {
  puts "This is sample"
}
```

```console:実行結果
[root@server ~]# ./test.exp
This is sample
```


#z 参考情報
[Tclの文法](http://www.interq.or.jp/japan/s-imai/tcltk/)（大変参考になりました!）
[文法とコマンド](https://freesoftnet.co.jp/webfiles/tclkits/doc/tclcom.html)
[Tcl 8.4.1　Manual Command Reference](https://freesoftnet.co.jp/webfiles/tclkits/doc/TclCmdRef/TclCmd/string_jp.htm)
[Tcl/Tk お気楽 GUI プログラミング応用編](http://www.nct9.ne.jp/m_hiroi/tcl_tk_doc/tcltk203.html)
[コマンドラインと特殊文字](https://freesoftnet.co.jp/webfiles/tclkits/doc/tclcom.html)

[Tcl – 外部プログラムの実行について -exec-](https://blog.it-see.net/it-dokata/tcl-tk/exec/)
[説明のないとってもシンプルなサンプルプログラム集](http://simplesandsamples.com/stderr.tcl.html)
[Tcl-tk-tcl-error-handling](https://www.finddevguides.com/Tcl-tk-tcl-error-handling)
[Tcl – 配列(1) -配列の作成・更新-](https://blog.it-see.net/it-dokata/tcl-tk/array-1/)

[Tcl 8.4.1　Manual Command Reference](https://freesoftnet.co.jp/webfiles/tclkits/doc/TclCmdRef/tcl_contents_jp.htm)
[TCL-Arrays with Examples](https://bidsarmanish.blogspot.com/2016/12/arrays-in-tcl.html)
[signal](https://wiki.tcl-lang.org/page/signal)

[Tcl – 変数の有効範囲 -global, upvar-](https://blog.it-see.net/it-dokata/tcl-tk/global-and-upvar/)
[send special characters](https://wiki.tcl-lang.org/page/send+special+characters)