跳轉到內容

Tcl 程式設計/簡介

來自華夏公益教科書

什麼是 Tcl?

[編輯 | 編輯原始碼]

Tcl 的名字來源於“工具命令語言”(Tool Command Language),發音為“tickle”。Tcl 是一種非常簡單的開源解釋型程式語言,它提供諸如變數、過程和控制結構等常見功能,以及其他主要語言中沒有的許多有用功能。Tcl 幾乎可以在所有現代作業系統上執行,例如 Unix、Macintosh 和 Windows(包括 Windows Mobile)。

雖然 Tcl 足夠靈活,可以用於幾乎所有可以想象到的應用程式,但它在幾個關鍵領域確實很出色,包括:自動與外部程式互動、將庫嵌入應用程式程式、語言設計和通用指令碼。

Tcl 由 John Ousterhout 於 1988 年建立,並根據 BSD 風格的 許可證 分發(它允許你做 GPL 所允許的一切,以及關閉你的原始碼)。截止 2008 年 2 月,當前的穩定版本是 8.5.1(較舊的 8.4 分支中的 8.4.18)。

第一個與 Tcl 協同工作的重大 GUI 擴充套件是 Tk,它是一個旨在快速 GUI 開發的工具包。這就是為什麼 Tcl 現在通常被稱為 Tcl/Tk 的原因。

該語言具有深遠的自省能力,它的語法雖然 簡單,但卻與 Fortran/Algol/C++/Java 世界截然不同。雖然 Tcl 是一種基於字串的語言,但它有相當多的面向物件的擴充套件,例如 Snitincr TclXOTcl,僅舉幾例。

Tcl 最初是作為實驗性計算機輔助設計 (CAD) 工具的可重用命令語言而開發的。直譯器實現為一個 C 庫,可以連結到任何應用程式中。為 Tcl 直譯器新增新函式非常容易,因此它是一個理想的可重用“宏語言”,可以整合到許多應用程式中。

但是,Tcl 本身就是一種程式語言,可以粗略地描述為

  • LISP/Scheme(主要是因為它的尾遞迴功能),
  • C(控制結構關鍵字,表示式語法)和
  • Unix shell(但具有更強大的結構化功能)。

一種語言,多種風格

[編輯 | 編輯原始碼]

雖然“一切皆命令”的語言似乎一定是“命令式”和“過程式”的,但 Tcl 的靈活性使得人們可以非常輕鬆地使用函式式或面向物件的程式設計風格。有關示例,請參見下面的“Tcl 示例”。

傳統的“過程式”方法是

proc mean list {
   set sum 0.
   foreach element $list {set sum [expr {$sum + $element}]}
   return [expr {$sum / [llength $list]}]
}


以下還有另一種風格(在較長的列表上速度不快,但僅依賴於 Tcl)。它透過構建一個表示式來工作,其中列表的元素用加號連線,然後評估該表示式

proc mean list {expr double([join $list +])/[llength $list]}

從 Tcl 8.5 開始,將數學運算子公開為命令,以及使用擴充套件運算子,這種風格更好

proc mean list {expr {[tcl::mathop::+ {*}$list]/double([llength $list])}}

或者,如果你已經匯入了 tcl::mathop 運算子,只需

proc mean list {expr {[+ {*}$list]/double([llength $list])}}

請注意,以上所有內容都是有效的獨立 Tcl 指令碼。

在 Tcl 中實現其他程式語言(無論是(逆波蘭)表示法,還是其他任何語言)也非常容易,以便進行實驗。人們可能會稱 Tcl 為“計算機科學實驗室”。例如,以下是如何在 Tcl 中計算數字列表的平均值(首先編寫更多 Tcl 來實現 J 風格的函式式語言——請參見 Tacit programming in examples)

Def mean = fork /. sum llength

或者,人們可以實現類似於 FORTH 或 Postscript 的 RPN 語言,並編寫

 : mean  dup sum swap size double / ;


一個更實際的方面是 Tcl 對“面向語言的程式設計”非常開放——在解決問題時,指定一個(小)語言,該語言最簡單地描述和解決該問題——然後去實現該語言…

我為什麼要使用 Tcl?

[編輯 | 編輯原始碼]

好問題。一般建議是:“為工作使用最佳工具”。好的工匠有一套好工具,並且知道如何最好地使用它們。

Tcl 是其他指令碼語言(如 awk、Perl、Python、PHP、Visual Basic、Lua、Ruby,以及任何其他即將出現的語言)的競爭者。這些語言各有優劣,當某些語言在適用性上相似時,最終就變成了一種個人喜好問題。

Tcl 的優勢在於

  • 最簡單的語法(可以輕鬆擴充套件)
  • 跨平臺可用性:Mac、Unix、Windows 等
  • 強大的國際化支援:一切都是 Unicode 字串
  • 健壯、經過良好測試的程式碼庫
  • Tk GUI 工具包以 Tcl 為原生語言
  • BSD 許可證,允許開源使用,例如 GPL,以及閉源使用
  • 一個非常有幫助的社群,可以透過新聞組、Wiki 或聊天聯絡 :)

Tcl 並不是解決所有問題的最佳方案。但是,瞭解 Tcl 的功能卻是一次寶貴的體驗。

示例:一個微型 Web 伺服器

[編輯 | 編輯原始碼]

在逐一介紹 Tcl 的各個部分之前,提供一個稍微長一點的示例可能更合適,這樣你就能體會到它是什麼樣的。以下是用 41 行程式碼編寫的完整微型 Web 伺服器,它提供靜態內容(HTML 頁面、影像),但也提供了一部分 CGI 功能:如果一個 URL 以 .tcl 結尾,就會呼叫一個 Tcl 直譯器來執行它,並將結果(一個動態生成的 HTML 頁面)傳送出去。

請注意,不需要任何擴充套件包——Tcl 已經可以用 socket 命令很好地完成這些任務。套接字是一個可以與 puts 命令寫入的通道。fcopy 命令非同步地(在後臺)從一個通道複製到另一個通道,源通道可以是程序管道(“exec tclsh” 部分)或一個開啟的檔案。

這個伺服器經過測試,即使在 200MHz 的 Windows 95 上透過 56k 調變解調器並同時為多個客戶端提供服務時也能很好地工作。此外,由於程式碼的簡潔性,這是一個關於 HTTP 工作原理(部分)的教學示例。

# DustMotePlus - with a subset of CGI support
set root      c:/html
set default   index.htm
set port      80
set encoding  iso8859-1
proc bgerror msg {puts stdout "bgerror: $msg\n$::errorInfo"}
proc answer {sock host2 port2} {
    fileevent $sock readable [list serve $sock]
}
proc serve sock {
    fconfigure $sock -blocking 0
    gets $sock line
    if {[fblocked $sock]} {
        return
    }
    fileevent $sock readable ""
    set tail /
    regexp {(/[^ ?]*)(\?[^ ]*)?} $line -> tail args
    if {[string match */ $tail]} {
        append tail $::default
    }
    set name [string map {%20 " " .. NOTALLOWED} $::root$tail]
    if {[file readable $name]} {
        puts $sock "HTTP/1.0 200 OK"
        if {[file extension $name] eq ".tcl"} {
            set ::env(QUERY_STRING) [string range $args 1 end]
            set name [list |tclsh $name]
        } else {
            puts $sock "Content-Type: text/html;charset=$::encoding\n"
        }
        set inchan [open $name]
        fconfigure $inchan -translation binary
        fconfigure $sock   -translation binary
        fcopy $inchan $sock -command [list done $inchan $sock]
    } else {
        puts $sock "HTTP/1.0 404 Not found\n"
        close $sock
    }
}
proc done {file sock bytes {msg {}}} {
    close $file
    close $sock
}
socket -server answer $port
puts "Server ready..."
vwait forever

以下是我用它測試過的一個小“CGI”指令碼(儲存為 time.tcl)

# time.tcl - tiny CGI script.
if {![info exists env(QUERY_STRING)]} {
    set env(QUERY_STRING) ""
}
puts "Content-type: text/html\n"
puts "<html><head><title>Tiny CGI time server</title></head>
<body><h1>Time server</h1>
Time now is: [clock format [clock seconds]]
<br>
Query was: $env(QUERY_STRING)
<hr>
<a href=index.htm>Index</a>
</body></html>"

哪裡可以獲取 Tcl/Tk

[編輯 | 編輯原始碼]

在大多數 Linux 系統上,Tcl/Tk 已經安裝好了。你可以在控制檯提示符(xterm 或類似的)中輸入 tclsh 來確認。如果出現 "%" 提示符,就說明你已經準備好了。為了確保,在 % 提示符下輸入 info pa 檢視補丁級別(例如 8.4.9)以及 info na 檢視可執行檔案在檔案系統中的位置。

Tcl 是一個開源專案。如果你想自己構建它,可以在 http://tcl.sourceforge.net/ 獲取原始碼。

對於所有主要平臺,你可以從 ActiveState 下載二進位制 ActiveTcl 發行版。除了 Tcl 和 Tk,它還包含許多流行的擴充套件——它被稱為規範的“包含電池”發行版。

或者,你可以獲得 Tclkit:一個封裝在一個單個檔案中的 Tcl/Tk 安裝包,你不需要解壓縮。執行時,該檔案會將自己掛載為虛擬檔案系統,允許訪問其所有部分。

2006 年 1 月,釋出了一個新的、有前景的 Tcl 單檔案 vfs 發行版;eTcl。可在 http://www.evolane.com/software/etcl/index.html 下載 Linux、Windows 和 Windows Mobile 2003 的免費二進位制檔案。特別是在 PocketPC 上,它提供了一些其他埠中一直缺少的功能:套接字、視窗“縮回”,並且可以透過提供啟動指令碼以及安裝純 Tcl 庫來擴充套件。

第一步

[edit | edit source]

要檢視你的安裝是否有效,你可以將以下文字儲存到一個名為 hello.tcl 的檔案中並執行它(在 Linux 上的控制檯中輸入 tclsh hello.tcl,在 Windows 上雙擊)

package require Tk
pack [label .l -text "Hello world!"]

它應該會彈出一個帶有問候語的小灰色視窗。

要使指令碼直接可執行(在 Unix/Linux 和 Windows 上的 Cygwin 上),請使用以下第一行(# 位於最左邊)

#!/usr/bin/env tclsh

或(使用較舊的、已棄用的複雜風格)

#! /bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" ${1+"$@"}

這樣,shell 就可以確定要使用哪個可執行檔案來執行指令碼。

一種更簡單的方法,並且強烈推薦初學者和經驗豐富的使用者使用,就是以互動方式啟動 tclsh 或 wish。你將在控制檯中看到一個 % 提示符,可以在其中輸入命令並檢視其響應。即使錯誤資訊在這裡也很有幫助,也不會導致程式中止——不要害怕嘗試任何你喜歡的操作!例如

$ tclsh
info patchlevel
8.4.12
expr 6*7
42
expr 42/0
divide by zero

你甚至可以以互動方式編寫程式,最好是一行一行地寫

proc ! x {expr {$x<=2? $x: $x*[! [incr x -1]]}}
! 5
120

有關更多示例,請參閱“快速瀏覽”一章。


語法

[edit | edit source]

語法僅僅是語言結構的規則。英語的簡單語法可以這樣說(暫時忽略標點符號)

  • 文字由一個或多個句子組成
  • 句子由一個或多個單片語成

雖然簡單,但它也很好地描述了 Tcl 的語法——如果你將“指令碼”替換為“文字”,將“命令”替換為“句子”。還存在另一個差異,即 Tcl 單詞可以再次包含指令碼或命令。所以

if {$x < 0} {set x 0}

是一個由三個單片語成的命令:if、一個用大括號括起來的條件、一個用大括號括起來的命令(也由三個單片語成)。

Take this for example

是一個格式良好的 Tcl 命令:它呼叫 Take(必須在之前定義)並傳遞三個引數“this”、“for”和“example”。命令可以隨意解釋其引數,例如

puts acos(-1)

會將字串“acos(-1)”寫入 stdout 通道,並返回空字串“”,而

expr acos(-1)

會計算 -1 的反餘弦並將結果返回 3.14159265359(Pi 的近似值),或者

string length acos(-1)

會呼叫 string 命令,該命令會再次分派到其 length 子命令,該子命令會確定第二個引數的長度並返回 8。

快速總結

[edit | edit source]

Tcl 指令碼是一個字串,它是一個命令序列,由換行符或分號分隔。

命令是一個字串,它是一個單詞列表,由空格分隔。第一個單詞是命令的名稱,其他單詞作為引數傳遞給它。在 Tcl 中,“一切都是命令”——即使在其他語言中被稱為宣告、定義或控制結構的東西也是如此。命令可以隨意解釋其引數——特別是,它可以實現另一種語言,例如 expr

單詞是一個字串,它是一個簡單的單詞,或者以 { 開頭並以匹配的 }(大括號)結尾,或者以 " 開頭並以匹配的 " 結尾。用大括號括起來的單詞不會被解析器評估。在引號中的單詞中,在呼叫命令之前可能會發生替換

  • $[A-Za-z0-9_]+ 替換給定變數的值。或者,如果變數名包含該正則表示式以外的字元,則可以新增另一層大括號來幫助解析器正確識別
puts "Guten Morgen, ${Schüler}!"

如果程式碼寫成 $Schüler,這將被解析為變數 $Sch 的值,緊跟著常量字串 üler

  • 單詞的一部分可以是嵌入式指令碼:一個用 [] 方括號括起來的字串,其內容在呼叫當前命令之前作為指令碼進行評估(見上文)。

簡而言之:指令碼和命令包含單詞。單詞可以再次包含指令碼和命令。(這會導致單詞超過一頁長……)

算術和邏輯表示式不是 Tcl 語言本身的一部分,而是 expr 命令的語言(也用於 ifforwhile 命令的一些引數),基本上等效於 C 語言的表示式,具有中綴運算子和函式。請參閱下面關於 expr 的單獨章節。


手冊頁:11 條規則

[edit | edit source]

以下是 Tcl(8.4)的完整手冊頁,其中包含“十誡”,即 11 條規則。(從 8.5 開始,第十二條規則涉及 {*} 特性)。

以下規則定義了 Tcl 語言的語法和語義

(1) 命令 Tcl 指令碼是一個字串,包含一個或多個命令。分號和換行符是命令分隔符,除非用引號括起來,如以下所述。在命令替換期間,右括號是命令終止符(見下文),除非用引號括起來。

(2) 評估 命令評估分為兩個步驟。首先,Tcl 直譯器將命令分解成單詞並執行替換,如以下所述。這些替換對所有命令執行的方式相同。第一個單詞用於定位一個命令過程來執行命令,然後所有命令單詞都傳遞給命令過程。命令過程可以隨意解釋其每個單詞,例如整數、變數名、列表或 Tcl 指令碼。不同的命令會不同地解釋其單詞。

(3) 單詞 命令的單詞由空格分隔(換行符除外,換行符是命令分隔符)。

(4) 雙引號 如果單詞的第一個字元是雙引號 ("),則單詞由下一個雙引號字元終止。如果引號之間出現分號、右括號或空格字元(包括換行符),則它們將被視為普通字元幷包含在單詞中。命令替換、變數替換和反斜槓替換將對引號之間的字元執行,如以下所述。雙引號不會作為單詞的一部分保留。

(5) 大括號 如果單詞的第一個字元是左大括號 ({),則單詞由匹配的右大括號 (}) 終止。大括號在單詞中巢狀:每個額外的左大括號都必須有一個額外的右大括號(但是,如果單詞中左大括號或右大括號用反斜槓引起來,則它不會被計入定位匹配的右大括號)。除了以下描述的反斜槓-換行符替換之外,對大括號之間的字元不執行任何替換,分號、換行符、右括號或空格也不會被賦予任何特殊解釋。單詞將正好包含外層大括號之間的字元,不包括大括號本身。

(6) 命令替換 如果一個單詞包含左括號 ([),則 Tcl 將執行命令替換。為此,它會遞迴地呼叫 Tcl 直譯器來處理左括號後面的字元作為 Tcl 指令碼。該指令碼可以包含任意數量的命令,並且必須以右括號 (]) 終止。指令碼的結果(即其最後一個命令的結果)將替換到單詞中,代替括號及其之間的所有字元。在一個單詞中可以進行任意數量的命令替換。對用大括號括起來的單詞不執行命令替換。

(7) 變數替換 如果一個單詞包含美元符號 ($),則 Tcl 將執行變數替換:美元符號和後面的字元將被單詞中變數的值替換。變數替換可以採用以下任何形式

$name

Name 是標量變數的名稱;名稱是字母、數字、下劃線或名稱空間分隔符(兩個或多個冒號)的序列。

$name(index)

Name 給出陣列變數的名稱,index 給出該陣列中元素的名稱。Name 只能包含字母、數字、下劃線和名稱空間分隔符,並且可以是空字串。對 index 的字元執行命令替換、變數替換和反斜槓替換。

${name}

Name 是標量變數的名稱。它可以包含除右大括號以外的任何字元。在一個單詞中可以進行任意數量的變數替換。對用大括號括起來的單詞不執行變數替換。

(8) 反斜槓替換 如果一個單詞中出現反斜槓 (\),則會發生反斜槓替換。在除以下描述的情況之外的所有情況下,反斜槓都會被丟棄,後面的字元將被視為普通字元幷包含在單詞中。這允許將雙引號、右括號和美元符號等字元包含在單詞中,而不會觸發特殊處理。下表列出了特殊處理的反斜槓序列,以及替換每個序列的值。

\a
可聽警報(鈴聲)(0x7)。
\b
退格鍵(0x8)。
\f
換頁符(0xc)。
\n
換行符(0xa)。
\r
回車符(0xd)。
\t
製表符(0x9)。
\v
垂直製表符 (0xb)。
\<newline>whiteSpace
一個空格字元將替換反斜槓、換行符以及換行符後的所有空格和製表符。此反斜槓序列是唯一的,因為它在命令實際解析之前,在單獨的預處理階段被替換。這意味著它即使出現在大括號之間也會被替換,並且如果結果空格不在大括號或引號中,則它將被視為單詞分隔符。
\\
字面反斜槓 (\),沒有特殊效果。
\ooo
數字 ooo(一個、兩個或三個)給出將要插入的 Unicode 字元的八位八進位制值。Unicode 字元的較高位將為 0。
\xhh
十六進位制數字 hh 給出將要插入的 Unicode 字元的八位十六進位制值。可以存在任意數量的十六進位制數字;但是,除了最後兩個之外,其他所有數字都會被忽略(結果始終是一個位元組量)。Unicode 字元的較高位將為 0。
\uhhhh
十六進位制數字 hhhh(一個、兩個、三個或四個)給出將要插入的 Unicode 字元的十六位十六進位制值。

除非如上所述的反斜槓-換行符,否則不會對括在大括號中的單詞執行反斜槓替換。

(9) 註釋 如果在 Tcl 預期命令的第一個單詞的第一個字元出現的地方出現了井號 (#),則井號和其後的字元,一直到下一個換行符,都被視為註釋並被忽略。註釋字元僅在出現在命令開頭時才有意義。

(10) 替換順序 每個字元在 Tcl 直譯器中被處理一次,作為建立命令單詞的一部分。例如,如果發生了變數替換,則不會對變數的值執行任何進一步的替換;該值將原樣插入到單詞中。如果發生了命令替換,則巢狀命令將完全由對 Tcl 直譯器的遞迴呼叫處理;在進行遞迴呼叫之前不會執行任何替換,並且不會對巢狀指令碼的結果執行任何額外的替換。替換從左到右進行,每個替換在嘗試執行下一個替換之前完全評估。因此,像

set y [set x 0][incr x][incr x]

這樣的序列始終將變數 y 設定為值 012。

(11) 替換和詞邊界 替換不會影響命令的詞邊界。例如,在變數替換期間,變數的整個值將成為單個單詞的一部分,即使變數的值包含空格。

註釋

[edit | edit source]

註釋的第一個規則很簡單:註釋以 # 開頭,其中預期命令的第一個單詞,並一直持續到行尾(可以透過尾部的反斜槓擴充套件到下一行)

# This is a comment \
going over three lines \
with backslash continuation

Tcl 新使用者遲早會遇到的問題之一是註釋的行為方式出乎意料。例如,如果您像這樣註釋掉程式碼的一部分

# if {$condition} {
    puts "condition met!"
# }

這碰巧可以工作,但註釋中任何不平衡的大括號都可能導致意外的語法錯誤。原因是 Tcl 的分組(確定詞邊界)發生在考慮 # 字元之前。

要在同一行上在命令後面添加註釋,只需新增分號即可

puts "this is the command" ;# that is the comment

註釋僅在預期命令的地方被視為註釋。在資料(例如 switch 中的比較值)中,# 只是一個字面字元

if $condition {# good place
   switch -- $x {
       #bad_place {because switch tests against it}
       some_value {do something; # good place again}
   }
}

要註釋掉多行程式碼,最簡單的方法是使用 “if 0”

if 0 {
    puts "This code will not be executed"
    This block is never parsed, so can contain almost any code
    - except unbalanced braces :)
}

資料型別

[edit | edit source]

在 Tcl 中,所有值都是字串,並且短語“一切都是字串”通常用來說明這一點。但是正如2可以在英語中解釋為“數字 2”或“表示數字 2 的字元”,Tcl 中的兩個不同函式可以以兩種不同的方式解釋相同的值。例如,expr 命令將 “2” 解釋為一個數字,但 string length 命令將 “2” 解釋為單個字元。Tcl 中的所有值都可以解釋為字元或字元所代表的其他內容。需要記住的重要一點是,Tcl 中的每個值都是一個字元字串,每個字元字串可能被解釋為其他內容,具體取決於上下文。這將在下面的示例中變得更加清晰。出於效能原因,從 8.0 版本開始的 Tcl 會跟蹤字串值和該字串值的最後解釋方式。本節涵蓋了 Tcl 值(字串)被解釋為的各種“型別”。

字串

[edit | edit source]

字串是一個包含零個或多個字元的序列(其中幾乎所有情況下都接受所有 16 位 Unicode,在下面更詳細地介紹)。字串的大小是自動管理的,因此您只需在字串長度超過虛擬記憶體大小時才需要擔心它。

與許多其他語言不同,Tcl 中的字串不需要引號進行標記。以下是完全有效的

set greeting Hello!

引號(或大括號)更用於分組

set example "this is one word"
set another {this is another}

區別在於,在引號內,會執行替換(如變數、嵌入命令或反斜槓),而在大括號內,不會執行替換(類似於 shell 中的單引號,但可以巢狀)

set amount 42
puts "You owe me $amount" ;#--> You owe me 42
puts {You owe me $amount} ;#--> You owe me $amount

在原始碼中,引號或大括號字串可以跨越多行,物理換行符也是字串的一部分

set test "hello
world
in three lines"

反轉字串,我們首先讓一個索引 i 指向字串的末尾,並遞減 i 直到它為零,將索引的字元追加到結果 res 的末尾

proc sreverse str {
set res ""
for {set i [string length $str]} {$i > 0} {} {
    append res [string index $str [incr i -1]]
} 
set res
}

sreverse "A man, a plan, a canal - Panama"
amanaP - lanac a ,nalp a ,nam A


十六進位制轉儲字串

proc hexdump string {
    binary scan $string H* hex
    regexp -all -inline .. $hex
}

hexdump hello
68 65 6c 6c 6f

在字串中查詢子字串可以透過多種方式完成

string first  $substr  $str ;# returns the position from 0, or -1 if not found
string match *$substr* $str ;# returns 1 if found, 0 if not
regexp $substr  $str ;# the same

匹配是在 string first 中使用精確匹配,在 string match 中使用 glob 樣式匹配,在 regexp 中使用正則表示式匹配。如果 substr 中有對 glob 或正則表示式有特殊意義的字元,則建議使用 string first

列表

[edit | edit source]

許多字串也是格式良好的列表。每個簡單單詞都是長度為 1 的列表,更長列表的元素由空格隔開。例如,一個對應於三個元素列表的字串

set example {foo bar grill}

包含不平衡引號或大括號的字串,或緊跟在大括號後面的非空格字元,無法直接解析為列表。您可以顯式地分割它們以建立一個列表。

列表的“建構函式”當然叫做 list。建議在元素來自變數或命令替換時使用(大括號不會執行此操作)。由於 Tcl 命令本身就是列表,因此以下內容可以完全替代 list 命令

proc list args {set args}

列表可以再次包含列表,可以到任何深度,這使得對矩陣和樹的建模變得容易。以下字串表示一個 4 x 4 單位矩陣,它是一個包含列表的列表。外部大括號將整個內容分組為一個字串,其中包括字面內部大括號和空格,包括字面換行符。然後,列表解析器將內部大括號解釋為分隔巢狀列表。

{{1 0 0 0}
 {0 1 0 0}
 {0 0 1 0}
 {0 0 0 1}}

換行符也是有效的列表元素分隔符。

Tcl 的列表操作在一些示例中演示

set      x {foo bar}
llength  $x        ;#--> 2
lappend  x  grill  ;#--> foo bar grill
lindex   $x 1      ;#--> bar (indexing starts at 0)
lsearch  $x grill  ;#--> 2 (the position, counting from 0)
lsort    $x        ;#--> bar foo grill
linsert  $x 2 and  ;#--> foo bar and grill
lreplace $x 1 1 bar, ;#--> foo bar, grill

請注意,只有上面的lappend 是可變的。要就地更改列表(列表……)的元素,lset 命令很有用 - 只需提供所需的索引即可

set test {{a b} {c d}}
{a b} {c d}
lset test 1 1 x
{a b} {c x}

lindex 命令也接受多個索引

lindex $test 1 1
x

示例:要確定元素是否包含在列表中(從 Tcl 8.5 開始,可以使用in 運算子來實現這一點)

proc in {list el} {expr {[lsearch -exact $list $el] >= 0}}
in {a b c} b
1
in {a b c} d
#ignore this line, which is only here because there is currently a bug in wikibooks rendering which makes the 0 on the following line disappear when it is alone 
0

示例:按值從列表變數中刪除元素(與 lappend 相反),如果存在

proc lremove {_list el} {
  upvar 1 $_list list
  set pos [lsearch -exact $list $el]
  set list [lreplace $list $pos $pos]
}

set t {foo bar grill}
foo bar grill
lremove t bar
foo grill
set t
foo grill

一個更簡單的替代方案,它還刪除了 el 的所有出現

proc lremove {_list el} {
  upvar 1 $_list list
  set list [lsearch -all -inline -not -exact $list $el]
}

示例:要從列表 L 中繪製一個隨機元素,我們首先確定它的長度(使用 llength),將該長度乘以一個大於 0.0 小於 1.0 的隨機數,截斷該數為整數(因此它位於 0 到長度-1 之間),並將其用於索引(lindex)到列表中

proc ldraw L {
   lindex $L [expr {int(rand()*[llength $L])}]
}

示例:轉置矩陣(交換行和列),使用整數作為生成的變數名

proc transpose matrix {
   foreach row $matrix {
       set i 0
       foreach el $row {lappend [incr i] $el}
   }
   set res {}
   set i 0
   foreach e [lindex $matrix 0] {lappend res [set [incr i]]}
   set res
}

transpose {{1 2} {3 4} {5 6}}
{1 3 5} {2 4 6}

示例:漂亮地列印一個表示表格的包含列表的列表

proc fmtable table {
   set maxs {}
   foreach item [lindex $table 0] {
       lappend maxs [string length $item]
   }
   foreach row [lrange $table 1 end] {
       set i 0
       foreach item $row max $maxs {
           if {[string length $item]>$max} {
               lset maxs $i [string length $item]
           }
           incr i
       }
   }
   set head +
   foreach max $maxs {append head -[string repeat - $max]-+}
   set res $head\n
   foreach row $table {
       append res |
       foreach item $row max $maxs {append res [format " %-${max}s |" $item]}
       append res \n
   }
   append res $head
}

測試

fmtable {
   {1 short "long field content"}
   {2 "another long one" short}
   {3 "" hello}
}
+---+------------------+--------------------+
| 1 | short            | long field content |
| 2 | another long one | short              |
| 3 |                  | hello              |
+---+------------------+--------------------+

列舉:列表也可以用來實現列舉(從符號到非負整數的對映)。lsearch/lindex 的一個很好的包裝示例

proc makeEnum {name values} {
   interp alias {} $name: {} lsearch $values
   interp alias {} $name@ {} lindex $values
}

makeEnum fruit {apple blueberry cherry date elderberry}

這將“apple”分配給 0,“blueberry”分配給 1,等等。

fruit: date
3
fruit@ 2
cherry

數字

[edit | edit source]

數字是可以解析為數字的字串。Tcl 支援整數(32 位或 64 位寬)和“雙精度”浮點數。從 Tcl 8.5 開始,支援大數(任意精度整數)。算術運算透過expr 命令完成,該命令基本上採用與 C 相同的運算子語法(包括三元運算子 x?y:z)、括號和數學函式。有關 expr 的詳細討論,請參見下文。

使用format 命令控制數字的顯示格式,該命令執行適當的舍入

expr 2/3.
0.666666666667
format %.2f [expr 2/3.]
0.67

在 8.4 版本(當前版本為 8.5)之前,Tcl 遵循 C 語言的約定,即以 0 開頭的整數被解析為八進位制,因此

0377 == 0xFF == 255

這在 8.5 中發生了變化,儘管 - 太多人偶然發現了作為小時或月份的“08”,並引發了語法錯誤,因為 8 不是有效的八進位制數。在將來,如果您確實想要八進位制,則必須編寫 0o377。您可以使用 format 命令執行數字進位制轉換,其中格式為 %x 表示十六進位制、%d 表示十進位制、%o 表示八進位制,輸入數字應具有 C 語言的標記來指示其進位制

format %x 255
ff
format %d 0xff
255
format %o 255
377
format %d 0377
255

具有整數值的變數可以使用 incr 命令最有效地修改

incr i    ;# default increment is 1
incr j 2
incr i -1 ;# decrement with negative value
incr j $j ;# double the value

最大正整數可以從十六進位制形式確定,前面是 7,後面跟著幾個“F”字元。Tcl 8.4 可以使用 64 位的“寬整數”,並且最大整數是

expr 0x7fffffffffffffff
9223372036854775807

演示:再加一個,它就變成了最小整數

expr 0x8000000000000000
-9223372036854775808

大數:從 Tcl 8.5 開始,整數可以是任意大小的,因此不再存在最大整數。例如,您想要一個很大的階乘

proc tcl::mathfunc::fac x {expr {$x < 2? 1: $x * fac($x-1)}}

expr fac(100)


93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

IEEE 特殊浮點數: 從 8.5 版本開始,Tcl 支援幾種特殊的浮點數,即 Inf(無窮大)和 NaN(非數字)。

set i [expr 1/0.]
Inf
expr {$i+$i}
Inf
expr {$i+1 == $i}
1
set j NaN ;# special because it isn't equal to itself
NaN
expr {$j == $j}
#ignore this line, which is only here because there is currently a bug in wikibooks rendering which makes the 0 on the following line disappear when it is alone 
0

布林值

[edit | edit source]

Tcl 與 C 類似,以數字形式支援布林值,其中 0 代表假,任何其他數字代表真。它還支援字串 "true"、"false"、"yes" 和 "no" 以及其他一些字串(見下文)。規範的 "true" 值(由布林表示式返回)為 1。

foreach b {0 1 2 13 true false on off no yes n y a} {puts "$b -> [expr {$b?1:0}]"}
0 -> 0
1 -> 1
2 -> 1
13 -> 1
true -> 1
false -> 0
on -> 1
off -> 0
no -> 0
yes -> 1
n -> 0
y -> 1
expected boolean value but got "a"

字元

[edit | edit source]

字元是書寫元素(例如字母、數字、標點符號、漢字、連字等)的抽象。從 8.1 版本開始,Tcl 內部使用 Unicode 表示字元,Unicode 可以被視為 0 到 65535 之間的無符號整數(最近的 Unicode 版本甚至超出了這個邊界,但 Tcl 實現目前最多使用 16 位)。任何 Unicode U+XXXX 可以使用 \uXXXX 轉義符指定為字元常量。建議在 Tcl 指令碼中直接使用 ASCII 字元 (\u0000-\u007f),並對其他所有字元進行轉義。

使用以下方法在數字 Unicode 和字元之間進行轉換:

set char [format %c $int]
set int  [scan $char %c]

注意,超過 65535 的 int 值會再次產生“遞減”的字元,而負 int 值甚至會產生兩個偽字元。format 不會發出警告,因此最好在呼叫之前進行測試。

字元序列稱為字串(見上文)。單個字元沒有特殊的資料型別,因此單個字元只是一個長度為 1 的字串(一切都字串)。在 UTF-8 中,Tcl 內部使用的編碼,單個字元的編碼可能佔用 1 到 3 個位元組的空間。要確定單個字元的位元組長度:

string bytelength $c ;# assuming [string length $c]==1

字串例程也可以應用於單個字元,例如 [string toupper] 等。使用以下方法找出字元是否在給定集合(字元字串)中:

expr {[string first $char $set]>=0}

由於字元的 Unicode 處於不同的範圍內,檢查字元程式碼是否在一個範圍內可以或多或少地粗略地對其類別進行分類:

proc inRange {from to char} {
    # generic range checker
    set int [scan $char %c]
    expr {$int>=$from && $int <= $to}
}
interp alias {} isGreek {}    inRange 0x0386 0x03D6
interp alias {} isCyrillic {} inRange 0x0400 0x04F9
interp alias {} isHangul {}   inRange 0xAC00 0xD7A3

這是一個有用的幫助程式,用於將所有超出 ASCII 集的字元轉換為它們的 \u.... 轉義符(因此結果字串是嚴格的 ASCII):

proc u2x s {
   set res ""
   foreach c [split $s ""] {
     scan $c %c int
     append res [expr {$int<128? $c :"\\u[format %04.4X $int]"}]
   }
   set res
}

內部表示

[edit | edit source]

在用 C 編寫的 Tcl 主實現中,每個值都具有字串表示(UTF-8 編碼)和結構化表示。這是一種實現細節,可以提高效能,但對語言沒有語義影響。Tcl 跟蹤這兩種表示,確保如果其中一個發生更改,另一個表示將在下次使用時更新以反映該更改。例如,如果一個值的字串表示為 "8",並且該值最後一次作為數字在 [expr] 命令中使用,那麼它的結構化表示將是數值型別,例如有符號整數或雙精度浮點數。如果值 "one two three" 最後一次在列表命令中使用,那麼它的結構化表示將是列表結構。C 側有各種其他“型別”,它們可以被用作結構化表示。從 Tcl 8.5 版本開始,只儲存值的最新結構化表示,並在需要時用其他表示替換它。這種值的“雙埠”有助於避免重複解析或“字串化”,否則這些操作會經常發生,因為每次在原始碼中遇到一個值時,它都會被解釋為字串,然後才能在當前上下文中被解釋。但是對於程式設計師來說,“一切都是字串”的觀點仍然得到維護。

這些值儲存在稱為物件的引用計數結構中(物件一詞有很多含義)。從所有使用值的程式碼(與實現特定表示的程式碼相反)的角度來看,它們是不可變的。在實踐中,這是透過寫時複製策略實現的。

變數

[edit | edit source]

變數可以是區域性變數或全域性變數,也可以是標量變數或陣列變數。它們的名稱可以是任何不包含冒號(冒號保留用於名稱空間分隔符)的字串,但為了 $-解引用方便,通常使用 [A-Za-z0-9_]+ 模式,即一個或多個字母、數字或下劃線。

變數不需要事先宣告。如果變數之前不存在,則在第一次賦值時建立,並在不再需要時取消設定。

set foo    42     ;# creates the scalar variable foo
set bar(1) grill  ;# creates the array bar and its element 1
set baz    $foo   ;# assigns to baz the value of foo
set baz [set foo] ;# the same effect
info exists foo   ;# returns 1 if the variable foo exists, else 0
unset foo         ;# deletes the variable foo

使用 $foo 符號檢索變數的值僅僅是 [set foo] 的語法糖。後者更強大,因為它可以巢狀,以進行更深層的解引用。

set foo   42
set bar   foo
set grill bar
puts [set [set [set grill]]] ;# gives 42

有些人可能期望 $$$grill 返回相同的結果,但事實並非如此,因為 Tcl 解析器。當它遇到第一個和第二個 $ 符號時,它嘗試找到一個變數名稱(包含一個或多個字母、數字或下劃線),但沒有成功,因此這些 $ 符號按字面意思保留下來。第三個 $ 允許對變數 grill 進行替換,但不會對之前的 $ 符號進行回溯。因此,$$$grill 的計算結果為 $$bar。巢狀的 [set] 命令使使用者可以更好地控制。

區域性變數與全域性變數

[edit | edit source]

區域性變數只存在於定義它的過程中,並在過程完成後被釋放。預設情況下,過程中的所有變數都是區域性變數。

全域性變數存在於過程之外,只要它們沒有被明確取消設定。它們可能需要用於長期存在的資料,或者不同過程之間的隱式通訊,但總的來說,儘可能少使用全域性變數更安全,也更有效。下面是一個只有一個賬戶的簡單銀行示例:

 set balance 0 ;# this creates and initializes a global variable

 proc deposit {amount} {
    global balance
    set balance [expr {$balance + $amount}]
 }

 proc withdraw {amount} {
    set ::balance [expr {$::balance - $amount}]
 }

這說明了兩種引用全域性變數的方法 - 或者使用 global 命令,或者使用 :: 字首限定變數名稱。變數 amount 在兩個過程中的都是區域性變數,它的值是相應過程的第一個引數的值。

自省

info vars ;#-- lists all visible variables
info locals
info globals

在過程(不建議)中使所有全域性變數可見:

eval global [info globals]

標量變數與陣列變數

[edit | edit source]

上面在 資料型別 中討論的所有值型別都可以放入標量變數中,這是普通型別。

陣列是變數的集合,由一個可以是任何字串的鍵索引,實際上是雜湊表。其他語言中被稱為 "array"(由整數索引的值的向量)的東西,在 Tcl 中更像列表。一些示例:

#-- The key is specified in parens after the array name
set         capital(France) Paris

#-- The key can also be substituted from a variable:
set                  country France
puts       $capital($country)

#-- Setting several elements at once:
array set   capital         {Italy Rome  Germany Berlin}

#-- Retrieve all keys:
array names capital    ;#-- Germany Italy France -- quasi-random order

#-- Retrieve keys matching a glob pattern:
array names capital F* ;#-- France

一個奇特的陣列名稱是 ""(空字串,因此我們可以稱之為“匿名陣列”:),這使得閱讀起來很愉快。

set (example) 1
puts $(example)

請注意,陣列本身不是值。它們可以作為引用傳遞到過程,而不是作為 $capital(這會嘗試檢索值)。dict 型別(從 Tcl 8.5 開始可用)可能更適合這些目的,同時還提供雜湊表功能。

系統變數

[edit | edit source]

在啟動時,tclsh 提供以下全域性變數:

argc
命令列上的引數數量
argv
命令列上的引數列表
argv0
可執行檔案或指令碼的名稱(命令列上的第一個詞)
auto_index
包含從何處載入更多命令的指令的陣列
auto_oldpath
(與 auto_path 相同?)
auto_path
用於搜尋包的路徑列表
env
陣列,反映環境變數
errorCode
最後一個錯誤的型別,或 {},例如 ARITH DIVZERO {divide by zero}
errorInfo
最後一個錯誤訊息,或 {}
tcl_interactive
如果直譯器是互動式的,則為 1,否則為 0
tcl_libPath
庫路徑列表
tcl_library
Tcl 系統庫目錄的路徑
tcl_patchLevel
詳細的版本號,例如 8.4.11
tcl_platform
包含作業系統資訊的陣列
tcl_rcFileName
初始資原始檔的名稱
tcl_version
簡短的版本號,例如 8.4

可以使用臨時環境變數從命令列控制 Tcl 指令碼,至少在包括 Cygwin 的類 Unix 系統中。示例指令碼片段:

set foo 42
if [info exists env(DO)] {eval $env(DO)}
puts foo=$foo

此指令碼通常會報告:

 foo=42

要對其進行遠端控制而無需編輯,請在呼叫之前設定 DO 變數:

DO='set foo 4711' tclsh myscript.tcl

這將明顯報告:

foo=4711

解引用變數

[edit | edit source]

引用是指一個指向另一個事物的東西(如果你不介意我使用這個科學術語)。在 C 語言中,引用透過*指標*(記憶體地址)實現;在 Tcl 中,引用是字串(一切皆為字串),具體來說是變數名,透過雜湊表可以解析(解引用)到它們指向的“另一個事物”。

puts foo       ;# just the string foo
puts $foo      ;# dereference variable with name of foo
puts [set foo] ;# the same

可以使用巢狀的 set 命令多次執行此操作。比較以下 C 和 Tcl 程式,它們執行相同的(微不足道的)工作,並表現出顯著的相似性。

#include <stdio.h>
int main(void) {
  int    i =      42;
  int *  ip =     &i;
  int ** ipp =   &ip;
  int ***ippp = &ipp;
  printf("hello, %d\n", ***ippp);
  return 0;
}

...以及 Tcl

set i    42
set ip   i
set ipp  ip
set ippp ipp
puts "hello, [set [set [set [set ippp]]]]"

C 中的星號對應於 Tcl 中對 set 的呼叫,進行解引用。C 中的 & 運算子沒有對應的運算子,因為在 Tcl 中,宣告引用不需要特殊的標記。這種對應關係並不完美;有四個 set 呼叫,但只有三個星號。這是因為在 C 中提及一個變數是隱式的解引用。在本例中,解引用用於將它的值傳遞給 printf。Tcl 將所有四個解引用都顯式化(因此,如果你只有 3 個 set 呼叫,你會看到 hello,i)。單次解引用非常常用,因此通常縮寫為 $varname,例如

puts "hello, [set [set [set $ippp]]]"

使用 set 的地方 C 使用星號,而最後一個(預設)解引用使用 $。

變數名雜湊表要麼是全域性的,用於在該作用域中評估的程式碼,要麼是區域性於過程的。你仍然可以使用 upvar 和 global 命令“匯入”作用域“更高”的變數的引用。(如果名稱是唯一的,C 中後者是自動的。如果 C 中有相同的名稱,則最內層作用域獲勝)。

變數跟蹤

[編輯 | 編輯原始碼]

Tcl 的一個特殊功能是,你可以將跟蹤與變數(標量、陣列或陣列元素)相關聯,這些變數在可選情況下會在讀取、寫入或取消設定時進行評估。

除錯是其中一個明顯的用途。但還有更多可能性。例如,你可以引入常量,任何嘗試更改其值的嘗試都會引發錯誤。

proc const {name value} {
  uplevel 1 [list set $name $value]
  uplevel 1 [list trace var $name w {error constant ;#} ]
}

const x 11
incr x
can't set "x": constant

跟蹤回撥會附加三個詞:變數名;陣列鍵(如果變數是陣列,否則為空),以及模式。

  • r - 讀取
  • w - 寫入
  • u - 取消設定

如果跟蹤只是一個像上面一樣的單條命令,並且你不想處理這三個,請使用註釋“;#”將它們遮蔽掉。

另一種可能性是將本地物件(或過程)與變數繫結 - 如果變數被取消設定,則物件/過程會被銷燬/重新命名。

華夏公益教科書