Tcl был создан Джоном Оустерхаутом в качестве скриптового языка в своих инструментах проектирования электрических цепей. В 1997 году за разработку языка Tcl автор получил ACM Software System Award. Tcl может использоваться и как встраиваемый скриптовый язык, и как язык программирования общего назначения. Кроме того, он может быть использован как библиотека в программах на C, даже в случаях когда не требуется написание скриптов, поскольку Tcl может предоставить программе на C различные типы данных, такие как динамические строки, списки и хэш-таблицы. Также с помощью этой библиотеки возможно использовать форматирование строк, операции с файловой системой, работу с кодировками и динамически загружаемые библиотеки. К другим особенностям Tcl относятся:
-
Удобный кроссплатформенный API для работы с сетью
-
Поддержка виртуальной файловой системы (VFS)
-
Стекируемые каналы ввода-вывода
-
Асинхронность в ядре языка
-
Поддержка корутин
-
Простая и надёжная модель потоков выполнения
Tcl имеет много общего с Lisp, но в отличие от списков, в Tcl «валютой» языка
являются строки. Все значения являются строками. Список в Tcl это просто строка в
определённом формате, а тело процедуры (скрипт) это ещё одна строка, а не блок.
С целью увеличения производительности, интерпретатор Tcl использует кэшированные
внутренние представления различных типов данных. Например, рутины (routines), работающие
со списками, фактически используют внутреннее представление списков, а интерпретатор
Tcl обновляет строковое представление в том случае если оно используется в скрипте.
В Tcl используется подход copy-on-write, позволяющий оперировать большими объёмами
данных без дополнительного оверхеда. Процедуры в Tcl автоматически компилируются
в байткод, кроме случаев когда в процедуре используются динамические рутины, такие
как uplevel
, upvar
и trace
.
Программировать на Tcl приятно. Его находят привлекательным хакеры, которым интересны Lisp, Forth или Smalltalk, а также инженеры и учёные, которым просто необходим гибкий инструмент для выполнения их задач. В Tcl языковые конструкции, включая циклы и математические операторы, представлены в виде изменяемых рутин, в отличие от других языков программирования, где они закреплены в синтаксисе, что позволяет синтаксису Tcl не мешать работать с предметной областью проекта. Синтаксис Tcl в этом смысле даже более минималистичен чем у Lisp.
1#! /bin/env tclsh
2
3###############################################################################
4## 1. Рекомендации
5###############################################################################
6
7# Tcl это не shell или C! Этот момент требует уточнения, поскольку привычки
8# написания shell-скриптов почти работают в Tcl и часто люди начинают
9# изучать Tcl со знанием синтаксиса других языков. Поначалу это работает, но
10# когда скрипты становятся сложнее, наступает фрустрация.
11
12# Фигурные скобки {} в Tcl используются не для построения блоков кода или
13# списков, а как механизм экранирования (quoting) для кода. Фактически в Tcl
14# нет ни списков, ни блоков кода. Фигурные скобки использутся для
15# экранирования специальных символов и потому подходят для представления
16# тела процедур и строк, которые должны интерпретироваться как списки.
17
18
19###############################################################################
20## 2. Синтаксис
21###############################################################################
22
23# Скрипт состоит из команд, разделённых символами перевода строки или символами
24# точки с запятой. Каждая команда представляет собой вызов рутины. Первое слово
25# это имя вызываемой рутины, а последующие слова это аргументы. Слова разделены
26# пробелами. Так как каждый аргумент это слово в команде, он является строкой и
27# может быть неэкранирован:
28set part1 Sal
29set part2 ut; set part3 ations
30
31
32# символ доллара используется для подставления значения переменных:
33set greeting $part1$part2$part3
34
35
36# Когда "set" получает только имя переменной, возвращается значение переменной:
37set part3 ;# Возвращает значение переменной
38
39
40# Содержимое квадратных скобок заменяется на результат выполнения:
41set greeting $part1$part2[set part3]
42
43
44# Встроенный таким образов скрипт может состоять из нескольких команд, но
45# результат подстановки определяется последней командой:
46set greeting $greeting[
47 incr i
48 incr i
49 incr i
50]
51puts $greeting ;# Выведет "Salutations3"
52
53# Каждое слово в команде является строкой, включая имя рутины, поэтому
54# подстановки могут быть использованы и таким образом:
55set action pu
56
57# следующие команды эквивалентны:
58puts $greeting
59${action}ts $greeting
60[set action]ts $greeting
61
62
63# Обратный слэш экранирует специальные символы:
64set amount \$16.42
65
66
67# и он же используется для ввода специальных символов:
68puts lots\nof\n\n\n\n\n\nnewlines
69
70
71# Слово в фигурных скобках никак не интерпретируется и в нём не работают
72# никакие подстановки, за исключением экранирования закрывающей скобки:
73set somevar {
74 Это литерал знака $, а это \} экранированная закрывающая скобка
75}
76
77
78# В слове внутри двойных кавычек, пробельные символы теряют своё
79# специальное значение:
80set name Neo
81set greeting "Hello, $name"
82
83
84# Имя переменной может быть любой строкой:
85set {first name} New
86
87
88# Фигурные скобки используются для доступа к переменным с составными именами:
89set greeting "Hello, ${first name}"
90
91
92# "set" всегда можно использовать вместо подстановки переменной:
93set greeting "Hello, [set {first name}]"
94
95
96# Чтобы "распаковать" список в команду используется оператор расширения "{*}"
97# Эти две команды эквивалентны:
98set name Neo
99set {*}{name Neo}
100
101
102# Массив это особая переменная, являющаяся контейнером для других переменных.
103set person(name) Neo
104set person(destiny) {The One}
105set greeting "Hello, $person(name)"
106
107
108# "variable" может быть использована для объявления или установки переменных.
109# В отличие от "set", которая использует глобальное и локальное пространство
110# имён, "variable" работает только с локальным пространством:
111variable name New
112
113
114# "namespace eval" создаёт новое пространство имён, если его не существует.
115# Пространство имён может содержать рутины и переменные:
116namespace eval people {
117 namespace eval person1 {
118 variable name Neo
119 }
120}
121
122
123# Двумя или более двоеточиями в именах переменных отделяется название
124# пространства имён:
125namespace eval people {
126 set greeting "Hello $person1::name"
127}
128
129# Два или более двоеточия также отделяют название пространства имён
130# в имени рутины:
131proc people::person1::speak {} {
132 puts {I am The One.}
133}
134
135# Полные(fully-qualified) имена начинаются с двух двоеточий:
136set greeting "Hello $::people::person1::name"
137
138
139
140###############################################################################
141## 3. Больше никакого синтаксиса
142###############################################################################
143
144# Все остальные функции реализованы посредством рутин. С этого момента и далее
145# больше нет нового синтаксиса. Всё остальное что можно изучить о Tcl это
146# поведение отдельных рутин и какие значения они присваивают своим аргументам.
147
148
149
150###############################################################################
151## 4. Переменные и пространства имён
152###############################################################################
153
154# Каждая переменная и рутина связана с пространством имён.
155
156# Чтобы получить интерпретатор, который не может сделать ничего, достаточно
157# удалить глобальное пространство имён. Особой пользы в этом нет, но это хорошо
158# иллюстрирует природу Tcl. Фактически имя глобального пространства имён это
159# пустая строка, но единственный способ представить её -- в виде полного имени:
160proc delete_global_namespace {} {
161 namespace delete ::
162}
163
164# Поскольку "set" всегда учитывает и глобальное, и текущее пространства имён,
165# более безопасно использовать "variable" чтобы объявить новую переменную или
166# задать значение переменной. Если переменная с именем "name" уже существует
167# в глобальном пространстве имён, использование "set" задаст значение
168# глобальной переменной, тогда как "variable" работает только с текущим
169# пространством имён.
170
171namespace eval people {
172 namespace eval person1 {
173 variable name Neo
174 }
175}
176
177# После объявления переменной в пространстве имён, [set] видит её, а не
178# одноимённую переменную в глобальном пространстве имён:
179
180namespace eval people {
181 namespace eval person1 {
182 variable name
183 set name Neo
184 }
185}
186
187# Но если "set" приходится создать новую переменную, он всегда делает это
188# с учётом текущего пространства имён:
189unset name
190namespace eval people {
191 namespace eval person1 {
192 set name neo
193 }
194
195}
196set people::person1::name
197
198
199# Абсолютное имя всегда начинается с имени глобального пространства имён, то
200# есть с пустой строки, за которой следует два двоеточия:
201set ::people::person1::name Neo
202
203
204# В пределах процедуры "variable" связывает перменную в текущем пространстве
205# имён с локальной областью видимости:
206namespace eval people::person1 {
207 proc fly {} {
208 variable name
209 puts "$name is flying!"
210 }
211}
212
213
214
215
216###############################################################################
217## 5. Встроенные рутины
218###############################################################################
219
220# Математические операции можно выполнять при помощи "expr":
221set a 3
222set b 4
223set c [expr {$a + $b}]
224
225# Поскольку "expr" самостоятельно занимается подстановкой значений переменных,
226# математическое выражение нужно оборачивать в фигурные скобки чтобы отключить
227# подстановку значений переменных интерпретатором Tcl.
228# Подробнее об этом можно прочесть здесь:
229# "https://wiki.tcl-lang.org/page/Brace+your+expr-essions"
230
231
232# "expr" выполняет подстановку переменных и результатов команд:
233set c [expr {$a + [set b]}]
234
235
236# "expr" предоставляет разные математические функции:
237set c [expr {pow($a,$b)}]
238
239
240# Математические операторы сами по себе доступны в виде рутин в
241# пространстве имён ::tcl::mathop
242::tcl::mathop::+ 5 3
243
244# Рутины могут быть импортированы из других пространств имён:
245namespace import ::tcl::mathop::+
246set result [+ 5 3]
247
248
249# Не числовые значения должны быть квотированы. Такие операторы как "eq"
250# Могут быть использованы чтобы провести строковое сравнение:
251set name Neo
252expr {{Bob} eq $name}
253
254# Общие операторы сравнения тоже работают со строками если числовое значение
255# операнда недоступно:
256expr {{Bob} == $name}
257
258
259# "proc" создаёт новые рутины:
260proc greet name {
261 return "Hello, $name!"
262}
263
264# можно указать несколько параметров:
265proc greet {greeting name} {
266 return "$greeting, $name!"
267}
268
269
270# Как было отмечено ранее, фигурные скобки не обозначают блок кода.
271# Любое значение, даже третий аргумент "proc", является строкой.
272# Предыдущая команда может быть переписана без использования фигурных скобок:
273proc greet greeting\ name return\ \"\$greeting,\ \$name!\"
274
275
276
277# Если последний параметр называется "args", все дополнительные аргументы,
278# переданные рутине, собираются в список и передаются как "args":
279proc fold {cmd first args} {
280 foreach arg $args {
281 set first [$cmd $first $arg]
282 }
283 return $first
284}
285fold ::tcl::mathop::* 5 3 3 ;# -> 45
286
287
288# Условное выполнение тоже реализовано как рутина:
289if {3 > 4} {
290 puts {This will never happen}
291} elseif {4 > 4} {
292 puts {This will also never happen}
293} else {
294 puts {This will always happen}
295}
296
297
298# Циклы реализованы как рутины. Первый и третий аргумент для "for"
299# обрабатываются как скрипты, а второй аргумент как выражение:
300set res 0
301for {set i 0} {$i < 10} {incr i} {
302 set res [expr {$res + $i}]
303}
304unset res
305
306
307# Первый аргумент для "while" тоже обрабатывается как выражение:
308set i 0
309while {$i < 10} {
310 incr i 2
311}
312
313
314# Список это строка, а элементы списка разделены пробелами:
315set amounts 10\ 33\ 18
316set amount [lindex $amounts 1]
317
318# Если элемент списка содержит пробел, его надо экранировать:
319set inventory {"item 1" item\ 2 {item 3}}
320
321
322# Хорошая практика использовать списковые рутины для обработки списков:
323lappend inventory {item 1} {item 2} {item 3}
324
325
326# Фигурные скобки и бэкслеш могут быть использованы чтобы хранить более
327# комплексные структуры внутри списков. Список выглядит как скрипт, за
328# исключением того, что перевод строки и точка с запятой теряют своё
329# специальное значение, а также не производится подстановка значений.
330# Эта особенность Tcl называется гомоиконичность
331# https://ru.wikipedia.org/wiki/Гомоиконичность
332# В приведённом списке есть три элемента:
333set values {
334
335 one\ two
336
337 {three four}
338
339 five\{six
340
341}
342
343
344# Поскольку как и все значения, список является строкой, строковые
345# операции могут выполняться и над списком, с риском повреждения:
346set values {one two three four}
347set values [string map {two \{} $values] ;# $values больше не \
348 правильно отформатированный список
349
350
351# Безопасный способ работать со списками — использовать "list" рутины:
352set values [list one \{ three four]
353lappend values { } ;# добавить символ пробела как элемент в список
354
355
356# Использование "eval" для вычисления значения скрипта:
357eval {
358 set name Neo
359 set greeting "Hello, $name"
360}
361
362
363# Список всегда можно передать в "eval" как скрипт, содержащий одну команду:
364eval {set name Neo}
365eval [list set greeting "Hello, $name"]
366
367
368# Следовательно, когда используется "eval", используйте "list" чтобы собрать
369# необходимую команду:
370set command {set name}
371lappend command {Archibald Sorbisol}
372eval $command
373
374
375# Частая ошибка: не использовать списковые функции для построения команды:
376set command {set name}
377append command { Archibald Sorbisol}
378try {
379 eval $command ;# Здесь будет ошибка, превышено количество аргументов \
380 к "set" в {set name Archibald Sorbisol}
381} on error {result eoptions} {
382 puts [list {received an error} $result]
383}
384
385# Эта ошибка запросто может произойти с "subst":
386
387set replacement {Archibald Sorbisol}
388set command {set name $replacement}
389set command [subst $command]
390try {
391 eval $command ;# Та же ошибка, лишние аргументы к \
392 {set name Archibald Sorbisol}
393} trap {TCL WRONGARGS} {result options} {
394 puts [list {received another error} $result]
395}
396
397
398# "list" корректно форматирует значение для подстановки:
399set replacement [list {Archibald Sorbisol}]
400set command {set name $replacement}
401set command [subst $command]
402eval $command
403
404
405# "list" обычно используется для форматирования значений для подстановки в
406# скрипты, вот несколько примеров:
407
408
409# "apply" вычисляет список из двух элементов как рутину:
410set cmd {{greeting name} {
411 return "$greeting, $name!"
412}}
413apply $cmd Whaddup Neo
414
415# Третий элемент может быть использован для указания пространства имён рутины:
416set cmd [list {greeting name} {
417 return "$greeting, $name!"
418} [namespace current]]
419apply $cmd Whaddup Neo
420
421
422# "uplevel" вычисляет скрипт на уровень выше в списке вызовов:
423proc greet {} {
424 uplevel {puts "$greeting, $name"}
425}
426
427proc set_double {varname value} {
428 if {[string is double $value]} {
429 uplevel [list variable $varname $value]
430 } else {
431 error [list {not a double} $value]
432 }
433}
434
435
436# "upvar" связывает переменную на текущем уровне вызовов с переменной на
437# более высоком уровне:
438proc set_double {varname value} {
439 if {[string is double $value]} {
440 upvar 1 $varname var
441 set var $value
442 } else {
443 error [list {not a double} $value]
444 }
445}
446
447
448# Избавляемся от встроенной рутины "while" и используем "proc" чтобы написать
449# свою версию:
450rename ::while {}
451# обработка оставлена как упражнение:
452proc while {condition script} {
453 if {[uplevel 1 [list expr $condition]]} {
454 uplevel 1 $script
455 tailcall [namespace which while] $condition $script
456 }
457}
458
459
460# "coroutine" создаёт новый стек вызовов, новую рутину для входа в этот стек
461# и вызывает эту рутину. "yield" приостанавливает вычисления в этом стеке и
462# возвращает управление вызывавшему стеку:
463proc countdown count {
464 # отправить что-нибудь обратно создателю корутины, фактически
465 # останавливая стек вызовов на время.
466 yield [info coroutine]
467
468 while {$count > 1} {
469 yield [incr count -1]
470 }
471 return 0
472}
473coroutine countdown1 countdown 3
474coroutine countdown2 countdown 5
475puts [countdown1] ;# -> 2
476puts [countdown2] ;# -> 4
477puts [countdown1] ;# -> 1
478puts [countdown1] ;# -> 0
479catch {
480 puts [coundown1] ;# -> invalid command name "countdown1"
481} cres copts
482puts $cres
483puts [countdown2] ;# -> 3
484
485
486# Стеки корутин могут передавать контроль друг другу:
487
488proc pass {whom args} {
489 return [yieldto $whom {*}$args]
490}
491
492coroutine a apply {{} {
493 yield
494 set result [pass b {please pass the salt}]
495 puts [list got the $result]
496 set result [pass b {please pass the pepper}]
497 puts [list got the $result]
498}}
499
500coroutine b apply {{} {
501 set request [yield]
502 while 1 {
503 set response [pass c $request]
504 puts [list [info coroutine] is now yielding]
505 set request [pass a $response]
506 }
507}}
508
509coroutine c apply {{} {
510 set request [yield]
511 while 1 {
512 if {[string match *salt* $request]} {
513 set request [pass b salt]
514 } else {
515 set request [pass b huh?]
516 }
517 }
518}}