Quiz Memorial Forum

Welcome Guest ( Log In | Register )

 
Reply to this topicStart new topic
> Tcl-скрипты и специальные символы
Kreon
post Sep 16 2007, 01:42 PM
Post #1


Developer
******

Group: Root Admin
Posts: 396
Joined: --
From: Внешние пределы
Member No.: 2



Как писать Tcl-скрипты, которые не сбиваются на спецсимволах
(перевод статьи "How to write eggdrop scripts that won't choke on special characters")

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

Подобных проблем можно полностью избежать, если писать верный Tcl-код. Необходимо соблюдать лишь два золотых правила.

Первое золотое правило Tcl для eggdrop-скриптов
Необходимо включать все split'ы и join'ы, необходимые для преобразования между списками и строками.

Лучше всего представлять все списки и строки как переменные двух совершенно разных типов и всегда помещать в код все split'ы и join'ы для конвертации между ними. Если мы поступим таким образом, то Tcl-интерпретатор сам проведет все необходимые действия со спецсимволами. Большой плюс заключается в том, что нам нет необходимости понимать, как должны быть представлены значения в списках - вместо этого мы просто даем возможность интерпретатору позаботиться об этом за нас.

К примеру, однажды мне попался скрипт, содержавший три строки, подобные этим:
CODE
bind dcc o tell do_dcc_tell
proc do_dcc_tell { hand idx arg } {
set arg [lrange $arg 0 0]

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

Автор скрипта мог бы написать вместо неё
CODE
set arg [lindex $arg 0]
но это так же может привести к ошибкам с никами, содержащими спецсимволы.

Причины того, почему приведенный выше код некорректен, мы можем прочесть в tcl-commands.doc. Там говорится, что значение arg будет представлять собой "строку аргументов", тогда как команды lrange и lindex должны применяться только к спискам. Вследствие этого, прежде, чем мы сможем использовать lrange или lindex, мы должны преобразовать строку в список, что мы можем сделать с помощью команды split. Тогда наша строка будет выглядеть следующим образом:
CODE
set arg [lrange [split $arg] 0 0]

Однако, это все ещё неправильно. Оставшаяся часть процедуры предполагает использование arg как строки, а не списка, тогда как lrange возвращает именно список (в данном случае - всего из одного элемента). Поэтому мы должны использовать join для преобразования переменной обратно в строку. В итоге наша последняя, правильная версия будет выглядеть так:
CODE
set arg [join [lrange [split $arg] 0 0]]

Альтернативно (и, как видно, более аккуратно) мы можем записать верно и вторую версию, т.е.:
CODE
set arg [lindex $arg 0]
как
CODE
set arg [lindex [split $arg] 0]

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

Заслуживает внимания тот факт, что если бы последний аргумент do_dcc_tell мы назвали бы args вместо arg, то ситуация была бы другой. Если в Tcl-процедуре последний аргумент носит специальное название args, то интерпретатор представляет его как список из одного элемента, представляющего собой строку со всеми необходимыми экранированиями спецсимволов. Но об этом ниже.

Вот другой пример правильно написанного скрипта:
CODE
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, который распространяется вместе с eggdrop'ом.

Оригинальная версия этого скрипта, которую я скачал из Tcl-библиотеки, не содержала
CODE
set rest [split $rest]
но в ней была строка
CODE
set cmd [string tolower [lindex $rest 0]]
где ясно, что мы делаем ошибку, пытаясь применить lindex к строке.

Такой скрипт иногда давал бы неверные ответы, если вводимые параметры содержали специальные Tcl-символы, как например обратный слеш '\'.

Представленная выше версия была исправлена, и также упрощена для большей наглядности.


Второе золотое правило Tcl для eggdrop-скриптов
Когда необходимо отсрочить выполнение команды, нужно убедиться, что она имеет верную форму.

Нам может понадобиться составить команду, которая должна быть выполнена не сразу же, а спустя какое-либо время. Например, при завершении таймера или при вызове её с помощью команды eval. Команда list предоставляет подходящий путь сохранения команды в правильной форме. Она добавляет обратные слеши и фигурные скобки для экранирования спецсимволов, которые могут присутствовать в аргументах.

Ниже представлена процедура, в которой нарушено выполнение второго золотого правила. Она дает автоприветствие всем посетителям канала, если они уже не получили автоприветствие за последние три минуты.

CODE
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
}

Предположим, что nick имеет значение abc, и chan имеет значение #room. Двойные кавычки вызовут подстановку переменных $nick и $chan, и команда, переданная таймеру, будет иметь следующий вид:
CODE
unset jn_msg_done(abc:#room)

Когда время таймера выйдет, команда unset выполнится без проблем.

Но теперь предположим, что вместо значения abc, nick имеет значение a[x]c. Двойные кавычки вызовут подстановку переменных $nick и $chan, и команда, переданная таймеру, будет выглядеть как
CODE
unset jn_msg_done(a[x]c:#room)

Когда таймер закончится, Tcl-интерпретатор попытается выполнить один круг подстановок на jn_msg_done(a[x]c:#room), так как первым шагом при выполнении любой команды является подстановка значений на слова команды.

Таким образом, интерпретатор попытается выполнить подстановку команды [x]. Это даст нам сообщение об ошибке, так как команда с названием x не определена. Как можно догадаться, если бы ник был a[die]b, то была бы выполнена команда die, выключающая бота.

Чтобы исправить скрипт, мы должны заменить команду таймера на следующую:
CODE
timer 3 [list unset jn_msg_done($nick:$chan)]

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

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

Когда бы мы не захотели построить команду, которую необходимо выполнить позднее, команда list может быть использована для создания команды в верной форме.

Чаще в похожих на приведенный выше пример скриптах, которые я видел, используется $uhost вместо $nick. Но uhost'ы тоже могут содержать специальные символы Tcl, как и ники, хотя и реже. Поэтому там тоже может возникнуть точно такая же проблема, как и выше, и разрешена она может быть так же с помощью команды list.

Вообще говоря, в скриптах лучше использовать $uhost вместо $nick. Я привел пример с применением $nick, поскольку читателю будет так проще экспериментировать со скриптом, скопировав его в Tcl-файл.


Другие способы разрешения проблемы
Есть и другие способы, которые иногда используются для избежания проблем, которые могут случиться при попадании специальных символов в аргументы процедур.

Например, некоторые скрипты используют для фильтрации ввода нечто подобное:
CODE
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
}

Такой фильтр, безусловно, может иногда вылечить проблему спецсимволов, которая может произойти в плохо написанных скриптах. Но поможет он или нет зависит от самого скрипта. Он может не помочь, или решить проблему только отчасти. Более того, добавление подобного фильтра в отдельных случаях может даже вызвать другие проблемы.

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

Подробнее о специальном параметре args
Мы можем использовать args как название последнего аргумента, что позволит Tcl-процедуре принять произвольное число параметров. Но этот же факт может вызвать некоторое недоразумение, если процедура вызвана с помощью встроенных в eggdrop команд bind.

В скрипте, который я скачал, я увидел часть кода, подобную следующей:
CODE
bind dcc - m2f dcc_calc_m2f
proc dcc_calc_m2f {hand idx args} {
  if {[llength $args] == "1"} {

Автор скрипта, несомненно, считал, что если пользователь напечатает
.m2f aaa bbb ccc
то значением аргументов будет список из трех элементов - 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 проверены тестированием на eggdrop 1.6.18.


Автор: © 2001, 2002, 2003, Pierre
Перевод: © 2007, Kreon
Постоянная ссылка в разделе документации: url


--------------------
Не понял сам - не дай понять другому
User is offlineProfile CardPM
Go to the top of the page
+Quote Post

Reply to this topicStart new topic
2 User(s) are reading this topic (2 Guests and 0 Anonymous Users)
0 Members:

 



Lo-Fi Version Time is now: 28th March 2024 - 01:16 PM
Design by GPF © 2006