Исследование функций и скриптов в Mikrotik script. Рассматриваются разные способы создания и вызова функций и скриптов с передачей в них параметров. Оператор :parse и особый тип данных code.

1. Что такое функция в Mikrotik script?

Если почитать Manual:Scripting (https://wiki.mikrotik.com/wiki/Manual:Scripting#Functions), видно, что на данный момент функции можно создавать двумя способами: через команду :parse и упрощенным способом через конструкцию do={...}. Сначала рассмотрим второй способ. Объяснение буду давать на примерах.

Определение функций через конструкцию do={...}

Функцию можно определить, например, следующим образом:

[admin@MikroTik] > :global Fun1 do={ :return "result $0 $1 $var1 $2 $var2" }

Тут используются безымянные позиционные аргументы $0, $1, $2, и именованные аргументы $var1, $var2. Оператор :return возвращает результат вычисления. В данном примере это строка.

Пример классического вызова функции (через переменную $Fun1):

[admin@MikroTik] > :put [$Fun1 1 var2=22 "3" var1="4"] 
result $Fun1 1 4 3 22

Оператор :put отображает возвращаемую функцией строку. Видно, что аргумент $0 просто указывает на имя самой же переменной, хранящей код функции. Таким образом первый передаваемый в функцию позиционный аргумент в данном случае $1

Не забываем, что перед вызовом глобальной функции в скрипте нужно ее определить и объявить. Если глобальная функция уже была определена ранее (есть в /environment), то ее достаточно только объявить (без определения) перед первым вызовом в скрипте. Удобнее это делать в начале скрипта.

:global Fun1
:put [$Fun1 1 var2=22 "3" var1="4"]

Посмотрим, что внутри функции, а точнее переменной, содержащей код функции.

[admin@MikroTik] > :put $Fun1 
;(evl (evl /returnvalue=(. result  $0   $1   $var1   $2   $var2)))

Внутри переменной $Fun1 массив, элементы которого имеют следующие типы:

[admin@MikroTik] > :put [:typeof $Fun1]
array
[admin@MikroTik] > :put [:typeof ($Fun1->0)]
nil
[admin@MikroTik] > :put [:typeof ($Fun1->1)]
code

Первый элемент нам не интересен, т.к. он не содержит ничего. А вот второй элемент уже представляет больший интерес, это тот самый код функции (тип данных code), который представляет собой распарсенный блок do={...} из определения функции. Почему Mikrotik поместил код функции в массив, да еще во второй элемент - загадка. Но вы можете вызвать код функции через этот второй элемент. Вообще можно выполнять напрямую все, что имеет тип данных code.

Давайте "выполним" второй элемент массива. Также добавим еще одно значение позиционного аргумента "5"):

[admin@MikroTik] /environment> :put [($Fun1->1) 1 var2=22 "3" var1="4" 5]
result 1 3 4 5 22

Вот чудо, $0 теперь не ссылается на имя переменной функции, а является первым передаваемым позиционным аргументом!

Давайте еще раз посмотрим code:

[admin@MikroTik] /environment> :put ($Fun1->1)
(evl (evl /returnvalue=(. result  $0   $1   $var1   $2   $var2)))

Как я уже писал, это распарсенный код из определения функции. Хочу отметить, что переменные (аргументы) тут входят в состав выражения динамически. Т.е. подстановка значений будет происходить во время вызова функции.

Идем дальше. Добавим в функцию вызов какой-нибудь команды API, например, запросим версию прошивки роутера:

[admin@MikroTik] > :global Fun2 do={ :return "result $1 $var1 $[/system/resource/get version ]" }
[admin@MikroTik] > :put [$Fun2 aaa var1=bbb]
result aaa bbb 7.1rc5 (testing)

Посмотрим содержимое функции:

[admin@MikroTik] > :put $Fun2
;(evl (evl /returnvalue=(. result  $1   $var1   (evl (evl /system/resource/getvalue-name=version)))))

Видно, что /system/resource/get также выполняется динамически во время вызова функции (Fun2), т.е. вычисление выполняется при вызове, но не во время определения функции. Для большинства задач это правильное поведение, не возникает неоднозначности. Все всегда вычисляется при выполнении, будь то вызовы API роутера или передача аргументов внутрь функции. Но версия прошивки точно не меняется между вызовами скриптовых функций и логичнее (и быстрее) было бы вычислить это выражение еще на этапе парсинга, добавив результат в качестве константы в содержимое функции (code).
Если вы хотите более тонкой оптимизации, добро пожаловать в функции на базе оператора :parse.

Определение функций через оператор :parse

Пример объявления-определения и вызова:

[admin@MikroTik] > :global FunX [:parse ":return 123"]
[admin@MikroTik] > :put [$FunX]
123

Посмотрим, что внутри такой функции, а точнее, что содержит переменная $FunX:

[admin@MikroTik] > :put $FunX; :put [:typeof $FunX]
(evl /returnvalue=123)
code

Оператор :parse преобразует строку (с кодом) в тот самый тип данных code, упомянутый ранее. Похоже на определение функции при помощи блока do={...}, но более сложное. Добавляется еще один уровень абстракции. Ведь теперь код функции изначально должен быть представлен в виде строки - аргумента оператора :parse. Зато такой способ при его должном понимании несет дополнительные возможности. Парсинг выполняется явно, под нашим контролем, с помощью управления экранированием символов. Давайте перепишем функцию Fun1 на этот лад.

:global FunA [:parse " :return \"result \$0 \$1 \$var1 \$2 \$var2\" "]
[admin@MikroTik] > :put [$FunA 1 var2=22 "3" var1="4"]
result $FunA 1 4 3 22

Содержимое $FunA:

[admin@MikroTik] > :put $FunA; :put [:typeof $FunA]
(evl /returnvalue=(. result  $0   $1   $var1   $2   $var2))
code

Очень похоже на code функции Fun1, все то же динамическое вычисление результата при каждом вызове функции. В строке-коде " :return \"result \$0 \$1 \$var1 \$2 \$var2\" " заэкранированы все переменный и внутренние кавычки.

А что будет, если мы не будем экранировать переменные?

Объявим такую функцию:

:global FunB [:parse " :return \"result $0 $1 $var1 $2 $var2\" "]

Результат вызова и содержимое:

[admin@MikroTik] > :put [$FunB 1 var2=22 "3" var1="4"]
result  
[admin@MikroTik] > :put $FunB
(evl /returnvalue=result     )

Получилась какая-то ерунда. Действительно, оператор :parse пытается подставить все переменные ($0, $var1 и т.д.) на этапе парсинга строки-кода, но они еще не определены на этапе определения функции FunB! Поэтому эти переменные должны быть объявлены и определены заранее, например:

[admin@MikroTik] > {
:local 0 "zero"
:local 1 "one"
:local 2 "two"
:local var1 111
:local var2 222
:global FunB [:parse " :return \"result $0 $1 $var1 $2 $var2\" "]
:put [$FunB]
}
result zero one 111 two 222

Или так:

[admin@MikroTik] > :local 0 "zero"; :local 1 "one"; :local 2 "two"; :local var1 111; :local var2 222; :global FunB [:parse " :return \"result $0 $1 $var1 $2 $var2\" "]; :put [$FunB]
result zero one 111 two 222

А что внутри $FunB:

[admin@MikroTik] > :put $FunB
(evl /returnvalue=result zero one 111 two 222)

Видно, что подстановка параметров произошла на этапе определения функции, и в code нет динамической подстановки, только строка с уже произведенными заменами.

А ведь можно еще так:

:global FunC [:local 0 "zero"; :local 1 "one"; :local 2 "two"; :local var1 111; :local var2 222; :parse " :return \"result $0 $1 $var1 $2 $var2\" "]; :put [$FunC]
result zero one 111 two 222

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

Осталось переписать под :parse Fun2 (там, где запрашивается версия прошивки роутера):

[admin@MikroTik] > :global FunD [:parse " :return \"result \$1 \$var1 \$[/system/resource/get version]\" "]
[admin@MikroTik] > :put [$FunD aaa var1=bbb]
result aaa bbb 7.1rc5 (testing)
[admin@MikroTik] > :put $FunD
(evl /returnvalue=(. result  $1   $var1   (evl (evl /system/resource/getvalue-name=version))))

Эта функция аналогична Fun2.

Теперь уберем экранирование [/system/resource/get version], т.е. заставим вычисляться это выражение на этапе определения функции:

[admin@MikroTik] > :global FunE [:parse " :return \"result \$1 \$var1 $[/system/resource/get version]\" "]
[admin@MikroTik] > :put [$FunE aaa var1=bbb]
result aaa bbb 7.1rc5 (testing)
[admin@MikroTik] > :put $FunE
(evl /returnvalue=(. result  $1   $var1  7.1rc5 (testing)))

Видите, code стал компактнее и с уже подставленной версией прошивки?

2. Если функция - это тип данных code + переменная, то всегда ли нужна переменная?

Еще один интересный момент, которые следует из анализа определения функции с оператором :parse. Посмотрим на объявление $FunE, по сути это просто объявление переменной $FunE c присвоением ей результата парсинга:

[:parse " :return \"result \$1 \$var1 $[/system/resource/get version]\" "]


Таким образом можно делать вызовы без объявления переменной:

[admin@MikroTik] > :put [[:parse " :return \"result \$1 \$var1 $[/system/resource/get version]\" "] aaa var1=bbb]
result  bbb 7.1rc5 (testing)

Тут используется вложенные операторы вызова [...].

Но куда же пропал аргумент $1? При таком вызове первый позиционный аргумент будет $0, ведь больше нет имени переменой-функции.

[admin@MikroTik] > :put [[:parse " :return \"result \$0 \$1 \$var1 $[/system/resource/get version]\" "] aaa var1=bbb ccc]
result aaa ccc bbb 7.1rc5 (testing)

3. Можно ли при вызове скриптов передавать параметры внутрь?

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

Но есть обходной способ, который опирается на технику, описанную выше. Создадим скрипт со следующим содержимым и назовем его scriptA:

# --- scriptA ---
:put "test $0"
:put "test $1"
:put $var1
:return "result $0 $1 $var1 $[/system/resource/get version ]"
# ---------------

Можно запросить командой API содержимое скрипта, распарсить и вызвать его по аналогии с функцией с передачей параметров внутрь. Также можно использовать локальную переменную для хранения code скрипта.

[admin@MikroTik] > :local script [:parse [/system/script/get scriptA source]]; :put $script 
(evl /putmessage=(. test  $0));(evl /putmessage=(. test  $1));(evl /putmessage=$var1);(evl /returnvalue=(. result  $0   $1   $var1   (evl (evl 
/system/resource/getvalue-name=version))))

[admin@MikroTik] > :local script [:parse [/system/script/get scriptA source]]; :put [$script aaa bbb var1=ccc]
test $script
test aaa
ccc
result $script aaa ccc 7.1rc5 (testing)

Или вообще сделать вызов без использования промежуточной переменной для хранения code:

[admin@MikroTik] > :put [[:parse [/system/script/get scriptA source]] aaa bbb var1=ccc]
test aaa
test bbb
ccc
result aaa bbb ccc 7.1rc5 (testing)

В scriptA, кстати, присутсвует оператор :return. В результате вызова скрипта действительно возращается значение-строка, скрипт в таком вызове работает как функция.

Резюмируя написанное, можно сказать, что функция в Mikrotik script это распарсенный код, присвоенный обычной локальной или глобальной переменной. А точнее тип данных code, который получается в результате явной или неявной (конструкция do={...}) работы оператора :parse. :parse в свою очередь может либо сразу подменять/вычислять переменные или выражения, начинающиеся на $, либо, если использовалось экранирование \$, сохранять переменные в code. И есть оператор вызова [...], который умеет выполнять этот code.

4. Про оператор :return

Оператор :return служит для возвращения из функций результатов вычислений. Он обладает одной полезной особенностью: после него выполнение функции сразу завершается. Его можно использовать для создания альтернативы вложенных if-else конструкций. Известно, что в Mikrotik script нет оператора switch и нет elseif альтернатив оператора if. Получаются очень громоздкие конструкции ветвлений, типа:

:if () do={
...
} else={
  :if () do={
    ...
  } else={
    :if () do={
      ...
    } else={
      ...
    }
  }
}

Если описать функцию-селектор с :return в каждом if, то можно получить компактную и понятную конструкцию:

:global selector do={
  :if ($1 = "A") do={
    ...
    :return a
  }
  :if ($1 ="B") do={
    ...
    :return b
  }
  :if ($1 = "C") do={
    ...
    :return c
  }
}

Комментарии (0)