Программирование - EggDrop боты, IRC и TCL.
РазноеКак отлаживать куски программы в отдельном месте, не
мешая боту?
Удобно отлаживать кусочки текста тем, кто плохо изучил TCL следующим
образом. Поместите куда угодно функцию и к ней бинд: bind dcc n test testproc
proc testproc {hand idx arg} {
# (хотя объявлять заранее не нужно, lappend сама создаст)
set a ""
# записываем 3 элемента в список
lappend a AAAAA
lappend a BBBBB
lappend a CCCCC
# выводим разультат на экран
putlog "**test**: $a"
# удаляем первый элемент
set a [lrange $a 1 end]
# выводим результат на экран
putlog "**test**: $a"
}
Далее зайдите в DCC-чат как владелец бота и выполните команду
".test". Вы сразу увидите ее результат.
Если на вашем не
запрещена команда .tcl (рекомендуется
включить для отладок), то можно обойтись без bind ..
команда: в чате бота пишите любую TCL команду
.tcl testproc [аргументы] или целый скрипт
.tcl команда1; команда2; ... Иногда
бывают ТАКИЕ глюки, что руки опускаются...
Да, к сожалению, TCL иногда не пишет об ошибках и не выполняет
программу. Пример: set asd 1
after 1000 testtest
proc testtest {} {
putlog "test 1: ...."
# с этого места нихера работать не будет и в лог (чат бота) ошибки не идут
putlog "test 2: $asd"
}
Глюк проявляется при after + необъявленная переменная. Если
объявить переменную гобальной, либо вызавать testtest напрямую (без
after), то TCL пишет ошибку. Важный совет при установке/проверке
флагов юзеру
matchattr $hand X $chan -- не
правильно matchattr $hand |X $chan --
правильно matchattr $hand -|X $chan --
правильно Если сделать неправильно, проверяться будут глоабальные
флаги, а не флаги канала. Тоже самое с chattr - не забывайте писать
вертикальную черту RAW: получение ВСЕГО текста от сервера. В
частности, как узнать о коннекте/дисконнекте к серверу, т.к. стандарные
средсва бота этого не позволяют.
Все сообщения, что шлет сервер, имеют свой номер. Например, в ответ
на /whois сервер шлет 4 или 5 raw-сообщений с номерами от 311 до 318. В
случее ошибочной команды - 421. Когда сервер шлет информацию, что кто-то
сменил топик или дал кому-то опа, сервер тоже на самом деле шлет
raw-сообщение, только такие сообщения не нужно отлавливать, они хорошо
документированы и принимаются по команде типа "bind
mode..". Чтобы самостоятельно узнать, какому номеру
соответствует нужное вам действие, постройте bind для всевозможных
номеров (на каждый номер сообщения нужен 1 bind). Функция выглядит так:
bind raw - 1 a
bind raw - 2 a
bind raw - 3 a
.........
.........
proc a { from key msg } {
putlog "RAW: $from | $key | $msg"
}
| Чтобы сгенерировать
такой список (не руками, конечно), напишите пару строк на любом языке,
хоть на TCL. Мне быстрее это было сделать на PHP:
< ? for ($i=0; $i<500; $i++) { echo "bind raw - $i a\n"; } ? >
proc a { from key msg } {
putlog "RAW: $from | $key | $msg"
}
| Готовый код тут
(raw.tcl). К запущенному боту подключать как .tcl
source путь/raw.tcl
Вставьте это в бота,
сделайте .rehash и все неописанные ботом
сообщения будут попадать в лог, что можно наблюдать в чате. Пишем любую
команду, наример: .tcl putserv "WHOIS dima"
(у вас .tcl [любой-TCL-код] скорее всего не
работает, т.к. закоментирована в eggdrop.conf по соображению
безопасности - раскомментируйте ее на время экспериментов). В ответ вы
увидите результат и все дальше сами поймете.
Краткая справка: При коннекте к серверу:
251 | TAHK :There are 1 users and 9 invisible on 1 servers
254 | TAHK 2 :channels formed
255 | TAHK :I have 10 clients and 0 servers
WHOIS TAHK
311 | TAHK TAHK ~TAHK 212.193.6.215 * : !>>> HELP HTTP://BOT.NET.RU <<
Будьте осторожны со временем [unixtime]
В ботах, во премя выполненения какого-то события время неподвижно,
т.е. функция [unixtime] все время
возвращает одно и тоже число. Проверить это просто:
.tcl putlog "1: [unixtime]"; benchmark 100000 { append s "s" } ; putlog "2: [unixtime]";
|
В примере функция benchmark 100'000 раз выполняет тестовый скрипт.
Длиться это около 10 секунд, а [unixtime]
не меняется. Выход простой - используйте в таких особых случаях
[clock second] Она вернет тоже, что и
[unixtime], т.е. компьютерное время, но
зато она не блокируется на время обработки событий в боте.
Кстати, если вам не лень писать 2 слова, вместо одного,
программа будет работать быстрее, вот тесты: .tcl benchmark 10000 { set a [unixtime] }
All time: 0.839 sec. One cycle time: 0.000083 sec (Check time ~ 1 sec)
.tcl benchmark 10000 { set a [clock seconds] }
All time: 0.614 sec. One cycle time: 0.000061 sec (Check time ~ 0 sec)
Как видно, [clock seconds] на треть
быстрее.
Где *вечно* хранить кучу разных переменных + как
зафиксировать RESTART/REHASH?
С начала, о сути проблемы. Часто возникает необходимость хранить
свои собственные переменные/массивы/списки так, чтобы они не стирались
при перезапуске бота, rehash и restart.
Решение проблемы
rehash. Допусим, нам надо иметь глобальную переменную $x, которая
не должна пропадать при .rehash и в начале (при старте бота) там
долно быть наше нужное значение, к примеру строка "привет". Для того,
чтобы переменная не потерялась при .rehash, используйте такой код:
# в глобальной области программы (не внутри какой-либо функции):
if {![info exists x]} {set x "привет"}
|
При .rehash бот перегружает все скрипты
(уничтожив старые функции и добавив выполнив код повтороно, т.е. добавив
новые), но не уничтожает объявленные глобальные переменные, активные
таймеры (after ms script) и т.п. результат
деятельности программы. А при .restart, как
и при запуске бота, и память, и скрипты, и все остальное уничтожается.
Т.е. если вы добавили новые скомпилированные модули, то они
перезагрузятся. (Чтобы зафиксировать коннект к серверу - смотрите главу
о RAW)
Если подумать над сказанным выше (и куском кода), то можно
сделать 2 простых вывода:
- чтобы зафиксировать событие запуска бота или .restart, нужно
использовать приведенный выше пример, а вместо "set x
..." вызывать процедуру
bot_start, в которой поместить все то,
что должен делать бот при старте. Например, туда можно поместить
set x "привет" (не забыв написать
global x в начале функции) и кучу других
необходимых действий.
- чтобы зафиксировать .rehash нужно
сделать аналогично, только не проверять на наличие какой-либо
переменной, а прямо в глобальной области любого .tcl скрипта вызвать
функцию bot_rehash и в ней разместить все
необходимые действия
Итак, после этих знаний вернемся к вопросу, где длительно хранить
свои переменные. Самое простое решение - это хранение в XTRA полях бота.
Пример:
setuser XTRA <название переменной> <значение переменной>
|
Хранить это можно как у всех пользователей, так и у какого-то одного
(либо персональюную информацию, либо общую). Т.е. для общей инфомарции
можно завести мнимого юзера -my, и уже в
его XTRA поля писать разные данные. Назвать данного пользователя надо с
"-", чтобы сложнее было его испортить. Например, нужно поставить
заглушку на вход пользователей с именем, начинающимся на "-", и не
давать удалять их командой .-user.
Все было бы хорошо, но такой способ имеет существенные
ограничения - размер переменных не бесконечен, а ограничен примерно в
1000 байт. Поэтому вариант хранения в XTRA полях не подходит для больших
данных, которых, увы, много.
Решение ниже.
#
# инициализация массива MyVar для постоянного хранения данных
#
proc init_myvar {} {
global myvar
set name "myvar.txt"
if {![file exists $name]} {
catch {
set g [open $name w+]
puts $g "test test"
close $g
}
}
catch {
set g [open $name r]
while {![eof $g]} {
set s [gets $g]
set myvar([lindex $s 0]) [sminus $s 1]
}
close $g
}
if {![info exists myvar(test)]} {
putlog "File $name (any variables) not read!"
die "Can't read myvar file"
}
}
if {![info exists init_myvar]} {
set init_myvar 1
init_myvar
}
#
# чтение из массива MyVar
#
proc gv {name} {
global myvar
if {[info exists myvar($name)]} {return $myvar($name)}
return ""
}
#
# запись в массив MyVar + сохранение на диск
#
proc sv {name value} {
global myvar
set myvar($name) $value
if {$myvar($name)==""} {unset myvar($name)}
set ok 0
catch {
set g [open "myvar.txt" w+]
foreach a [array names myvar] {
if {[strlen $a]>0} {
puts $g "$a $myvar($a)"
}
}
close $g
set ok 1
}
if {$ok!=1} {die "Can't save myvar"}
}
|
Описание.
Как видите, кусок текста if {![info exists init_myvar]} {
set init_myvar 1
init_myvar
}
проверяет, не произошел ли старт бота или .restat по
команде, и если да - вызывает функцию загрузки сохраненный данных.
Работает пример просто. Для записи/чтения чего-то в область
постоянного хранения нужно испрользовать sv
и gv (сокращение 'set variable' и 'get
variable'):
# сохранить данные
sv <название переменной> <значение переменной>
# пример записи переменной $mypass под именем 'pass' в память:
sv pass $mypass
# загрузить данные
set <куда загрузить> [gv <название переменной>]
# пример чтения из памяти переменной под названием 'pass' и запись
# ее в переменную $mypass
set mypass [gv pass]
|
Программа хранит данные в памяти и записывает при изменении их на
диск. Если ей неудается создать/прочитать файл для храниения
myvar.txt, то бот сразу завершает работу,
выводя сообщение на экран (если ошибка проиходит при старте) и в логи.
Если записать в переменную пустую строку (""), то
переменная из области памяти удалется. Если попробовать прочитать
несуществующую переменную, вам вернут пустую строку.
Вы можете
хранить любые данные и под любыми именами, кроме:
- не используйте пробел в названиях переменных
- не используйте символ перехода на новую строку в самих данных
Иначе вам придется переделать процедуру чтения/сохранения
из/в файл. Другие события
Для процедуры хранения данных нам было необходимо ловить момент
запуска бота, restart или rehash. Чтобы установить обработчик на само
событие - момент, когда команда поступила, но бот еще не выполнил,
читайте документацию на событие EVNT (чтобы повесить свою функцию на это
событие, используют обычный bind): см. tcl-commands.rus.txt,
раздел "Прочие функции".
Там написано, как принимать события от
команды kill -[тип] [pid], rehesh - до и
после выполнения, переключение логов, коннект к сервеку и т.п.
Замена и регулярные выражения
Иногда я забываю порядок перечисления аргументов в функциях
регулярных выражений, думаю кто-то тоже, поэтому привожу несколько
примеров. Для тех, кто писал на PHP/Perl и знаком с регулярными
выражения и их различий в PHP & Perl, то в TCL нет диапазона повтора
атомов. Т.е. можно использовать все традиционные функции
PHP-ориентированных регулярных выражений, если верить документации. Хотя
не проверял, сумеет ли TCL понять некоторые хитрые диапазоны символов,
как это может PHP.
По случаю сообщаю об ошибке в русскоязычной
документации TCL. Это не описано в документации, но TCL понимает
фигурные скобки, как наиболее гибкий множитель:
{ число1 [ , [ число2 ] ]
}
Примеры (выражения -
выделены): a{4} ==> равносильно aaaa (повторить а ровно 4 раза)
a{2,4} ==> равносильно aaa?a? (повторить a от 2х до 4х включительно раз)
a{0,1} ==> равносильно a?
a{1,} ==> равносильно a+ (повторить a от 1го до +бесконечности раз)
a{0,} ==> равносильно a*
В примерах, а - это любой атом, множество, подстрока или
что-то еще
regsub -all -nocase {[^a-z0-9_()-]}
$filename "_" filename Заменить в функции $filename
все символы, кроме букв и цифр на символ "_". Ответ записать в
переменную $filename (писать без $). При замене не учитывать регистр
букв (-nocase) и заменить не только первый найденный кусок, попадающий
под шаблон, но все остальные (-all).
if {[regexp
-nocase {([0-9][0-9]?)} $string - int]}
{соответствует} {нет} Проверить на соответствие
шаблону и вырезать что-нибудь в добавочные перенные. $string - входящая
строка. В переменную "-" (минус после $string) поместят всю найденную
строку, но это не нужные данные. После этого минуса не указывая $ перечислить переменные, соответствующие
маркерам (круглым скобкам в шаблоне).
if {[string
match шаблон проверяемая_строка]} {соответствует}
{нет} Еще в TCL есть простейший вид regexp - это
проверка соответствия строки некому шиблону. В шаблоне можно
использовать только *, ? и указывать диапазон букв с помощью [..]. Затем слеш позволяет превратить особые
символы в обычные (и самого себя, если указать двойной слеш). Никаких
более функций не поддерживается. Будьте осторожны, когда будете
проверять соответствие какого-то хоста под шаблон бана, т.к. символы
[ и \ в IRC
понимаются буквально, а в string match - как начало диапазона и
экранирование особого символа.
Переменные/функцииВ TCL бывают обычные переменные, списки и
массивы. Работа со списками вынесена в отдельную главу, а тут описаны
действия над переменными и массивами. Информация данной главы следует из
мануала TCL, но найти ее крайне трудно при переходе на TCL с какого-либо
традиционного веб-языка, типа PHP =)
Объявление переменных
перед первым использованием:
set a hello
# тоже самое:
set a "hello"
Использование особых символов $, [, ], (, ), {, }, ", \ set a "hello \[ скобка \" кавычка \{ еще скобка \$ доллар \\ слеш"
# тоже самое:
set a {hello [ скобка " кавычка \{ еще скобка $ доллар \ слеш}
Установить список с перечнем элементов: set mylist {
"hello"
"world"
}
Установить элемент массива (это приводит к авто-созданию самого
массива) set myarray(a) "hello"
set myarray(b) "world"
Сослаться на переменную, имя которой в другой переменной: set $name hello
Если до выполнения этой команды в $name поместить "qqq", по после
выполнения появится переменная $qqq, содержащая "hello". Чтобы проще
понять, в боте выполните пример: .tcl set a b; set $a 1; putlog "\$a=$a, \$b=$b"
вернет:
$a=b, $b=1
Проверить существование переменой
С обычными переменными и списками: if {[info exists
название_переменной]} {существует} {не существует}
(НЕЛЬЗЯ использовать $ в названии переменной )
С массивами:
if {[array exists название_массива]} {существует} {не
существует} (НЕЛЬЗЯ использовать $ в названии переменной
)
C элементами массива (проверьте сперва, что массив есть, иначе
будет ошибка): if {[info exists название_массива(элемент)]}
{существует} {не существует} Ипользовать $ тут по
прежнему нельзя, но если элемент у вас храниться в переменно, то и в
проверки его надо писать с $. Пример (есть ли элемент
$myvar(test)): info exists myvar(test)
Пример (есть ли элемент $myvar($i), $i - переменная):
info exists myvar($i) Удалить
переменную
unset назвение_переменной или
unset название_массива или
unset название_массива(элемент)
(использовать $ перед названием перенной нельзя). Предварительно
проверяйте, существует ли переменная/массив/элемент массива, иначе будет
ошибка. Существование фунции
info commands маска вернет список
определенных в самом TCL, боте или скриптах функций. Если использовать
маску *, то вам вернут все функции. Удобно проверять, когда вы
используете специфические функции, а суещствуют ли они вообще. Пример:
# check function md5
if {[info commands md5] != "md5" } {
die "Function md5() not found. Can't load <***.tcl>"
}
|
Список переменных
Кроме проверки существования конкретных переменных можно с помощью
info vars маска получить список всех
локальных переменных + объявленных глоабальных. "Локальность"
заключается в том, что команда info скорее всего будет выполнена не в
глобальной области, а внутри какой-то функции. В качестве маски можно
использовать "*", чтобы получить все переменные.
Чтобы
получить глобальные переменные - info globals
маска
Работа со списками.Здесь краткая справка по работе с массивами.
Что такое список?
Это массив. Только в TCL их называют списками. Все функции для
списков начинаются на букву 'L'. Кроме списков в TCL существуют и
настоящие массивы, но для использования массивов начинающим
программистам нужно начать работу со списков.
Как задать пустой список?
set mylist "" Хоть и задали пустую
строку, однако это правильно.
Как задать список из 2-х слов?
set mylist "слово1 слово2" См.
другие примеры в мане, функция list.
Как использовать разделитель, отличный от пробела?
См. в мане split (команда разделяет строку на части и создает
из них правильный Tcl-список): split mylist
разделитель. Пример: split "bot.net.ru"
. вернет список из 3х слов (обратите внимание на
крайнюю точку справа в строке с примером).
Еще есть join - склеить список в строку, разделяя элементы нужным
разделитем (по умолчанию - проблел): join mylist
разделитель
Как добавить элемент к списку?
См. в мане lappend - добавить в конец. Пример:
lappend mylist новый_элемент (функция
действует над $mylist, а не возвращает результат). См. linsert - для
вставки в любое место списка любого числа новых элементов:
linsert список позиция элемент1 элемент2
...
Как узнать кол-во элементов в списке?
Как удалить все эленты с первого по N?
Такая операция часто нужна, например в команде !topic
#канал фраза нужно вырезать навание канала ([lindex $arg
0]) и затем сделать переменную topic без первого слова. Для этого не
удалим, а возьмем все с N-ного элемента до конца. Для этого есть
lrange mylist начало конец (конец - не
включительно). Таким образом: set mylist [lrange $mylist N
end] где end - спец. слово, указывает на последний
элемент. Чтобы удалить все, кроме первого элемента, в качестве N
подставьте 1. Еще для удаления или замены можо использовать
lreplace, см. ман.
Но это еще не все. Попробуйте в
качестве $mylist поставить строку, содержащую символы [ или \ и вы
увидите глюки. Чтобы правильно воспользоваться командой, выполните ее
так: set mylist [join [lrange [split $mylist] N
end]] Как обратится к какому либо элементу
списка?
lindex mylist позиция (с нуля) - вернет
нужный элемент Как вывести на экран (в чат бота) список?
|