Программирование - 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 позиция (с нуля) - вернет
нужный элемент
Как вывести на экран (в чат бота) список?
|