Роутер ОС Микротик, как известно, имеет мощнейший LUA-подобный встроенный скриптовый язык, позволяющий осуществлять исполнение сценариев, в том числе при наступлении каких-либо событий в сети или по расписанию. Скрипты могут состоять из одной строки кода или иметь внушительные размеры, при передаче управления друг другу формируя сложные программы. Встроенный скриптовый язык существенно расширяет возможности системы, практически не ограничивая полет фантазии программиста. Существующее официальное руководство по скриптам написано кратко и, разумеется, не может охватить все особенности программирования для Роутер ОС.
В этой статье, не претендующей на полное руководство к разделу, мы рассмотрим одну из интересных и важных рубрик «скриптинга», а именно — функции.
Перед прочтением статьи, пользователям, начинающим изучать скрипты, рекомендую ознакомиться с официальным руководством Микротик по скриптам по ссылке выше, либо с его переводом (например, здесь). Следует знать типы переменных в скриптах Микротик, иметь понятие об областях видимости, окружении переменных и т.д… Также будет весьма полезна статья habr.com/ru/post/270719, в которой автор подробно разбирает типы переменных LUA Микротик и варианты их объявления и использования.
Вспомним также, что в языках программирования все команды можно разделить на две основные группы – процедуры и функции. В отличие от процедур функции не только позволяют исполнить встроенный код, но также могут использовать переданные им параметры (аргументы) и вернуть результат (в виде переменных или массивов). Функции обычно используются там, где есть необходимость повторно вызывать одни и те же фрагменты кода, чтобы не повторять их текст в листинге скрипта.
До версии Роутер ОС 6.2 функции в Микротик не были доступны напрямую, однако можно было использовать возможности команды :parse, как обходной способ для создания функций.
Например:
Или можно определить функцию из текста существующего скрипта:
Здесь я также приведу пример небольшого, изящно написанного, скрипта, почерпнутого мной где-то на просторах Интернет и позволяющего создать функции из всех скриптов репозитория, имена которых начинаются в данном примере на «Function.»:
В этом скрипте, сначала производится поиск всех скриптов репозитория роутера, имена которых соответствуют требованиям поиска и заполнение ими переменной $fnArray. Затем переменная fnArray преобразуется в массив данных, а в последней строке пробегая по элементам массива циклом :foreach скрипты помещаются в переменные окружения и становятся функциями.
Позднее, начиная с версии 6.2, синтаксис был доработан и появилась возможность передачи функциям параметров, а также реализован возврат из функций. Соответствующие примеры приведены в официальном руководстве. Я же приведу здесь в качестве практического примера простую функцию FuncPing, позволяющую определить доступность устройства с указанным адресом в сети:
В данном случае функция декларируется как глобальная переменная в окружении FuncPing, которой присваивается код функции, помещенный между do={}.
Функции, определённые как глобальные переменные, становятся доступны для всех скриптов репозитория, терминала и Планировщика роутера. Определенные как локальные, функции ограничены областью видимости скрипта.
В функциях Микротик можно передавать параметры двух типов: позиционные и именованные, а также их сочетание.
В примере, приведенном выше, осуществляется передача именованных параметров-аргументов PingAdr, который должен содержать адрес пингуемого функцией устройства в сети, и Count, содержащего количество «пингов» для проверки. В случае передачи функции именованных параметров, их положение в строке вызова не имеет значения (можно менять местами).
Обратите внимание на строку кода функции:
Здесь осуществляется проверка параметра Count (число пингов). Если $Count=«nothing» (т.е. не был задан), устанавливается значение $PingCount=3, если же $Count передан в функцию, то $PingCount принимает значение $Count.
Когда нам нужно определить доступность устройства в сети, вызываем функцию FuncPing, определенную как глобальная переменная, не забыв предварительно продекларировать (объявить) её в коде своего скрипта, иначе он не сможет её выполнить. Чтобы функция была исполнена следует подставить символ «$» перед её именем, для наглядности вся конструкция может быть заключена в квадратные скобки (не обязательно), дополнительно обозначающие необходимость исполнить действие внутри:
Результат работы функция возвращает из своего кода оператором :return (в нашем примере :return $PingAnswer). Из функции возвращено может быть одно значение переменной любого типа или массив переменных.
Пока, всё изложенное выше, общеизвестно, и лишь уточняет некоторые детали. Пойдём дальше в нашем изучении функций. Следует понимать, что функция в скриптах Микротик «с точки зрения» скриптового языка по конструкции напоминает ни что иное как массив. Такая конструкция удобна как для передачи параметров, так и для возврата результатов.
Это положение может быть доказано, например, следующим. Я условно называю это выражением function knows its name (функция знает своё имя).
Заключается этот эффект в том, что вне зависимости передавали ли функции аргументы или нет (в том числе строковые, позиционные или смешанные), при вызове функции всегда имеется параметр $0, который содержит имя нашей функции (вероятно он служит как какой-то внутренний указатель в скриптах Микротик).
Таким образом, получается, что функция – это смешанный массив, содержащий как позиционные ($0), так и именованные аргументы (ключевые и/или нумерованные элементы массива – аргументы функции).
«Узнать своё имя» функция может следующим образом (берем $0 и отсекаем у него первый символ, это всегда «$»):
Этот трюк можно использовать для нескольких целей: для печати в лог роутера событий, осуществляемых в функции, в том числе в подпрограммах обработки ошибок, сокращая текст надписей.
Кроме, того данный эффект позволяет избежать столкновения с одной ошибкой, допущенной разработчиками Микротик и изученной мной на собственном опыте.
А именно, по неизвестным причинам, при вызове любой функции дважды подряд с заключением вызова в квадратные скобки, вызываемая функция в скриптах выполняется трижды. С чем связана такая ошибка мне не известно. При этом в службе техподдержки Микротик знают о существовании этой ошибки, но до сих пор не спешат, так же по неизвестным причинам, её исправлять.
Наглядно проиллюстрировать «ошибку третьего вызова» можно следующим образом:
Здесь функция вызвана подряд дважды, а будет исполнена трижды, ошибочно повторяя именно второй вызов. Начиная с третьего раза ошибка не повторяется (последующие вызовы осуществляются корректно).
Для избежания этой ошибки, при необходимости обращения к функции дважды подряд, следует либо вызывать функции без заключения конструкции в квадратные скобки:
либо как-то блокировать лишний вызов … но как это сделать?
К счастью, как оказалось, ошибочный третий вызов функции производится без передачи функции параметра $0 (имени функции). Да, да по какой-то причине при этом ошибочном вызове $0 пуст. Это позволяет реализовать программную страховку от ошибочного выполнения внутри самой функции. Сделаем это так, допустим есть функция:
Если мы попросим выполнить её дважды:
Как мы уже выяснили она будет выполнена трижды.
Добавление в функцию проверки на наличие имени в $0 устраняет проблему:
Если имеется проверка на $0, то при ошибочном третьем запуске тело кода функции будет просто проигнорировано (не будет исполнено). Эта проверка осуществляется строкой проверки на условие :if ([$len $0]!=0) do={ }.
К сожалению, пока эта ошибка не исправлена разработчиками Микротик. Для корректной работы такую проверку следует вставлять в каждую пользовательскую функцию, либо не использовать в конструкциях квадратные скобки. Это одинаково справедливо для функций, объявленных как глобальные переменные, так и для «локальных» функций.
Отдельно стоит остановиться на обработке ошибок в функциях. Не секрет, что при выполнении функций, как и обычных скриптов, могут возникать ошибки, приводящие к остановке работы. Это весьма не желательно, так как не позволяет полностью «автоматизировать» процесс. Разработчики Рос внесли возможность организации обработки ошибок, позволяющий избежать остановки выполнения сценариев при возникновении подобных ситуаций.
Обработка ошибок организуется командами do { контролируемое действие } on-error={ действие при ошибке }. В обработчик ошибок имеет смысл «заворачивать» команды, при исполнении которых есть риск ошибочного завершения, например действие команд :fetch, :resolve и т.п… Всё это справедливо и для функций. Внутри функций, я также делаю проверки на корректность переданных аргументов.
В качестве примера демонстрации обработки ошибок, приведу «старую» свою функцию FuncMail, написанную ещё в 2017 году, и служащую для отправки сообщений на почту. Функция имеет два именованных параметра: Mailtext (должен содержать текст пересылаемого сообщения) и Email (должен содержать адрес получателя):
Заметьте, обработка ошибок сделана для команды отправки почты /tool e-mail send, но не сделана для :resolve smtp сервера (для данного примера). Также сделана обработка ошибки при вызове функции без параметров:
Возврат из функции не всегда нужен, однако функции тем и хороши, что могут посредством переменных различных типов возвращать результаты своей работы. При этом можно возвращать из функций «логические» (булевые) переменные, такие как «thrue» и «false», числовые и строковые переменные, переменные других типов (при необходимости), а также массивы данных. Возврат из функции прекращает её работу и осуществляется командой :return.
Например:
В этом примере функция возвратит полученные конкатенацией системные дату и время в виде единой строки. А в примере ниже возврат осуществляется в виде массива элементов (первый элемент дата, второй – время)
По индексам массива можно получить каждый (нужный) элементы массива (помним, что индекс первого элемента массива равен 0, второго 1 и т.д…
Можно тоже самое сделать с созданием ключевого массива (каждый элемент имеет имя (ключ), но описание работы с массивами выходит за рамки данной статьи и достойно отдельного рассмотрения.
Когда надобность в функции отпадает её можно удалить. Локальные функции не требуют удаления, так как удаляются системой при выходе скрипта из области видимости (либо по завершению работы). Функции, объявленные как глобальные переменные могут быть удалены из окружения присвоением пустого значения командой :set.
или
Формально можно также удалить переменную из окружения системной командой удаления:
Обнулить (очиcтить значение), как уже ясно из вышеизложенного, можно декларированием с присвоением пустого действия так:
Также напомню важную рекомендацию из официальной документации Wiki по скриптам относительно функций, которую следует всегда иметь ввиду:
«Если функция содержит глобально определённую переменную с именем совпадающим с именем передаваемого именного параметра, то глобально определённая переменная будет проигнорирована для совместимости со старыми версиями. Эта возможность может быть изменена в будущих версиях. Избегайте использование параметров с теми же именами, что и глобальные переменные.»
Пример:
Вывод:
1234
lala
global value 123
Любая функция может в свою очередь вызывать другие функции. Чтобы это работало, вызываемые функции должны быть продекларированы в коде той функции, которая их вызывает (пример из Wiki):
Вывод:
9
В Роутер ОС Микротик поддерживается рекурсивный вызов функций. То есть функция может вызывать саму себя! Это бывает нужно в случаях, когда необходимо повторять ту или иную часть кода с различными значениями входных параметров. Рекурсивные функции фактически формируют циклы и наиболее эффективны при обработке сложных данных, которые слишком сложны, чтобы пройтись по ним с помощью обычных циклов языка, задаваемых командами :for, :foreach или :while. Для работы рекурсии в теле функции она должна декларировать сама себя.
Простой пример рекурсивного вызова функции:
Вывод:
5
4
3
2
1
end
Заметьте, что обращаемся мы к функции один раз (:log info [$CountDown 5]), а выполняет она сама себя пока не исчерпает данные по условию (6 раз).
При наличии сложных данных рекурсивные функции работают эффективнее и быстрее циклов, при простых – наоборот. С рекурсивными функциями следует быть осторожными, так как при неправильном рекурсивном вызове может получится формирование бесконечного цикла и, как следствие, переполнение стека данными с аварийным завершением работы (чаще всего происходит перезагрузка роутера, как при исполнении /system reboot).
Более сложный пример необходимости использовании рекурсивного вызова функций в скриптах Микротик можно видеть, например, здесь.
Автором был создан великолепный парсер данных из формата JSON в многомерный ассоциативный массив. В функции $fJParsePrint, из набора парсера, можно видеть в коде функции рекурсивный вызов, который передает внутрь функции текущий уровень вложения и элемент-подмассив, таким образом осуществляется обход всего дерева массивов в переменной $JParseOut (подробнее в статье).
В той же статье можно видеть недокументированную команду :any, ювелирно использованную автором парсера для исключение повторного развертывания функций при вызове, что ускоряет работу системы. При объявлении каждой из функций сначала производится проверка на её «не пустость» командой :any. Если переменная (она же функция) пуста – требуется её определение (наполнение кодом), если же не пуста, соответственно она уже содержит код и её переопределение не требуется. В качестве примера короткой, но мощной функции, для демонстрации работы :any, в указанной статье можно видеть функцию fByteToEscapeChar, «налету» преобразующей позиционный параметр $1 (байтовое число) в строковый символ, т.е. осуществляющей преобразование кода ASCII в символ.
От себя добавлю интересную находку. Когда я проверил какой тип у параметра функции $0, то получил вот что:
Вывод:
$myFunc
lookup
То есть параметр $0 (тот, что содержит имя функции как мы выяснили выше) имеет некий тип «lookup», о котором нет ни слова в официальном руководстве Рос Микротик! Запомним это и посмотрим как это можно использовать (будет обсуждено далее).
Одним из интересных разделов в теме функции Роутер Ос является создание функций в массиве. Да, да, такое возможно! Действительно, раз функции можно создавать внутри переменных, почему нельзя было бы определять функции, как элементы массива?
Не скрою, данная идея не моя и была случайно почерпнута в одном из топиков официального форума Микротик здесь.
При этом по данной теме очень мало информации и примеров.
Перескажу содержание топика автора со своими комментариями ниже:
Функции могут принимать параметры, которые могут быть позиционными или именованными (это мы уже знаем). Позиционные начинаются с $1 для первого параметра функции. Поскольку функция может быть элементом массива, она также может принимать параметры, как и глобальная (или локальная) функция. Все это работает нормально (при правильном использовании).
Мы уже знаем, что функция knows its name (знает своё имя). Наглядный пример ниже демонстрирует как имя функции и параметр $1 формируют предложение:
Вывод:
hello world
Функцию массив можно задать следующим образом. Сначала создаём пустой массив:
Затем, наполняем его функциями, индексированными, например, по ключевым элементам:
Вызовем первую функцию:
Вывод:
hello
Функция должна была вывести hello world, а вместо этого выводит hello. Почему так происходит?
Всё дело в том, что параметр world, передается функции не как первый позиционный параметр $1, а как нулевой — $0, а параметр $1 получается нами вовсе не определен.
Правильно будет написать так:
Тогда world становится $0 параметром и функция выводит:
hello world
Далее цитирую источник:
" … С несколькими параметрами то, что было бы $2 в функции с глобальной областью действия, равно $1, когда функция определена в массиве. По сути, является ли первый аргумент $0 или $1, зависит от того, где он находится. Даже примешивание именованного параметра все еще работает, позиционные аргументы просто сдвигаются на единицу, а аргументы ключ=значение работают везде, как обычно.
Возможно, что это ошибка, но более тонкая. Но я вижу логику: функция массива больше похожа на анонимную функцию — это термины CS, а глобальная функция — это команда (команды имеют имя, имя — это аргумент), поэтому первый аргумент, известный функции, всегда равен $0, что может быть его имя, если таковое имеется…
… Таким образом используйте $0, когда функция определена в массиве."
В заключение этого небольшого обзора по функциям скриптов Микротик в качестве бонуса, приведу свою универсальную функцию массив, FuncAS (Function Active Sender), осуществляющую отправку сообщений в мессенджер Телеграмм, в виде SMS через модем роутера, SMS через API сервиса sms.ru, SMS через шлюз устройств Laurent5G, Netping и на почту.
Функция FuncAS представляет собой смешанный массив, имеющий девять элементов: $0, translite, telegram, sms, smsAPI, smsLT, smsNP, email, all. Элемент all стоит последним и производит вызов всех элементов, отправляя сообщения сразу на все «пункты приема». Благодаря работе над этой функцией, выяснилась ещё одна интересная особенность функций, определенных как массив. Если вызвать не один из элементов такой функции, а как обычно функцию «целиком» (без ключей) будет произведено выполнение последнего элемента массива-функции. Этой особенностью я и воспользовался разместив на последнем месте элемент с ключом «all». Заметьте, что элемент функции «translate» не является компонентом отправки, как другие элементы, а используется частью из них для транслитерации русскоязычных сообщений как подпрограмма.
Функция полностью корректно работает при отправке сообщений на английском языке и/или русском языке с аргументом «translite» и, с учётом ограничения длины сообщения, поддерживает спец. символы \$@!#%'()*+,-./:;<=>?[]^_`{|}~ Для Телеграмм, API sms.ru и большинства почтовых клиентов (проверено для mail.ru, yandex.ru и gmail.com) есть поддержка национального (русского) текста. Для Телеграмм (опционально при указании $1=«translite») и отправке SMS через модемы, работает транслитерация русских символов в латинские. В работе функции использованы, написанные мной ранее конвертеры строки (приведены и описаны в статьях Хабра habr.com/ru/post/518534 и habr.com/ru/post/519406).
Функция поддерживает отправку сообщений через модем, подключенный к роутеру (usb или pci-e), а также через многофункциональные интернет-контроллеры компаний KernelChip (Laurent-5G) и Алентис-Электроникс (устройства Netping, имеющие модем).
Для работы функции на роутере должны быть сконфигурированы /tool sms и /tool e-mail. При использовании ключа «telegram» должны быть определены global Emoji (Emoji роутера, по желанию), заданы ID Вашего Телеграм-бота и чата (глобальные переменные botID и myChatID). При использовании сервиса sms.ru (ключ «smsAPI») должна быть определена global apikey (ключ аккаунта пользователя на sms.ru). При пересылке сообщения через модем модуля Laurent-5G (ключ smsLT) должны быть определены глобальные переменный Ladr (адрес Laurent-5G в локальной сети) и Lpassword (пароль для авторизации при http get-запросах к модулю). При использовании ключа «smsNP» должны быть определены global NPadr (адрес шлюза Netping в сети), NPuser (login), NPpass (пароль) авторизации.
Выбор номера телефона получателя при всех вариантах отправок sms осуществляется в порядке $1 (из параметра функции) -> $ADMINPHONE (global var) -> first [/tool sms get allowed-number] по алгоритму «если не указан, то следующий». Для smsLT вместо $ADMINPHONE номер телефона берется из первой ячейки базы данных телефонных номеров модуля. Номера телефонов следует указывать в международном формате (+7десятизначный номер), поддерживается отправка sms только по России (+7). Выбор почтового адреса получателя при отправке e-mail осуществляется аналогично в порядке $1 -> $ADMINMAIL (global var).
Ещё раз обратим внимание, что в отличие от обычных функций, которые в $0 хранят своё имя, первый параметр функции массива будет $0, второй $1 и т.д...., что учитывается в коде элементов функции. Вызов функции без ключа, в виде [$FuncAS «you mesage»] вызывает последовательную работу всех элементов функции и равноценен записи [($FuncAS->«all») «you mesage»].
Основные аспекты работы функции отмечены в комментариях к коду. Функция объединяет шесть вариантов отправки сообщений посредством различных служб и устройств и способна работать как частично (отправляя сообщение по какому-либо одному каналу) так и полностью по всем последовательно. Служит наглядным примером того, как можно создавать функции в массивах. Вероятно, можно такое проделать и в многомерном массиве, однако ввиду усложнений синтаксиса вряд ли это необходимо.
Из вышесказанного следует также, что в функции-массиве можно разместить как «подфункции» так и данные.
Обратите внимание на последнюю часть функции под ключом ($FuncAS->«all»). Помня про lookup мы проверяем откуда вызвана данная часть функции: как элемент массива по ключу «all» или как обычная целая функция $FuncAS в этой строке кода:
Если функция вызвана без ключа, то осуществляется «передвижка» аргументов: $1 «передвигается» в $0, а $2, когда это нужно для работы блока транслитерации, становится $1.
Разумеется можно «разобрать» функцию массив на более привычные нам, отдельные функции, и использовать каждую со своим именем, однако объединение в функцию-массив тематически однообразных подфункций представляется удобным и логичным решением.
С 2017 года мной были созданы и собраны около 200 функций самого разного назначения, многие из которых ежедневно помогают мне в работе над скриптами Роутер ОС. Конечной целью было бы создание сайта-библиотеки скриптов и функций, которым могли бы пользоваться все желающие. В настоящее время можно обсуждать всё, что связано со скриптовыми функциями на русскоязычном форуме Микротик по ссылке.
Надеюсь, моя статья будет полезной пользователям изучающим скрипты Роутер ОС Микротик. Всем желаю удачи в изучении скриптов и успехов в программировании.
© Серков Сергей Владимирович 2022 (Sertik)
В этой статье, не претендующей на полное руководство к разделу, мы рассмотрим одну из интересных и важных рубрик «скриптинга», а именно — функции.
Перед прочтением статьи, пользователям, начинающим изучать скрипты, рекомендую ознакомиться с официальным руководством Микротик по скриптам по ссылке выше, либо с его переводом (например, здесь). Следует знать типы переменных в скриптах Микротик, иметь понятие об областях видимости, окружении переменных и т.д… Также будет весьма полезна статья habr.com/ru/post/270719, в которой автор подробно разбирает типы переменных LUA Микротик и варианты их объявления и использования.
Вспомним также, что в языках программирования все команды можно разделить на две основные группы – процедуры и функции. В отличие от процедур функции не только позволяют исполнить встроенный код, но также могут использовать переданные им параметры (аргументы) и вернуть результат (в виде переменных или массивов). Функции обычно используются там, где есть необходимость повторно вызывать одни и те же фрагменты кода, чтобы не повторять их текст в листинге скрипта.
До версии Роутер ОС 6.2 функции в Микротик не были доступны напрямую, однако можно было использовать возможности команды :parse, как обходной способ для создания функций.
Например:
:global myFunc [:parse ":log info Goodbay!"];
$myFunc;
Или можно определить функцию из текста существующего скрипта:
# добавляем скрипт в репозиторий (если его там ещё нет)
/system script add name=myScript source=":put \"Hello world !\""
# парсим скрипт и присваиваем его текст переменной, исполняем код:
:local myFunc [:parse [/system script get myScript source]]
$myFunc
Здесь я также приведу пример небольшого, изящно написанного, скрипта, почерпнутого мной где-то на просторах Интернет и позволяющего создать функции из всех скриптов репозитория, имена которых начинаются в данном примере на «Function.»:
:local fnArray;
:foreach f in=[/system script find where name~"^Function.*"] do={:set fnArray ($fnArray.",".[/system script get $f name])};
:set fnArray [:toarray $fnArray];
:foreach f in=$fnArray do={:exec script=":global \"$f\" [:parse [/system script get $f source]]"; /log info ("Defined function ".$f);};
В этом скрипте, сначала производится поиск всех скриптов репозитория роутера, имена которых соответствуют требованиям поиска и заполнение ими переменной $fnArray. Затем переменная fnArray преобразуется в массив данных, а в последней строке пробегая по элементам массива циклом :foreach скрипты помещаются в переменные окружения и становятся функциями.
Позднее, начиная с версии 6.2, синтаксис был доработан и появилась возможность передачи функциям параметров, а также реализован возврат из функций. Соответствующие примеры приведены в официальном руководстве. Я же приведу здесь в качестве практического примера простую функцию FuncPing, позволяющую определить доступность устройства с указанным адресом в сети:
#------ FuncPing-------#
# Функция проверки доступности устройства в сети методом пинга
# в именованном параметре PingAdr следует передать пингуемый IP-адрес
# при этом IP-адрес может быть "чистым"
# или с указанием порта через двоеточие (при пинге функция отсекает порт)
#
# ответ функции возвращается в виде:
# "done" - устройство доступно в сети
# "ERROR: device not responded" - устройство не доступно
:global FuncPing do={
:local Hadr;
:if ([:find $PingAdr ":"]>0) do={:set Hadr [:pick $PingAdr 0 [:find $PingAdr ":"]];} else={
:set Hadr $PingAdr}
:local PingCount
:if ([:typeof $Count]="nothing") do={:set PingCount 3} else={:set PingCount $Count}
:local Result [/ping $Hadr count=$PingCount];
:local PingAnswer;
:local MainIfInetOk false;
:set MainIfInetOk ((3*$Result) >= (2 * $PingCount))
:put "MainIfInetOk=$MainIfInetOk"
if (!$MainIfInetOk) do={:set PingAnswer "ERROR: device not responded"}
if ($MainIfInetOk) do={:set PingAnswer "done"}
:return $PingAnswer;
}
:log info [$FuncPing PingAdr="192.168.0.7" Count=5]
В данном случае функция декларируется как глобальная переменная в окружении FuncPing, которой присваивается код функции, помещенный между do={}.
Функции, определённые как глобальные переменные, становятся доступны для всех скриптов репозитория, терминала и Планировщика роутера. Определенные как локальные, функции ограничены областью видимости скрипта.
В функциях Микротик можно передавать параметры двух типов: позиционные и именованные, а также их сочетание.
В примере, приведенном выше, осуществляется передача именованных параметров-аргументов PingAdr, который должен содержать адрес пингуемого функцией устройства в сети, и Count, содержащего количество «пингов» для проверки. В случае передачи функции именованных параметров, их положение в строке вызова не имеет значения (можно менять местами).
Обратите внимание на строку кода функции:
:if ([:typeof $Count]="nothing") do={:set PingCount 3} else={:set PingCount $Count}
Здесь осуществляется проверка параметра Count (число пингов). Если $Count=«nothing» (т.е. не был задан), устанавливается значение $PingCount=3, если же $Count передан в функцию, то $PingCount принимает значение $Count.
Когда нам нужно определить доступность устройства в сети, вызываем функцию FuncPing, определенную как глобальная переменная, не забыв предварительно продекларировать (объявить) её в коде своего скрипта, иначе он не сможет её выполнить. Чтобы функция была исполнена следует подставить символ «$» перед её именем, для наглядности вся конструкция может быть заключена в квадратные скобки (не обязательно), дополнительно обозначающие необходимость исполнить действие внутри:
:global FuncPing
:local Result [$FuncPing PingAdr=«192.168.0.1» PingCount=5]
:put $Result
Результат работы функция возвращает из своего кода оператором :return (в нашем примере :return $PingAnswer). Из функции возвращено может быть одно значение переменной любого типа или массив переменных.
Пока, всё изложенное выше, общеизвестно, и лишь уточняет некоторые детали. Пойдём дальше в нашем изучении функций. Следует понимать, что функция в скриптах Микротик «с точки зрения» скриптового языка по конструкции напоминает ни что иное как массив. Такая конструкция удобна как для передачи параметров, так и для возврата результатов.
Это положение может быть доказано, например, следующим. Я условно называю это выражением function knows its name (функция знает своё имя).
Заключается этот эффект в том, что вне зависимости передавали ли функции аргументы или нет (в том числе строковые, позиционные или смешанные), при вызове функции всегда имеется параметр $0, который содержит имя нашей функции (вероятно он служит как какой-то внутренний указатель в скриптах Микротик).
Таким образом, получается, что функция – это смешанный массив, содержащий как позиционные ($0), так и именованные аргументы (ключевые и/или нумерованные элементы массива – аргументы функции).
«Узнать своё имя» функция может следующим образом (берем $0 и отсекаем у него первый символ, это всегда «$»):
:global FuncName do={:return [:pick $0 1 [:len $0]]}
:log info [$FuncName]
Этот трюк можно использовать для нескольких целей: для печати в лог роутера событий, осуществляемых в функции, в том числе в подпрограммах обработки ошибок, сокращая текст надписей.
Кроме, того данный эффект позволяет избежать столкновения с одной ошибкой, допущенной разработчиками Микротик и изученной мной на собственном опыте.
А именно, по неизвестным причинам, при вызове любой функции дважды подряд с заключением вызова в квадратные скобки, вызываемая функция в скриптах выполняется трижды. С чем связана такая ошибка мне не известно. При этом в службе техподдержки Микротик знают о существовании этой ошибки, но до сих пор не спешат, так же по неизвестным причинам, её исправлять.
Наглядно проиллюстрировать «ошибку третьего вызова» можно следующим образом:
[$FuncPing PingAdr=«192.168.0.1» PingCount=5]
[$FuncPing PingAdr=«192.168.0.7» PingCount=2]
Здесь функция вызвана подряд дважды, а будет исполнена трижды, ошибочно повторяя именно второй вызов. Начиная с третьего раза ошибка не повторяется (последующие вызовы осуществляются корректно).
Для избежания этой ошибки, при необходимости обращения к функции дважды подряд, следует либо вызывать функции без заключения конструкции в квадратные скобки:
$FuncPing PingAdr=«192.168.0.1» PingCount=5
$FuncPing PingAdr=«192.168.0.7» PingCount=2
либо как-то блокировать лишний вызов … но как это сделать?
К счастью, как оказалось, ошибочный третий вызов функции производится без передачи функции параметра $0 (имени функции). Да, да по какой-то причине при этом ошибочном вызове $0 пуст. Это позволяет реализовать программную страховку от ошибочного выполнения внутри самой функции. Сделаем это так, допустим есть функция:
:global myFunc do={:log info "Hello world !"}
Если мы попросим выполнить её дважды:
[$myFunc]; [$myFunc]
Как мы уже выяснили она будет выполнена трижды.
Добавление в функцию проверки на наличие имени в $0 устраняет проблему:
:global myFunc do={
:if ([$len $0]!=0) do={
:log info "Hello world !"}
}
Если имеется проверка на $0, то при ошибочном третьем запуске тело кода функции будет просто проигнорировано (не будет исполнено). Эта проверка осуществляется строкой проверки на условие :if ([$len $0]!=0) do={ }.
К сожалению, пока эта ошибка не исправлена разработчиками Микротик. Для корректной работы такую проверку следует вставлять в каждую пользовательскую функцию, либо не использовать в конструкциях квадратные скобки. Это одинаково справедливо для функций, объявленных как глобальные переменные, так и для «локальных» функций.
Отдельно стоит остановиться на обработке ошибок в функциях. Не секрет, что при выполнении функций, как и обычных скриптов, могут возникать ошибки, приводящие к остановке работы. Это весьма не желательно, так как не позволяет полностью «автоматизировать» процесс. Разработчики Рос внесли возможность организации обработки ошибок, позволяющий избежать остановки выполнения сценариев при возникновении подобных ситуаций.
Обработка ошибок организуется командами do { контролируемое действие } on-error={ действие при ошибке }. В обработчик ошибок имеет смысл «заворачивать» команды, при исполнении которых есть риск ошибочного завершения, например действие команд :fetch, :resolve и т.п… Всё это справедливо и для функций. Внутри функций, я также делаю проверки на корректность переданных аргументов.
В качестве примера демонстрации обработки ошибок, приведу «старую» свою функцию FuncMail, написанную ещё в 2017 году, и служащую для отправки сообщений на почту. Функция имеет два именованных параметра: Mailtext (должен содержать текст пересылаемого сообщения) и Email (должен содержать адрес получателя):
#------------- FuncMail-----------------#
# Функция отправки почты
# by Sergej Serkov 24.12.17
#---------------------------------------#
# Применение:
# [$FuncMail Email="user@mail.ru" Mailtext="test letter"]
# В качестве параметра функции в Mailtext нужно передать текст сообщения на почту
# отправка почты идет на ящик, указанный в параметре Email
# для корректной работы функции почтовый сервис Router OS /tool email должен быть настроен верно по умолчанию
:global FuncMail do={
:local smtpserv [:resolve [/tool e-mail get address]];
:local Eaccount [/tool e-mail get user];
:local pass [/tool e-mail get password];
:local Eport [/tool e-mail get port];
:local Etls "yes"; # если не используется установить в "no"
:if (([:len $Email]!=0) and ([:len $Mailtext]!=0)) do={
:log info " "; :log warning "FuncMail start mail sending ... to e-mail: $Email";
do {[/tool e-mail send from="<$Eaccount>" to=$Email server=$smtpserv \
port=$Eport user=$Eaccount password=$pass start-tls=$Etls subject=("from FuncMail Router $[/system identity get name]") \
body=$Mailtext;];
} on-error={:log info ""; :log error ("Call ERROR function $0 ERROR e-mail send");
:return "ERROR: <$0 e-mail send>"}
:log warning "Mail send"; :log info " "; :return "OK: <mail send>"
} else={:log error ("Call ERROR function $0 Email or Mailtext parametrs no defined"); :return ("ERROR: $0 < necessary parameters are not set >")}
}
:log info [$FuncMail Mailtext="Привет !" Email="user@mail.ru"]
Заметьте, обработка ошибок сделана для команды отправки почты /tool e-mail send, но не сделана для :resolve smtp сервера (для данного примера). Также сделана обработка ошибки при вызове функции без параметров:
:if (([:len $Email]!=0) and ([:len $Mailtext!]=0)) do={} else={:log error ("Call ERROR function $0 Email or Mailtext parametrs no defined"); :return ("ERROR: $0 < necessary parameters are not set >")}
Возврат из функции не всегда нужен, однако функции тем и хороши, что могут посредством переменных различных типов возвращать результаты своей работы. При этом можно возвращать из функций «логические» (булевые) переменные, такие как «thrue» и «false», числовые и строковые переменные, переменные других типов (при необходимости), а также массивы данных. Возврат из функции прекращает её работу и осуществляется командой :return.
Например:
:global cuDte do={:return ([/system clock get date]." ".[/system clock get time]);}
:log info [$cuDte]
В этом примере функция возвратит полученные конкатенацией системные дату и время в виде единой строки. А в примере ниже возврат осуществляется в виде массива элементов (первый элемент дата, второй – время)
:global cuDte do={:return [:toarray {[/system clock get date]; [/system clock get time]}] }
:local DateTime [$cuDte]
:log info $DateTime
:log info [:typeof $DateTime]
По индексам массива можно получить каждый (нужный) элементы массива (помним, что индекс первого элемента массива равен 0, второго 1 и т.д…
:log info ($DateTime->0)
:log info ($DateTime->1)
Можно тоже самое сделать с созданием ключевого массива (каждый элемент имеет имя (ключ), но описание работы с массивами выходит за рамки данной статьи и достойно отдельного рассмотрения.
Когда надобность в функции отпадает её можно удалить. Локальные функции не требуют удаления, так как удаляются системой при выходе скрипта из области видимости (либо по завершению работы). Функции, объявленные как глобальные переменные могут быть удалены из окружения присвоением пустого значения командой :set.
:set cuDte
или
:set сuDte (:nothing);
Формально можно также удалить переменную из окружения системной командой удаления:
/system script environment remove [find name="cuDte"]
Обнулить (очиcтить значение), как уже ясно из вышеизложенного, можно декларированием с присвоением пустого действия так:
:global cuDte []
Также напомню важную рекомендацию из официальной документации Wiki по скриптам относительно функций, которую следует всегда иметь ввиду:
«Если функция содержит глобально определённую переменную с именем совпадающим с именем передаваемого именного параметра, то глобально определённая переменная будет проигнорирована для совместимости со старыми версиями. Эта возможность может быть изменена в будущих версиях. Избегайте использование параметров с теми же именами, что и глобальные переменные.»
Пример:
:global my2 "123"
:global myFunc do={ :global my2; :put $my2; :set my2 "lala"; :put $my2 }
$myFunc my2=1234
:put «global value $my2»
Вывод:
1234
lala
global value 123
Любая функция может в свою очередь вызывать другие функции. Чтобы это работало, вызываемые функции должны быть продекларированы в коде той функции, которая их вызывает (пример из Wiki):
:global funcA do={ :return 5 }
:global funcB do={
:global funcA;
:return ([$funcA] + 4)
}
:put [$funcB]
Вывод:
9
В Роутер ОС Микротик поддерживается рекурсивный вызов функций. То есть функция может вызывать саму себя! Это бывает нужно в случаях, когда необходимо повторять ту или иную часть кода с различными значениями входных параметров. Рекурсивные функции фактически формируют циклы и наиболее эффективны при обработке сложных данных, которые слишком сложны, чтобы пройтись по ним с помощью обычных циклов языка, задаваемых командами :for, :foreach или :while. Для работы рекурсии в теле функции она должна декларировать сама себя.
Простой пример рекурсивного вызова функции:
:global CountDown do={
:global CountDown;
:if ($1>0) do={:log warning $1; :set $1 ($1-1);[$CountDown $1];}
:return "end"
}
:log info [$CountDown 5]
Вывод:
5
4
3
2
1
end
Заметьте, что обращаемся мы к функции один раз (:log info [$CountDown 5]), а выполняет она сама себя пока не исчерпает данные по условию (6 раз).
При наличии сложных данных рекурсивные функции работают эффективнее и быстрее циклов, при простых – наоборот. С рекурсивными функциями следует быть осторожными, так как при неправильном рекурсивном вызове может получится формирование бесконечного цикла и, как следствие, переполнение стека данными с аварийным завершением работы (чаще всего происходит перезагрузка роутера, как при исполнении /system reboot).
Более сложный пример необходимости использовании рекурсивного вызова функций в скриптах Микротик можно видеть, например, здесь.
Автором был создан великолепный парсер данных из формата JSON в многомерный ассоциативный массив. В функции $fJParsePrint, из набора парсера, можно видеть в коде функции рекурсивный вызов, который передает внутрь функции текущий уровень вложения и элемент-подмассив, таким образом осуществляется обход всего дерева массивов в переменной $JParseOut (подробнее в статье).
В той же статье можно видеть недокументированную команду :any, ювелирно использованную автором парсера для исключение повторного развертывания функций при вызове, что ускоряет работу системы. При объявлении каждой из функций сначала производится проверка на её «не пустость» командой :any. Если переменная (она же функция) пуста – требуется её определение (наполнение кодом), если же не пуста, соответственно она уже содержит код и её переопределение не требуется. В качестве примера короткой, но мощной функции, для демонстрации работы :any, в указанной статье можно видеть функцию fByteToEscapeChar, «налету» преобразующей позиционный параметр $1 (байтовое число) в строковый символ, т.е. осуществляющей преобразование кода ASCII в символ.
:global fByteToEscapeChar
:if (!any $fByteToEscapeChar) do={ :global fByteToEscapeChar do={
:return [[:parse "(\"\\$[:pick "0123456789ABCDEF" (($1 >> 4) & 0xF)]$[:pick "0123456789ABCDEF" ($1 & 0xF)]\")"]]
}}
От себя добавлю интересную находку. Когда я проверил какой тип у параметра функции $0, то получил вот что:
:global myFunc do={:log info $0; log info [:typeof $0]}
$myFunc
Вывод:
$myFunc
lookup
То есть параметр $0 (тот, что содержит имя функции как мы выяснили выше) имеет некий тип «lookup», о котором нет ни слова в официальном руководстве Рос Микротик! Запомним это и посмотрим как это можно использовать (будет обсуждено далее).
Одним из интересных разделов в теме функции Роутер Ос является создание функций в массиве. Да, да, такое возможно! Действительно, раз функции можно создавать внутри переменных, почему нельзя было бы определять функции, как элементы массива?
Не скрою, данная идея не моя и была случайно почерпнута в одном из топиков официального форума Микротик здесь.
При этом по данной теме очень мало информации и примеров.
Перескажу содержание топика автора со своими комментариями ниже:
Функции могут принимать параметры, которые могут быть позиционными или именованными (это мы уже знаем). Позиционные начинаются с $1 для первого параметра функции. Поскольку функция может быть элементом массива, она также может принимать параметры, как и глобальная (или локальная) функция. Все это работает нормально (при правильном использовании).
Мы уже знаем, что функция knows its name (знает своё имя). Наглядный пример ниже демонстрирует как имя функции и параметр $1 формируют предложение:
:global hello
:set hello do={:log info ("$[pick $0 1 [:len $0]]". " $1")}
$hello world
Вывод:
hello world
Функцию массив можно задать следующим образом. Сначала создаём пустой массив:
:global fnArray [:toarray ""]
Затем, наполняем его функциями, индексированными, например, по ключевым элементам:
:global fnArray [toarray ""]
:set ($fnArray->"hello") do={:log info "hello $1 "}
:set ($fnArray->"good by") do={:log info "good by $1 "}
Вызовем первую функцию:
[($fnArray->"hello") world]
Вывод:
hello
Функция должна была вывести hello world, а вместо этого выводит hello. Почему так происходит?
Всё дело в том, что параметр world, передается функции не как первый позиционный параметр $1, а как нулевой — $0, а параметр $1 получается нами вовсе не определен.
Правильно будет написать так:
:set ($fnArray->"hello") do={:log info "hello $0 "}
[($fnArray->"hello") world]
Тогда world становится $0 параметром и функция выводит:
hello world
Далее цитирую источник:
" … С несколькими параметрами то, что было бы $2 в функции с глобальной областью действия, равно $1, когда функция определена в массиве. По сути, является ли первый аргумент $0 или $1, зависит от того, где он находится. Даже примешивание именованного параметра все еще работает, позиционные аргументы просто сдвигаются на единицу, а аргументы ключ=значение работают везде, как обычно.
Возможно, что это ошибка, но более тонкая. Но я вижу логику: функция массива больше похожа на анонимную функцию — это термины CS, а глобальная функция — это команда (команды имеют имя, имя — это аргумент), поэтому первый аргумент, известный функции, всегда равен $0, что может быть его имя, если таковое имеется…
… Таким образом используйте $0, когда функция определена в массиве."
В заключение этого небольшого обзора по функциям скриптов Микротик в качестве бонуса, приведу свою универсальную функцию массив, FuncAS (Function Active Sender), осуществляющую отправку сообщений в мессенджер Телеграмм, в виде SMS через модем роутера, SMS через API сервиса sms.ru, SMS через шлюз устройств Laurent5G, Netping и на почту.
Функция FuncAS представляет собой смешанный массив, имеющий девять элементов: $0, translite, telegram, sms, smsAPI, smsLT, smsNP, email, all. Элемент all стоит последним и производит вызов всех элементов, отправляя сообщения сразу на все «пункты приема». Благодаря работе над этой функцией, выяснилась ещё одна интересная особенность функций, определенных как массив. Если вызвать не один из элементов такой функции, а как обычно функцию «целиком» (без ключей) будет произведено выполнение последнего элемента массива-функции. Этой особенностью я и воспользовался разместив на последнем месте элемент с ключом «all». Заметьте, что элемент функции «translate» не является компонентом отправки, как другие элементы, а используется частью из них для транслитерации русскоязычных сообщений как подпрограмма.
Функция полностью корректно работает при отправке сообщений на английском языке и/или русском языке с аргументом «translite» и, с учётом ограничения длины сообщения, поддерживает спец. символы \$@!#%'()*+,-./:;<=>?[]^_`{|}~ Для Телеграмм, API sms.ru и большинства почтовых клиентов (проверено для mail.ru, yandex.ru и gmail.com) есть поддержка национального (русского) текста. Для Телеграмм (опционально при указании $1=«translite») и отправке SMS через модемы, работает транслитерация русских символов в латинские. В работе функции использованы, написанные мной ранее конвертеры строки (приведены и описаны в статьях Хабра habr.com/ru/post/518534 и habr.com/ru/post/519406).
Функция поддерживает отправку сообщений через модем, подключенный к роутеру (usb или pci-e), а также через многофункциональные интернет-контроллеры компаний KernelChip (Laurent-5G) и Алентис-Электроникс (устройства Netping, имеющие модем).
Для работы функции на роутере должны быть сконфигурированы /tool sms и /tool e-mail. При использовании ключа «telegram» должны быть определены global Emoji (Emoji роутера, по желанию), заданы ID Вашего Телеграм-бота и чата (глобальные переменные botID и myChatID). При использовании сервиса sms.ru (ключ «smsAPI») должна быть определена global apikey (ключ аккаунта пользователя на sms.ru). При пересылке сообщения через модем модуля Laurent-5G (ключ smsLT) должны быть определены глобальные переменный Ladr (адрес Laurent-5G в локальной сети) и Lpassword (пароль для авторизации при http get-запросах к модулю). При использовании ключа «smsNP» должны быть определены global NPadr (адрес шлюза Netping в сети), NPuser (login), NPpass (пароль) авторизации.
Выбор номера телефона получателя при всех вариантах отправок sms осуществляется в порядке $1 (из параметра функции) -> $ADMINPHONE (global var) -> first [/tool sms get allowed-number] по алгоритму «если не указан, то следующий». Для smsLT вместо $ADMINPHONE номер телефона берется из первой ячейки базы данных телефонных номеров модуля. Номера телефонов следует указывать в международном формате (+7десятизначный номер), поддерживается отправка sms только по России (+7). Выбор почтового адреса получателя при отправке e-mail осуществляется аналогично в порядке $1 -> $ADMINMAIL (global var).
Ещё раз обратим внимание, что в отличие от обычных функций, которые в $0 хранят своё имя, первый параметр функции массива будет $0, второй $1 и т.д...., что учитывается в коде элементов функции. Вызов функции без ключа, в виде [$FuncAS «you mesage»] вызывает последовательную работу всех элементов функции и равноценен записи [($FuncAS->«all») «you mesage»].
Собственно код функции FuncAS под катом
#--------------------------------------------------------------------------------------------
# FuncAS version 1.4 by Sertik (Serkov S.V.) 09/01/2022
#--------------------------------------------------------------------------------------------
# полностью корректно работает при отправке сообщений на английском языке и/или русском языке с опцией "translite" и с учётом ограничения длины сообщения
# поддерживает спец. символы \$@!#%'()*+,-./:;<=>?[]^_`{|}~ (не для всех вариантов отправок)
;
# Usage: [($FuncAS->"telegram") "you mesage"]
# [($FuncAS->"sms") "you mesage" "phone number"]
# [($FuncAS->"sms") "you mesage" (phone number get from ADMINPHONE or first [/tool sms get allowed-number];
# [($FuncAS->"smsAPI") "you mesage" "phonenumber"]
# [($FuncAS->"smsAPI") "you mesage" (phone number get from ADMINPHONE or or first [/tool sms get allowed-number];
# [($FuncAS->"smsNP") ("привет text H"."\\"."\$"." @!#%'()*+,-./:;<=>?[]^_`{|}~") "+79104797703"]
# [($FuncAS->"smsLT") ("good by world") 2]
# [($FuncAS->"email") "you mesage" "e-mail"]
# [($FuncAS->"email") "you mesage"] (e-mail get from $ADMINMAIL)
# [($FuncAS->"all") "you mesage"] (e-mail get from $ADMINMAIL)
# [$FuncAS "you mesage"] = [($FuncAS->"all") "you mesage"] (execute last key)
# see also the examples at the end of the function code !
# defining global users variables:
:global Emoji; # :global Emoji "%E2%9B%BA"
:global botID; # :global botID "botXXXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
:global myChatID; #:global myChatID "XXXXXXXXX";
:global ADMINPHONE; #:global ADMINPHONE "+7910777777"
:global ADMINMAIL; # :global ADMINMAIL user@mail.ru
:global Ladr; # :global Ladr 192.168.0.10
:global Lpass; # :global Lpass Laurent5G password
:global NPadr; # :global NPadr 192.168.0.10:8080
:global NPuser; # :global NPuser Sertik
:global NPpass; # :global NPpass NPpassword
:global apikey; # :global apikey ХХХХХХХХ-ХХХХ-ХХХХ-ХХХХ-ХХХХХХХХХХХХ
:global FuncAS [:toarray ""]
# function transliteration
# string for transliteration is set in the parametr name "string"
:set ($FuncAS->"translite") do={
# table of the codes of Russian letters translite
:local rsimv [:toarray {"А"="A"; "Б"="B"; "В"="V"; "Г"="G"; "Д"="D"; "Е"="E"; "Ж"="ZH"; "З"="Z"; "И"="I"; "Й"="J"; "К"="K"; "Л"="L"; "М"="M"; "Н"="N"; "О"="O"; "П"="P"; "Р"="R"; "С"="S"; "Т"="T"; "У"="U"; "Ф"="F"; "Х"="KH"; "Ц"="C"; "Ч"="CH"; "Ш"="SH"; "Щ"="SCH"; "Ъ"="``"; "Ы"="Y`"; "Ь"="`"; "Э"="E`"; "Ю"="JU"; "Я"="YA"; "а"="a"; "б"="b"; "в"="v"; "г"="g"; "д"="d"; "е"="e"; "ж"="zh"; "з"="z"; "и"="i"; "й"="j"; "к"="k"; "л"="l"; "м"="m"; "н"="n"; "о"="o"; "п"="p"; "р"="r"; "с"="s"; "т"="t"; "у"="u"; "ф"="f"; "х"="kh"; "ц"="c"; "ч"="ch"; "ш"="sh"; "щ"="sch"; "ъ"="``"; "ы"="y`"; "ь"="`"; "э"="e`"; "ю"="ju"; "я"="ya"; "Ё"="Yo"; "ё"="yo"; "№"="#"}]
# encoding of the symbols and аssembly line
:local StrTele ""; :local code "";
:for i from=0 to=([:len $string]-1) do={:local keys [:pick $string $i (1+$i)];
:local key ($rsimv->$keys); if ([:len $key]!=0) do={:set $code ($rsimv->$keys);} else={:set $code $keys};
:if (($keys="Ь") and ([:pick $string ($i+1) (2+$i)]="Е")) do={:set $code "I"; :set $i ($i+1)}
:if (($keys="ь") and ([:pick $string ($i+1) (2+$i)]="е")) do={:set $code "i"; :set $i ($i+1)}
:if (($keys="Ь") and ([:pick $string ($i+1) (2+$i)]="е")) do={:set $code "I"; :set $i ($i+1)}
:if (($keys="ь") and ([:pick $string ($i+1) (2+$i)]="Е")) do={:set $code "i"; :set $i ($i+1)}
:if (($keys="Ы") and ([:pick $string ($i+1) (2+$i)]="Й")) do={:set $code "I"; :set $i ($i+1)}
:if (($keys="ы") and ([:pick $string ($i+1) (2+$i)]="й")) do={:set $code "i"; :set $i ($i+1)}
:if (($keys="ы") and ([:pick $string ($i+1) (2+$i)]="Й")) do={:set $code "i"; :set $i ($i+1)}
:if (($keys="Ы") and ([:pick $string ($i+1) (2+$i)]="й")) do={:set $code "I"; :set $i ($i+1)}
:set $StrTele ("$StrTele"."$code")}
:return $StrTele
}
# part to Telegram
:set ($FuncAS->"telegram") do={
:if ([:len $0]!=0) do={
:global Emoji
:global botID;
:global myChatID;
:if (([:len $botID]=0) or ([:len $myChatID]=0)) do={:log error "ERROR FuncAs telegram chatbotID not found"; :return "ERROR telegram"}
:if ((any $1) && ($1="translite")) do={
:global FuncAS
:set $0 [($FuncAS->"translite") string=$0]
}
# bypassing the transmission of special characters and symbol "H"
:local output $0
:local cp1251 [:toarray {"\20";"\01";"\02";"\03";"\04";"\05";"\06";"\07";"\08";"\09";"\0A";"\0B";"\0C";"\0D";"\0E";"\0F"; \
"\10";"\11";"\12";"\13";"\14";"\15";"\16";"\17";"\18";"\19";"\1A";"\1B";"\1C";"\1D";"\1E";"\1F"; \
"\21";"\22";"\23";"\24";"\25";"\26";"\27";"\28";"\29";"\2A";"\2B";"\2C";"\2D";"\2E";"\2F";"\3A"; \
"\3B";"\3C";"\3D";"\3E";"\3F";"\40";"\5B";"\5C";"\5D";"\5E";"\5F";"\60";"\7B";"\7C";"\7D";"\7E"; \
"\C0";"\C1";"\C2";"\C3";"\C4";"\C5";"\C7";"\C7";"\C8";"\C9";"\CA";"\CB";"\CC";"\CD";"\CE";"\CF"; \
"\D0";"\D1";"\D2";"\D3";"\D4";"\D5";"\D6";"\D7";"\D8";"\D9";"\DA";"\DB";"\DC";"\DD";"\DE";"\DF"; \
"\E0";"\E1";"\E2";"\E3";"\E4";"\E5";"\E6";"\E7";"\E8";"\E9";"\EA";"\EB";"\EC";"\ED";"\EE";"\EF"; \
"\F0";"\F1";"\F2";"\F3";"\F4";"\F5";"\F6";"\F7";"\F8";"\F9";"\FA";"\FB";"\FC";"\FD";"\FE";"\FF"; \
"\A8";"\B8";"\B9";"\48"}];
:local utf8 [:toarray {"0020";"0020";"0020";"0020";"0020";"0020";"0020";"0020";"0020";"0020";"000A";"0020";"0020";"000D";"0020";"0020"; \ "0020";"0020";"0020";"0020";"0020";"0020";"0020";"0020";"0020";"0020";"0020";"0020";"0020";"0020";"0020";"0020"; \
"0021";"0022";"0023";"0024";"0025";"0026";"0027";"0028";"0029";"002A";"002B";"002C";"002D";"002E";"002F";"003A"; \ "003B";"003C";"003D";"003E";"003F";"0040";"005B";"005C";"005D";"005E";"005F";"0060";"007B";"007C";"007D";"007E"; \ "D090";"D091";"D092";"D093";"D094";"D095";"D096";"D097";"D098";"D099";"D09A";"D09B";"D09C";"D09D";"D09E";"D09F"; \ "D0A0";"D0A1";"D0A2";"D0A3";"D0A4";"D0A5";"D0A6";"D0A7";"D0A8";"D0A9";"D0AA";"D0AB";"D0AC";"D0AD";"D0AE";"D0AF"; \ "D0B0";"D0B1";"D0B2";"D0B3";"D0B4";"D0B5";"D0B6";"D0B7";"D0B8";"D0B9";"D0BA";"D0BB";"D0BC";"D0BD";"D0BE";"D0BF"; \ "D180";"D181";"D182";"D183";"D184";"D185";"D186";"D187";"D188";"D189";"D18A";"D18B";"D18C";"D18D";"D18E";"D18F"; \
"D001";"D191";"2116";"0048"}];
:local convStr ""; :local code "";
:for i from=0 to=([:len $output]-1) do={
:local symb [:pick $output $i ($i+1)];
:local idx [:find $cp1251 $symb];
:local key ($utf8->$idx);
:if ([:len $key] != 0) do={
:set $code ("%$[:pick ($key) 0 2]%$[:pick ($key) 2 4]");
:if ([pick $code 0 3] = "%00") do={ :set $code ([:pick $code 3 6]); }
} else={ :set code ($symb); };
:set $convStr ($convStr.$code);
}
:set $0 $convStr
:log info ""; :log warning "FuncAs start telegram sending ...";
:do {
[/tool fetch url="https://api.telegram.org/$botID/sendmessage\?chat_id=$myChatID&text=$Emoji $0" keep-result=no;]
} on-error={:log error "ERROR FuncAs telegram send"; :return "ERROR telegram"}
:log warning "FuncAs message to telegram send"; :log info "";
:return "done telegram"
} else={:log error "ERROR FuncAs telegram send but no message text"; :return "ERROR telegram" }
}
# part to sms from modem router`s
:set ($FuncAS->"sms") do={
:if ([:len $0]!=0) do={
:local SMSdevice [/tool sms get port];
:local NumPhone
:global ADMINPHONE;
:if ([:len $1]!=0) do={:set NumPhone $1} \
else={
:if ([:len $ADMINPHONE]!=0) do={:set NumPhone $ADMINPHONE} \
else={
:local NumSMS [/tool sms get allowed-number];
:if ([:len $NumSMS]!=0) do={:set NumPhone ($NumSMS->0)} \
else={:log error "ERROR FuncAs sms phone number not found"; :return "ERROR sms"}
}}
# must be performed translite
:global FuncAS
:set $0 [($FuncAS->"translite") string=$0]
:log info ""; :log warning "FuncAS start sms sending to $NumPhone";
:do {
[/tool sms send $SMSdevice phone=$NumPhone message=$0];
} on-error={:log error "ERROR FuncAs sms send"; :return "ERROR sms"}
:log warning "FuncAs sms sent via modem"; :log info "";
:return "done sms"
} else={:log error "ERROR FuncAs sms send but no message text"; :return "ERROR sms" }
}
# part to sms from API sms.ru
# must be configured global variable $apikey for sms.ru user
;
:set ($FuncAS->"smsAPI") do={
:if ([:len $0]!=0) do={
:local SMSanswer;
:global apikey;
:if ([:len $apikey]!=0) do={
:local NumPhone
:global ADMINPHONE;
:if ([:len $1]!=0) do={:set NumPhone $1} \
else={
:if ([:len $ADMINPHONE]!=0) do={:set NumPhone $ADMINPHONE} \
else={
:local NumSMS [/tool sms get allowed-number];
:if ([:len $NumSMS]!=0) do={:set NumPhone ($NumSMS->0)} \
else={:log error "ERROR FuncAs smsAPI phone number not found"; :return "ERROR smsAPI"}
}}
:log info ""; :log warning "FuncAS start smsAPI sending to $NumPhone";
:do {
:set SMSanswer [/tool fetch url=("https://sms.ru/sms/send\?api_id="."$apikey") mode=https http-method=post http-data=("&to="."$NumPhone"."&msg="."$0"."&json=1") as-value output=user];
} on-error={:log error "ERROR FuncAs smsAPI fetch send"; :return "ERROR smsAPI"}
:if (($SMSanswer->"status")="finished") do={
:local ans;
:if ([find ($SMSanswer->"data") "ERROR"]>0) do={:set ans "ERROR smsAPI"; :log error "ERROR FuncAs smsAPI send"; :log info "";} else={:set ans "done smsAPI"; :log warning "FuncAs sms sent via API sms.ru"; :log info "";}
:return $ans;
} else={:log error "ERROR FuncAs smsAPI fetch to sms.ru"; :return "ERROR smsAPI"}
} else={:log error "ERROR FuncAs smsAPI apikey not defined"; :return "ERROR smsAPI"}
} else={:log error "ERROR FuncAs smsAPI send but no message text"; :return "ERROR smsAPI" }
}
# part to sms from Laurent-5G module
# must be configured Ladr, Lpass
:set ($FuncAS->"smsLT") do={
:if ([:len $0]!=0) do={
:local Lanswer;
:global Ladr
:global Lport
:global Lpass
:local Sport
:if (([:len $Ladr]!=0) && ([:len $Lpass]!=0)) do={
:if ([:len $0]!=0) do={
:if ([:len $1]=0) do={:set $1 "1"}
:if ([:len $Lport]=0) do={:set Sport "80";} else={:set Sport $Lport;}
# must be performed translite
:global FuncAS
:set $0 [($FuncAS->"translite") string=$0]
# replacing a space in a line with a sign "+"
# for normal space sending
:local string $0
:local StrTele
:for i from=0 to=([:len $string]-1) do={:local keys [:pick $string $i (1+$i)];
# \2B="+" or you can use \5F ("_")
:if ($keys=" ") do={:set $code "\2B";} else={:set $code $keys};
:set $StrTele ("$StrTele"."$code")}
:set $0 $StrTele;
:log info ""; :log warning "FuncAS start sms from Laurent getway sending to $1 database cell";
:local StrFetchLaurent; :set StrFetchLaurent ("http://"."$Ladr".":"."$Sport"."/cmd.cgi?psw=$Lpass&cmd=SMS,"."SND".","."$1,"."C,"."$0");
do {
:set Lanswer ([/tool fetch url=$StrFetchLaurent mode=http as-value output=user]->"data");
} on-error={: log info ""; :log error ("Call ERROR function <$0> ERROR fetch command");
:local Lanswer "ERROR $0 command ROS <fetch>"; : log info ""; :return $Lanswer}
:if ($Lanswer="#SMS,SND,OK\0D\0A") do={:log warning "FuncAs sms sent via Laurent getway"; :log info ""; :return "done smsLT";} else={
:if ($Lanswer="#ERR,NO_FREE_PLACE\0D\0A") do={:log info ""; :log error ("ERROR function FuncAS smsLT but no free place");
:log info ""; :return "ERROR FuncAS smsLT no free place"}
:if ($Lanswer="#ERR,BAD_PHN\0D\0A") do={:log info ""; :log error ("ERROR function FuncAS smsLT but invalid phone number");
:log info ""; return "ERROR FuncAS smsLT bad phone number"}
:return "ERROR smsLT unknown"}
} else={:log info ""; :log error "ERROR FuncAs smsLT parametrs no defined"; :return "ERROR smsLT"}
} else={:log info ""; :log error "ERROR FuncAS smsLT Ladr or Lpass not defined"; :return "ERROR smsLT"}
}
}
# part to sms from Netping device modem (as getway)
# [$FuncNPsendSMS "you text in utf8" "+7phonenumber"]
# должны быть определены NPadr, NPuser, NPpass
:set ($FuncAS->"smsNP") do={
:if ([:len $0]!=0) do={
:global NPadr; :global NPuser; :global NPpass;
:if (([:len $NPadr]!=0) && ([:len $NPuser]!=0) && ([:len $NPpass]!=0)) do={
:local NPanswer;
:local NumPhone
:global ADMINPHONE;
:if ([:len $1]!=0) do={:set NumPhone $1} \
else={
:if ([:len $ADMINPHONE]!=0) do={:set NumPhone $ADMINPHONE} \
else={
:local NumSMS [/tool sms get allowed-number];
:if ([:len $NumSMS]!=0) do={:set NumPhone ($NumSMS->0)} \
else={:log error "ERROR FuncAs smsNP phone number not found"; :return "ERROR smsAPI"}
}}
:if ([:pick $NumPhone 0 2]="+7") do={
# must be performed translite
:global FuncAS
:set $0 [($FuncAS->"translite") string=$0]
:log info ""; :log warning "FuncAS start sms from Netping getway sending to $NumPhone";
:do {
:set NPanswer [/tool fetch url=("http://"."$NPadr"."/sendsms.cgi\?utf8") mode=http http-method=post http-data=("["."$NumPhone"."]"." "."$0") user=$NPuser password=$NPpass as-value output=user];
} on-error={:log error "ERROR FuncAs smsNP fetch send"; :return "ERROR smsNP"}
:if (($NPanswer->"status")="finished") do={:log warning "FuncAs sms sent via Netping getway"; :log info "";
:set NPanswer ($NPanswer->"data");
:if ([:find $NPanswer "ok"]) do={:return "done smsNP"} else={:return "ERROR smsNP"}
} else={ :return "ERROR smsNP"}
} else={:log error "ERROR FuncAs smsNP invalid phone number format"; :return "ERROR smsNP"}
} else={:log error "ERROR FuncAs smsNP NPadr or NPuser or NPpass not defined"; :return "ERROR smsNP"}
} else={:log error "ERROR FuncAs smsNP send but no message text"; :return "ERROR smsNP" }
}
# part to e-mail
:set ($FuncAS->"email") do={
:if ([:len $0]!=0) do={
:global ADMINMAIL;
:if (([:len $1]=0) && ([:len $ADMINMAIL]=0)) do={:log error "ERROR FuncAs email send E-mail address not found"; :return "ERROR email"}
:if ([:len $1]!=0) do={} else={:set $1 $ADMINMAIL}
:local smtpserv
:do {
:set smtpserv [:resolve [/tool e-mail get address]];} on-error={:log error "ERROR FuncAs email no resolve email server"; :return "ERROR email"}
:local Eaccount [/tool e-mail get user];
:local pass [/tool e-mail get password];
:local Eport [/tool e-mail get port];
:local Etls "yes"; # если не используется установить в "no";
:log info " "; :log warning ("FuncAS start mail sending ... to e-mail: $1");
:do {
[/tool e-mail send from="<$Eaccount>" to=$1 server=$smtpserv \
port=$Eport user=$Eaccount password=$pass start-tls=$Etls subject=("from FuncAS Router $[/system identity get name]") body=$0;];
} on-error={:log error "ERROR FuncAs email send"; :return "ERROR email"}
:local sendit 60
:while ($sendit > 0) do={:delay 1s; :set $sendit ($sendit - 1)
:if (([tool e-mail get last-status] = "succeeded") or ([tool e-mail get last-status] = "failed")) do={:set $sendit 0}}
:if ([tool e-mail get last-status] = "succeeded") do={:log warning "FuncAs message to email send"; :log info ""; :return "done email"} \
else={:log error "ERROR FuncAs email send"; :return "ERROR email"}
} else={:log error "ERROR FuncAs email send but no message text"; :return "ERROR email" }
}
# part to all send channels (a psevdo recursive call is used !)
# must be the last to bypass the $0 <-->$1 effect
:set ($FuncAS->"all") do={
:if ([:len $0]!=0) do={
:global FuncAS;
:global ADMINMAIL;
:local T
:local E
:local MDsms
:local APIsms
:local LTsms
:local NPsms
# bypass when calling a function without keys !
:if ([:typeof $0]="lookup") do={:set $0 $1; :if ((any $2) && ($2="translite")) do={:set $1 $2}}
:set T [($FuncAS->"telegram") $0 $1]
:set E [($FuncAS->"email") $0 $ADMINMAIL]
:set MDsms [($FuncAS->"sms") $0]
:set APIsms [($FuncAS->"smsAPI") $0]
:set LTsms [($FuncAS->"smsLT") $0]
:set NPsms [($FuncAS->"smsNP") $0]
:return [:toarray {$T;$E;$MDsms; $APIsms; $LTsms; $NPsms}]
} else={:log error "ERROR FuncAs all but no message text"; :return "ERROR common sending" }
}
# -------------------------------------------------------------------
# examples call:
# -------------------------------------------------------------------
# :global FuncAS
# :log info [($FuncAS->"telegram") ("Router "."$[/system identity get name]"." test function FuncAS to Telegram Hello тест функции FuncAS для Телеграмм")]
# :log info [($FuncAS->"telegram") ("Router "."$[/system identity get name]"." test function FuncAS to Telegram Hello тест функции FuncAS для Телеграмм") "translite"]
# :log info [($FuncAS->"email") ("Router "."$[/system identity get name]"." test FuncAS to e-mail проверка") "user@gmail.com"]
# :log info [($FuncAS->"email") ("Router "."$[/system identity get name]"." test FuncAS to e-mail")]
# :log info [($FuncAS->"sms") ("Router "."$[/system identity get name]"." test function FuncAS test to SMS Привет !")]
# :log info [($FuncAS->"all") ("Router "."$[/system identity get name]"." test function FuncAS all channels")]
# exception support for Telegram
# :log info [($FuncAS->"telegram") ("text H"."\\"."\$"." @!#%'()*+,-./:;<=>?[]^_`{|}~")]
# :log info [($FuncAS->"smsAPI") "Verify for Sertik" "+79107777777"]
# :log info [($FuncAS->"smsAPI")]
# :log info [($FuncAS->"smsAPI") "смс от Микротик to you" "+79654123794"]
# exception transliteration for Telegram and send SMS modem
# :log info [($FuncAS->"smsNP") ("привет text H"."\\"."\$"." @!#%'()*+,-./:;<=>?[]^_`{|}~") "+79657777777"]
# :log info [($FuncAS->"telegram") ("Router "."$[/system identity get name]"." Привет админ ! С новым годом тебя ...") "translite"]
# :log info [$FuncAS "С новым годом !"]
# :log info [$FuncAS "С новым годом !" "translite"]
# :log info [($FuncAS->"smsLT") ("good by world") 2]
# :log info [($FuncAS->"smsLT") ("привет text H"."\\"."\$"." @!#%'()*+,-./:;<=>?[]^_`{|}~") 2]
# :log info [$FuncAS "good by world" sv_device@mail.ru]
# :log info [($FuncAS->"all") ("good by world до свидания мир") "translite"]
Основные аспекты работы функции отмечены в комментариях к коду. Функция объединяет шесть вариантов отправки сообщений посредством различных служб и устройств и способна работать как частично (отправляя сообщение по какому-либо одному каналу) так и полностью по всем последовательно. Служит наглядным примером того, как можно создавать функции в массивах. Вероятно, можно такое проделать и в многомерном массиве, однако ввиду усложнений синтаксиса вряд ли это необходимо.
Из вышесказанного следует также, что в функции-массиве можно разместить как «подфункции» так и данные.
Обратите внимание на последнюю часть функции под ключом ($FuncAS->«all»). Помня про lookup мы проверяем откуда вызвана данная часть функции: как элемент массива по ключу «all» или как обычная целая функция $FuncAS в этой строке кода:
:if ([:typeof $0]="lookup") do={:set $0 $1; :if ((any $2) && ($2="translite")) do={:set $1 $2}}
Если функция вызвана без ключа, то осуществляется «передвижка» аргументов: $1 «передвигается» в $0, а $2, когда это нужно для работы блока транслитерации, становится $1.
Разумеется можно «разобрать» функцию массив на более привычные нам, отдельные функции, и использовать каждую со своим именем, однако объединение в функцию-массив тематически однообразных подфункций представляется удобным и логичным решением.
С 2017 года мной были созданы и собраны около 200 функций самого разного назначения, многие из которых ежедневно помогают мне в работе над скриптами Роутер ОС. Конечной целью было бы создание сайта-библиотеки скриптов и функций, которым могли бы пользоваться все желающие. В настоящее время можно обсуждать всё, что связано со скриптовыми функциями на русскоязычном форуме Микротик по ссылке.
Надеюсь, моя статья будет полезной пользователям изучающим скрипты Роутер ОС Микротик. Всем желаю удачи в изучении скриптов и успехов в программировании.
© Серков Сергей Владимирович 2022 (Sertik)