A few undocumented operators that are kind of neat.

This post will only be of interest to a few select individuals. If you are unfamiliar with mikrotik scripting, or have never used functions, this post is probably not for you, but you’re welcome to read it anyway.

I’ll look at two operators I found by hitting F1 while in some parentheses.

The first one we’ll look at is called quote. It is a unary ‘>’ operator and functions exactly like quote in LISP, for anyone familiar.
It returns whatever it’s applied to as a literal. Even code. This means it can be used as an alternate way to make functions.
Here are all three ways I know of to make a function, including this new operator in action:

:global func1 do={:put "test1"}
:global func2 [parse ":put \"test2\""]
:global func3 (>[:put "test3"])

Interestingly, all 3 methods produce slightly different results. Here’s what you get when you put all 3 of these

put $func1
# ;(evl (evl /putmessage=test1))
put $func2
# (evl /putmessage=test2)
put $func3
# (evl (evl /putmessage=test3))

func1 is array with 2 elements, the second being a code object. The other 2 are code objects themselves. If you don’t know what I mean by this, try using typeof on these.

In practical terms (at least as far as I can tell), these are all treated identically, and it doesn’t matter which method you use to produce your functions. Technically there’s a very slight performance increase using parse, and it uses slightly less memory for variable storage, because it lacks the additional evl and isn’t in an array. These are probably negligible for everything you could want to do with this.

I’m sure I haven’t fully explored the power of this operator, for instance, I haven’t used it on a string with embedded command substitutions. That could be fun. But I’m moving on.

The next operator I’ll talk about is one labelled “activate in environment”. I first discovered this one back in June of last year, a coworker made a forum post about it for me, and rextended was also confused, I shelved it. But now I’ve finally cracked it, and I think it’s pretty neat.

The operator looks like this: <%%. It takes a little explaining. Take “environment” in this context to just mean “the collection of variables available”. What this operator actually does is run the code object on the left hand side with the array on the right hand side added to it’s environment.

As an example, say you make the following function:

:global add (>[:return ($0+$1)])
# This can now be called like this to print 3
:put [$add 1 2]
# To use our new operator with the same result, you do this:
:put ($add <%% {1;2})

Edit: using brackets here doesn’t actually work because $0 is the name of the function. It would have to be $1+$2 and the “environment” passed to <%% would be {“add”;1;2} for equivalence


If you have a keen eye, you just noticed that the contents of the array were accessed with $0 and $1. The array that was passed in is now the environment (collection of variables) for the function, so the 0th element is accessed with $0 and the 1st element is accessed with $1. This alone isn’t particularly powerful, so let’s look at another example.

:global f (>[:put $output])
# We have not defined output anywhere. It does not exist. And yet:
($f <%% {output="I exist!"})
# This will add the output variable from the array to the environment, giving the function access to output
# Right now, it's still equivalent to
[$f output="I exist!"]

This is more exciting! We can make objects the encapsulate parameters for a function, and give it that object. This alone may be giving some people some ideas. But if this excites you, I have another example to blow your mind. What if I told you that the array you pass in is passed by reference?

:global inc (>[:set $counter ($counter+1)])
:global c {counter=0}
# What do you suppose happens here?
($inc <%% $c)
# There is no longer an equivalent with bracket notation
# This is NOT equivalent anymore
[$inc $c]

Believe it or not, this works how you think it should. Go ahead and put the value of $c afterwards. You’ll find that counter has indeed incremented to 1. We can now pass in an object (array), do something with it’s variables, and have those changes stick. This is new functionality unique to this operator. It almost feels like object oriented programming, but it’s not quite that powerful.

I can understand why Mikrotik hasn’t documented this, and I don’t expect them to going forward. It’s niche, and only a handful of users could even hope to find a helpful use case. I’m not even claiming I have one. But it’s another quirky tool to help write some interesting scripts, and I look forward to the possibilities.

Sorry for delay, I do not notice this post before,
very good job, thanks.

:global f1 do={:return 5}
:global f2 do={:return 7}

:global add (>[:return ([$f1]+[$f2])])

:put ($add <%% {$f1;$f2})

12

:global f1 do={:return $1}
:global f2 do={:return $1}

:global add (>[:return ([$f1 2]+[$f2 3])])

:put ($add <%% {$f1;$f2})

5

:global f1 do={:return [/system clock get time]}
:global f2 do={:return [/system clock get date]}

:global add (>[:return ([$f1]." ".[$f2])])

:put ($add <%% {$f1;$f2})

17:37:48 jun/13/2024


:global f1 do={:return [/system clock get time]}
:global f2 do={:return [/system clock get date]}

:global add1 (>[:return ([$f1]." “.[$f2])])
:global add2 (>[:return ([$f2].” ".[$f1])])

:put ($add1 <%% {$f1;$f2})
:put ($add2 <%% {$f2;$f1})

17:46:31 jun/13/2024
jun/13/2024 17:46:32


Very interesting!

:global inc (>[:set $counter ($counter+1)])
:global c {counter=0}
:put ($inc <%% $c)
:put $c

counter=1

!

I cannot believe I didn’t see this one. Despite being in the target demographic.

Great find. Some folks found the (>) “op” type a little bit ago (maybe from here?). But the <%% one is new to me.

The simplest example of your two finding (>[]) and (<%%) is likely:

:global x (>[:put "$0 $1 $2 $3 $named"])       
($x <%% {1;2;3;4;named="blah"})



1 2 3 4 blah

Or, mainly for @rextended :wink:, array order does not matter - either “unnamed” elements use $0 $1, while “named” (tuples) do not have any $ since they have $name:

($x <%% {1;named="blah";2;3;4})



1 2 3 4 blah

Same order as above…

If one wants to go futher down the rabbit hole, “Positional Arguments in Array Function - $0 vs $1?”:
http://forum.mikrotik.com/t/positional-arguments-in-array-function-0-vs-1/154024/1

Another oddity around this is any key-value array are alphabetized, IDK why but perhaps in parse the “s-expressions” (see below):

:put {z=1;m=13;a=1}



a=1;m=13;z=1

while plain lists remain ordered:

:put ("z","m","a")



z;m;a


LOL. If you know LISP, RouterOS script makes more sense: everything is a list, including code. The S-expressions from “:put” a function is kinda the big giveaway. The CLI/script are parsed into S-expr (with :global become env vars to act more shell like). If you look at /console/inspect, it forms the allowed AST to be parsed.

IMO the <%% should just work with the existing dot (.) operator, if one going down this road. They already have it, except it not allowed with functions - instead it just confuses folks when trying do string concat using . dot and an array type.


I can understand why Mikrotik hasn’t documented this, and I don’t expect them to going forward. It’s niche, and only a handful of users could even hope to find a helpful use case. I’m not even claiming I have one. But it’s another quirky tool to help write some interesting scripts, and I look forward to the possibilities.

Today, RouterOS command cannot take an array with the arguments - for a “config language” … that has all the rich list/array support… not be able to be used to set an /ip/address or something with an array of parameters is kinda ridiculous… This one has always bugged me.

i.e. you should NOT need to use [:parse] for a command to take an array, from a purist standpoint: commands should be proper function to scripting. I’ve never figured some to avoid [:parse] in these cases. If you use :parse, you stick dealing with all the escaping and lose the syntax checked, so that is less than ideal. I was kinda hoping some trick with this “<%%” might work. But I could figure out any way to do, ideally, this:

:global ipAddrSettings {numbers="*56;comment="blah"}
/ip/address/set <%% $ipAddrSettings

… but that’s not allow… and tried a few others with more wrappings/order/etc .

The lack of this make scripting a more mundane default configuration harder - since I’d like just provide an array to /ip/address/set (or whatever) & not have to deference the array to the individual commands attributes, for every command, in a defconf.

Array (see my older posts) are everytime ordered by:
first unnamed values, ordered by number
then named values, ordered by name…

so {z=1;m=13;a=1} internally are ordered {a=1;m=13;z=1}
but (“z”,“m”,“a”) are really ->0=“z”, ->1=“m” and ->2=“a”, so is this reason that are unordered.
is why
:put {“k”;“Name2”=“blah”;“~”=“last”;7=“j”;5;“z”;“name1”=“blah”;“!”=“first”}
is printed as
k;5;z;!=first;7=j;Name2=blah;name1=blah;~=last
unsorted the unkeyed values, kkeping the original order, and ascii sorted the keyed values.

Is also why if is added the 20th element, all missing values from 3 to 18 are added (empty):
:local xtest {“k”;“Name2”=“blah”;“~”=“last”;7=“j”;5;“z”;“name1”=“blah”;“!”=“first”} ; :set ($xtest->19) “tw” ; :put $xtest
result:
k;5;z;;;;;;;;;;;;;;;;;tw;!=first;7=j;Name2=blah;name1=blah;~=last



with these examples you can then do increasingly complex and intriguing things…
:global allMyFunctionsAndVariables [:toarray “”]

:set ($allMyFunctionsAndVariables->“!!help”) do={
:put “$[:typeof $0] 0=>$0<”
:put “you called allMyFunctionsAndVariables directly…”
:put “some help here…”
:return [:nothing]
}

:set ($allMyFunctionsAndVariables->“testfunc”) do={
:put “$[:typeof $0] 0=>$0<”
:put “$[:typeof $1] 1=>$1<”
:put “$[:typeof $2] 2=>$2<”
:put “$[:typeof $3] 3=>$3<”
:put “$[:typeof $4] 4=>$4<”
:put “$[:typeof $5] 5=>$5<”
:put “$[:typeof $6] 6=>$6<”
:return [:nothing]
}

:set ($allMyFunctionsAndVariables->“somenumber”) 77

:set ($allMyFunctionsAndVariables->3) “:put 999”

$allMyFunctionsAndVariables

($allMyFunctionsAndVariables->“testfunc”) 5 4 3

:put ($allMyFunctionsAndVariables->“somenumber”)

[:parse ($allMyFunctionsAndVariables->3)]

That is, you can put both data and executable code into an array. This has been known for a long time.
In fact, it turns out that everything can be housed in one structure.

We need examples of justified and effective use (>[]) and (<%%)

Better to keep it simple.
However, we are still talking about a Router,
not a programming language to develop who knows what application.

I’m interested in all the secrets of Mikrotik ! :slight_smile:

/console/inspect always tells some story about scripting. So on the (>) syntax… Mikrotik does use the LISP “quote” term…

/console/inspect request=completion input="("



TYPE COMPLETION STYLE OFFSET PREFERENCE SHOW TEXT
completion ( syntax-meta 1 75 no start of subexpression
completion $ syntax-meta 1 75 no substitution
completion [ syntax-meta 1 75 no start of command substitution
completion { syntax-meta 1 75 no start of array value

completion > syntax-meta 1 75 no > quote >
completion syntax-meta 1 75 no quote
completion none 1 -1 no literal value that consists only of digits, letters and characters ._

And on the <%% operator, it’s terms “activate in environment”:

/console/inspect request=completion input="(\$fn <"



Columns: TYPE, COMPLETION, STYLE, OFFSET, PREFERENCE, SHOW, TEXT
TYPE COMPLETION STYLE OFFSET PR SHOW TEXT
#…
completion <%% syntax-meta 5 75 no > activate in environment > >
completion ( syntax-meta 6 75 no start of subexpression
completion $ syntax-meta 6 75 no substitution
completion [ syntax-meta 6 75 no start of command substitution
completion { syntax-meta 6 75 no start of array value
completion > syntax-meta 6 75 no quote
completion syntax-meta 6 75 no quote
completion fn variable-parameter 6 96 yes

For example, chaining functions becoming possible with the “activate in environment” <%%.

:global add do={ ($1 + 1) } 
:put ($add <%% $add <%% $add <%% $add <%% $add <%% {0})
;5

Again, it be 1000% more useful if router commands themself, not just user functions, could accept an array as the arguments.


Everything is a list (or “array”), including “code”.

Here the “:put” tells another part of scripting’s story.

:global fn do={:put "$1 $2"}
:put $fn
    # output:      ;(evl (evl /putmessage=(. $1   $2)))
:put [:typeof $fn]
    # output:      array
:put [:len $fn]
    # output:      2 
:put [:typeof ($fn->1)] 
    # output:      code

And :put see the effect of (>“”) is that “code” type and printed, since the “quote” operator (>) causes the str concat operation to “passthrough” un-executed, while adding is what causes execution of a “code” type":

:put (>"text $1") 
    # output:     (. text  $1)
:put [(>"text $1")]
    # output:     text

For another example using “:put” to see what’s going on… Here there is a function, and while not used in script, the <%% does seem to be a “primative” in the code type…

:global fn do={:global fn; $fn}                      
:put $fn                       
          # output:      ;(evl (evl /globalname=$fn) (<%% $fn (> $fn)))
:global fn do={:global fn; [$fn]}                      
:put $fn                       
          # output:      ;(evl (<%% (evl (evl /globalname=$fn);(<%% $fn (> $fn))) ))

And the trilogy of “scripting secrets” lies in:

/environment/print

which shows the parsed :global variables.


There are all fun tricks… but I would not recommend using them on some scripts that “cannot fail” - since either the <%% or >[ could simply be removed in future from whatever schema shown in /console/inspect that allows them today.

I wrote a more practical example of the $(>) syntax “quote” / "op yesterday, $qkeys. This lets you assign a keypress to a command, in a menu like tree. You can check the array, $qkeymap, to be whatever commands you regular use. Then, just run “$qkeys” at CLI and have one-key press to run any command defined in the array.

Kinda like “hotkey”, but the commands are defined in an array you can change - using the “quote” (aka “op”) type.

The $qkeys function takes an array with what to do with keypress defined in ($qkeymap). It does need 7.15, but only because it uses [:convert from=byte-array] which is new (but could be replace with @rextended’s fn that does ascii code to str conversion).
:global qkeys
:global qkeysmap

:set qkeysmap {
c={“container”;{
l=(>[/log/print follow-only proplist=message where topics~“container”])
p=(>[/container/print])
e=(>[/container/env/print])
m=(>[/container/mounts/print])
}}
“4”={“ip”;{
a={“address”;(>[/ip/address/print])}
r={“route”;(>[/ip/route/print])}
f={“firewall”;{
f={“filter”;(>[/ip/firewall/filter/print])}
n={“nat”;(>[/ip/firewall/nat/print])}
m={“mangle”;(>[/ip/firewall/mangle/print])}
c={“connections”;(>[/ip/firewall/connection/print])}
}}
}}
“6”={“ipv6”;{
a={“address”;(>[/ipv6/address/print])}
r={“route”;(>[/ipv6/route/print])}
“-”={“disable”;(>[/ipv6/settings/set disable-ipv6=yes])}
“+”={“enable”;(>[/ipv6/settings/set disable-ipv6=no])}
}}
i={“interfaces”;(>[/interface/print])}
v={“vlans”;(>[/interface/vlan/print])}
b={“bridge”;{
“E”={“export”;(>[/interface/bridge/export])}
v={“vlans”;(>[/interface/bridge/vlan/print])}
p={“ports”;(>[/interface/bridge/port/print])}
}}
r={“board”;(>[/system/routerboard/print])}
l={“lte mon”;(>[/interface/lte/monitor [find]])}
e={“export”;{
f={“file”;(>[:export file=[/terminal/ask prompt=" \1B[1;33m?\1B[0m file name:"]])}
p={“print”;(>[:export])}
}}
“`”={“edit macros”;(>[{/system/script/edit qkeys source; (>[/system/script/run qkeys])}])}
“C”={“clear”;(>[:put “\r\1Bc”])}
}

:set qkeys do={
:global qkeys
:global qkeysmap
:local loop true
:local currmap $qkeysmap
:local currpath “”
:local printHeader do={
:local cmds “\1B[1;36m$currpath >\1B[0m”
:foreach k,v in=($currmap,{q={“quit”}},{“/”={“top”}}) do={
:set cmds “$cmds \1B[1;32m($[:tostr $k]) \1B[2;39m$[:tostr ($v->0)] \1B[0m”
}
:put $cmds
}
:while (loop) do={
# normalize name so all keymaps have some name
:foreach k,v in=$currmap do={
:if ([:typeof $v]=“op”) do={
:set ($currmap->$k) {“($[:pick [:tostr $v] 10 30])”;$v}
}
}

    $printHeader currmap=$currmap currpath=$currpath

    # get key
    :local kcode [/terminal/inkey]
    :local key ([:convert to=raw from=byte-array {$kcode}])

    # find in map    
    :local currval ($currmap->$key)         
    :if ([:typeof $currval]!="nil") do={
        :local currname ($currval->0)
        :local currdata ($currval->1)
        :local currtype [:typeof $currdata]
        # found array (another tree)
        :if ($currtype="array") do={
            :set currpath "$currpath \1B[1;36m> $currname\1B[0m"
            :set currmap $currdata
        }
        # found op (function) to run
        :if ($currtype="op") do={
            :put "$currpath \1B[1;31m> $currname\1B[0m"
            :local rv [$currdata]
            :put "\t# \1B[2;35m$[:pick [:tostr $rv] 0 64]\1B[0m"
        }
    } else={
        # not in map
    }
    # if no "q" in map, then assign to quit
    :if ($key~"(q|Q)") do={ :set loop false }
    # / go to top
    :if ($kcode=47) do={ :set currmap $qkeysmap; :set currpath "" }

}
:return [:nothing] 

}
If you save it as a /system/script names “qkeys”, there is even a command to edit the script using ` backtick at the top menu. This gets you syntax coloring to better spot error if changing the array. If you want the macros to persist, add a schedule script that runs qkeys script at startup.

To run it just run the qkeys scripts, then type $qkeys at the CLI.
qkeys-bridge-vlans.png
There is a related “TUI” function $INQUIRE and $CHOICES that wrote a little bit ago using “op” type here: http://forum.mikrotik.com/t/inquire-prompt-user-for-input-using-arrays-choices-qkeys/167956/1

Neat! :+1: :smiley:

I insist…
http://forum.mikrotik.com/t/mikrotik-events-script-new-abroach/176282/10

I’m fine leaving RouterOS (& what lex/yacc-like things it has to do for CLI/API) to Mikrotik. Now they should open the boot process with ONIE/GRUB/whatever - getting something like cilium/NokiaSR/OpenWRT/etc. to run on Mikrotik hardware is very hard to do today. Modifying RouterOS… I’m not sure many actually want to… Running cilium/etc on lower-priced Mikrotik hardware, that I could see more.

I would have signed the Dude open source petition long ago if were not so silly. Since my bigger grip on open-ness is the DB schema to the Dude’s SQLite database. There are a gazzon tools today that could process SQLite from Dude’s data and present in stylish/modern UI/graphs/etc, all without the Dude UI. But in usual Mikrotik efficiencies, all the fields use encoded/mangled into some shortcodes for fields/etc. Someone tried with Python a few years ago to extract some data, but they could only get a few fields.

1 Like