Специальные символы

Материал из Egghelp.ru - TCL/TK Eggdrop Wiki
Перейти к: навигация, поиск

Многие скрипты Eggdrop не правильно обрабатывают имена, иденты или текст, содержащие символы, имеющие особое значение в TCL - [ ] { } " \ $. По этой причине на некоторых IRC каналах банятся ники, содержащие символы [ или {.

Этих проблем можно избежать, используя некоторые приемы при написании Tcl скриптов. Есть два золотых правила, которыми не следует пренебрегать при написании скриптов.

Первое золотое правило TCL

Используйте все split и join, необходимые для преобразования между списками и строками.

Лучше всего работать со списками и строками, как если бы они были двумя разными типами данных - использовать все необходимые операции преобразования. Если использовать все необходимые операции - интерпретатор сам обработает все специальные символы, присутствующие в переменных. В этом случае нам не обязательно иметь полное представление о списках TCL, вместо нас об этом позаботится сам интерпретатор.

Например, рассмотрим скрипт, содержащий следующие строки

 bind dcc o tell do_dcc_tell
 proc do_dcc_tell { hand idx arg } {
 set arg [lrange $arg 0 0]

При вызове процедуры первым параметром является ник. Скрипт выдаст ошибку, если ник содержит [ или {. Ошибка будет вызвана при обработке третьей строки скрипта.

Вместо третьей строки можно так же использовать

 set arg [lindex $arg 0]

но и это приведет к ошибкам, если ник содержит другие специальные символы Tcl.

Чтобы понять почему приведенный код ошибочек - давайте заглянем в tcl-commands.doc. Там говорится, что значение переменной arg это "строковый параметр". К тому же lrange и lindex должны использоваться только со списками. Поэтому прежде чем использовать lrange или lindex, мы должны преобразовать строковое значение в список, используя для этого соответствующую функцию split. Чтобы исправить первую версию скрипта мы должны заменить третью строку на следующую

 set arg [lrange [split $arg] 0 0]

Однако и этот вариант не идеален. Во всей последующей процедуре arg будет по-прежнему восприниматься как строка, а не список, к тому же lrange вернет нам список (пусть и состоящий из одного элемента). Завершающим штрихом будет использование функции join для преобразования списка в строку. И так, наша последняя и наиболее правильная версия

 set arg [join [lrange [split $arg] 0 0]]

Так же мы можем доработать альтернативный вариант (к тому же, он более краткий)

 set arg [lindex $arg 0]

используя тот же подход

 set arg [lindex [split $arg] 0]

В этом случае нам нет необходимости использовать join, так как lindex вернет ник, указанный пользователем, который уже является строкой.

Стоит отметить, что если бы мы использовать последний аргумент процедуры do_dcc_tell с именем args, а не arg, ситуация была бы иная. Если процедура Tcl в качестве последнего аргумента использует имя args, аргумент обрабатывается иначе, чем остальные аргументы.

Вот пример скрипта для Eggdrop. Написан он абсолютно правильно.

 bind pub B|B !orderfor pub_orderfor
 proc pub_orderfor {nick uhost hand chan rest} {
   global botnick
   set rest [split $rest]
   if {[llength $rest] < 2} {
     putnotc $nick "Syntax: !orderfor\
     <nick to order something for>\
     <what to order>"
     return 0
   }
   putchan $chan "\001ACTION sets\
   [join [lrange $rest 1 end]] in front of\
   [lindex $rest 0], compliments of $nick.\001"
   return 0
 }

Замечу, что процедуры putnotc и putchan определены в alltools.tcl, который распространяется вместе с дистрибутивом бота.

Исходная версия (которую я взял из скрипта) не содержала

 set rest [split $rest]

вместо этого в ней была строка

 set cmd [string tolower [lindex $rest 0]]

просмотров которую становится понятно, что здесь ошибочно применяется lindex к строковому параметру.

Оригинал скрипта периодически будет выдавать ошибки, если $rest будет содержать особые символы TCL.

Второе золотое правило TCL

Если скрипт содержит команды, которые будут выполнены по истечении таймера, эти команды должны быть оформлены правильно.

List позволяет оформлять такие такие команды в надлежащей форме. Она добавляет бэкслеши (обратные слеши, backslashes) и фигурные скобки (braces) чтобы избежать проблем с любыми специальными символами TCL, которые могут встретиться в команде.

Ниже приведен пример процедуры, где второе золотое правило TCL не соблюдено. Она реализует автоматическое приветствие всех зашедших на канал, если они еще не были поприветствованы ранее в течении трех последних минут.

 bind join - * do_jn_msg
 proc do_jn_msg {nick uhost hand chan} {
   global botnick jn_msg_done
   if {$nick == "X" || $nick == $botnick} {
     return 0
   }
   if {[info exists jn_msg_done($nick:$chan)]} {
     return 0
   }
   set jn_msg_done($nick:$chan) 1
   timer 3 "unset jn_msg_done($nick:$chan)"
   puthelp "NOTICE $nick :Welcome to $chan"
   return 0
 }

Предположим, что ник у нас abc а канал - '#room. Кавычки в команде timer подставляют в выражение значения $nick и $chan

 unset jn_msg_done(abc:#room)

По истечении таймера команда будет успешно выполнена.

Но представим, что вместо ника 'abc у нас будет a[b]c. В результате подстановки значений $nick и $chan команда в кавычках будет выглядеть так

 unset jn_msg_done(a[b]c:#room)

По истечении таймера интерпретатор TCL вначале подставит все необходимые значения в команду jn_msg_done(a[b]c:#room). Затем интерпретатор попробует выполнить подстановку команды [b]. В результате появится ошибка о том, что команды b не существует. Если бы в нашем случае ник был a[[[die]]]c - бот бы обработал и эту команду, в результате чего работа бота была бы завершена.

Чтобы исправить скрипт мы изменим команду в таймере следующим образом

 timer 3 [list unset jn_msg_done($nick:$chan)]

Если в параметре будут какие-либо специальные символы TCL - команда list добавит бэкслеши и/или фигурные скобки, чтобы выражение приняло нормальный вид. Нам абсолютно не обязательно знать какие специальные символы могут попасться. Команда unset, теперь оформленная правильным образом, будет передана в таймер и сработает без ошибок по истечении промежутка времени.

В действительности, в случае с a[b]c, команда list просто добавляет необходимые фигурные скобки. Если ник будет a[b]c{ - она так же добавит бэкслеши, чтобы экранировать фигурную скобку в конце ника. Но нам не обязательно знать все это. Достаточно просто правильным образом использовать команду list и позволить интерпретатору произвести необходимые подстановки.

Чаще всего я встречался с проблемами, аналогичной выше указанной, только там использовался не $nick пользователя, а $uhost'. Userhost так же может содержать специальные символы TCL, и проблема с ними решается точно таким же способом - корректным использованием команды list.

Другие методы решения проблем, связанных со специальными символами TCL

Так же есть и другие методы, позволяющие избежать проблем со специальными символами TCL.

Например, некоторые скрипты фильтруют входящие данные процедурами типа этой:

 proc filt {data} {
   regsub -all -- \\\\ $data \\\\\\\\ data
   regsub -all -- \\\[ $data \\\\\[ data
   regsub -all -- \\\] $data \\\\\] data
   regsub -all -- \\\} $data \\\\\} data
   regsub -all -- \\\{ $data \\\\\{ data
   regsub -all -- \\\" $data \\\\\" data
   return $data
 }

Подобный фильтр действительно может порой помочь со специальными символами TCL в плохих скриптах Eggdrop. Но результат и эффективность зависят от контекста скрипта. Данный фильтр может помочь решить проблемы с некоторыми специальными символами, но нарушить работоспособность и логику скрипта в целом.

Если в скрипте есть проблемы со специальными символами TCL - лучше исправить их корректным способом, учитывая два золотых правила, чем использовать фильтры подобно приведенному выше, что может работать далеко не во всех ситуациях или даже вызвать новые ошибки

Важное замечание об args

Если args расположен как последний аргумент процедуры TCL - все введенные данные будут обрабатываться как один элемент. Но здесь стоит объяснить про некоторые неясности, связанные с процедурами, вызываемыми командой bind. В одном из скриптов я видел примерно такой код:

 bind dcc - m2f dcc_calc_m2f
 proc dcc_calc_m2f {hand idx args} {
   if {[llength $args] == "1"} {

Скорее всего автор скрипта думал, что если пользователь наберет

  .m2f aaa bbb ccc

то значение args будет списком из трех элементов - aaa, bbb и ccc - и, поэтому, результатом [llength $args] будет 3.

В действительности же значением args будет список] из одного элемента и этим элементом будет строка aaa bbb ccc. Какую бы строку не ввел пользователь - результатом [llength $args] всегда будет 1.

Аналогично, значением [lindex $args 0] будет не aaa, а строка aaa bbb ccc.

Триггеры Eggdrop типа pub и msg ведут себя аналогичным образом.

Вышеуказанные свойства команды bind бота Eggdrop проверены на боте версии 1.6.6.