Feature Request: Expanding the usage of 'in' operator in RouterOS Scripting

I am writing to suggest an enhancement to the RouterOS scripting language. Currently, the in operator is limited to checking if an IP address belongs to a specific subnet: (:put (10.0.0.1 in 10.0.0.0/24))

While this is useful, expanding the scope of in would significantly improve script readability and performance. I propose making in a universal membership operator, similar to modern programming languages.

Proposed Use Cases:

1. Array (List) Membership Instead of writing a loop to check if a value exists in an array, we could use:

:local myList {"admin"; "operator"; "guest"}
:if ("admin" in $myList) do={ ... }

Current workaround: Requires a :foreach loop with a flag variable, which is inefficient.

2. String Substring Search Checking if a string contains a specific word or character:

:local interfaceName "ether1-wan"
:if ("wan" in $interfaceName) do={ ... }

Current workaround: Using :find and checking if it's not nil.

Why this matters:

  • Cleaner Code: Reduces the number of lines and complexity.

  • Performance: Built-in operators are generally faster than scripted loops.

  • Modernization: Aligns RouterOS scripting with modern syntax standards (like Python or JavaScript).

1 Like

False.

You're right that it's inefficient, also because it's the wrong method...

EDIT: added complex array example


No, is not a workaround, correct thing to use is find.
Also with "in" you must check if "in" is true or false...

And let's not forget the simplest one:

:local interfaceName "ether1-wan"; :if ($interfaceName~"wan") do={:put "yes, containing wan"}
1 Like

I know all this, and I have my own solutions regarding workarounds.

This is a function I created to reduce the number of if statements I use while writing code. It also solved my problem of finding an element within 1D or 2D arrays, because unfortunately, the methods for finding an element are not the same in 1D or 2D arrays.

:global in do={

    # Syntax

#   $in <list=array> <key=str>

    :local "input_debug" $debug; :local debug $"input_debug";

    :if ($debug =1) do={:set debug true;} else={:set debug false;};

    :if ($debug) do={

        :put ("list : \"" . [:typeof $list] . "\" [" . [:len $list] . "]");

        :put ("key: \"" . [:typeof $key] . "\" [" . [:len $key] . "]");

    };

    :if ((([:typeof $list] ~"nothing|nil") || ([:len $list] <1) || ($list ="")) \

    || (([:typeof $key] ~"nothing|nil") || ([:len $key] <1) || ($key =""))) do={

        :error ("\ninvalid inputs\r\

\n\$in <list=array> <key={str|num}>"\

        );

    };

    :return (([:typeof [:find in=$list key=$key]] ="num") || ([:typeof ($list -> [:tostr $key])] !="nothing"));

};

So if you knew, why did you purposely write it more complex than it should have been?

I get it, you usually make things up in a more crappy way to try to convince someone else that something's wrong, until you find someone who knows what's really going on and writes the truth.

Nice way.

I'm also looking for clean, simple, and easy-to-write code.

And you know how other languages ​​use the same applications for this keyword?

I don't believe it, you already know it's used by the system, and you create an object with the same name.

If this mirrors how you program in other languages...
AI aside... obvious, right?



    :local "input_debug" $debug; :local debug $"input_debug";

    :if ($debug =1) do={:set debug true;} else={:set debug false;};

    :if ($debug) do={

All these lines to make the effect of just one?

    :if ($debug="1") do={

Over and out.

I don't think this is worth to be added as script language comparison operator. It would be nice to have a function right out of the box. like in_array (haystack, needle) or something.

I am 50:50 about adding an in operator. The idiomatic way on RouterOS seems to be find, even configuration export generates find statements.

I must have missed it in the documentation – it seems like it does not specify what find returns if it does not find a matching result. For example, what is the return value of the following:

:local myList {"admin"; "operator"; "guest"}; :put [:find $myList "no match" -1]}

And what does it cost you to try it? So you know...

spolier

nil

Well, I have had a feature request ticket open on this very topic, SUP-208071:

Scripting - in should support array types to test existence in a list

First, thank you for all the recent connivance methods in recent RouterOS script. But one that remains annoying is checking if something is an element in a list-type array. While you can use :find to get an index of element by searching an array, it not that convenient to use to check existance in the array. Specifically it can be either nil or num type, so the comparisons get messy - especially you only need a boolean existence test, not the position.
My thought is the in could be extended to support the raw type on left-hand and the list-type array on right side which return a boolean. Currently in only supports ip types, but the syntax lend itself to array types IMO. For example,

:local evens (2,4,6,8,10)
:if ([:rndnum from=0 to=10] in $evens) do={
  :put "random number is even"
} else={
  :put "random number is odd"
}

Today, to do same, it’s way more contrived:

:local evens (2,4,6,8,10)
:if ([:find $evens [:rndnum from=0 to=10]] > -1) do={
  :put "random number is even"
} else={
  :put "random number is odd"
}

Obviously not a huge deal, since it’s possible and one could wrap it in a function…. But since y’all be cleaning up things, I run into needing some “is member of” for array types.

The response was the classic:

Thank you for your request. We will consider this for future implementation.

On your second one, IDK, the problem is a string is not an array... And you can use [:convert to=byte-array] to get a string as an array. So using something to get you array of chars from string be the indirect way to use "in for arrays", since in clouds that the string operators are just poor everywhere, so "in" does not buy you as much as some :regex or :sed or whatever to parse/change strings.

"~" exist...

:if ($interfaceName~"wan") do={

so :if ("wan" in $interfaceName) do={ is useless...

In fact my point was more on string the issue is not a find (or in sugar) is that you cannot do some a search-and-replace, easily. And, use the fuller set of regex like grouping, and/or regex's replace operators. That bigger issues on the string side, IMO. But the in for array at least align it with one-off esoteric case for ip subnets.

So basically +1 on #1, "dont care" on #2 in this thread.

@rextended, I guess I don't see improving the scripting ergonomics is bad, when it does not break older script... And you ignore more complex expression required to handling multiple matcher.

1 Like
:local myList {"admin"; "operator"; "guest"};  :put ($myList~"(^|;)admin(\$|;)")

:squinting_face_with_tongue:

1 Like

Better more complex example:

(y)
4 Y X W V Uniform
3 P O N M Tango
2 I H G L S
1 D Charlie Foxtrot K R
0 Alpha B E J Q
0 1 2 3 4 (x)

and on same array "andThis=isThis"

{
    :local test {{"Alpha";"D";"I";"P";"Y"};{"B";"Charlie";"H";"O";"X"};{"E";"Foxtrot";"G";"N";"W"};{"J";"K";"L";"M";"V"};{"Q";"R";"S";"Test";"Uniform"}}
    :set ($test->"andThis") "isThis"
    :put [:tostr $test]
    :put ($test~"(^|;)Alpha(\$|;)")
    :put ($test~"(^|;)Charlie(\$|;)")
    :put ($test~"(^|;)Uniform(\$|;)")
    :put ($test~"(^|;)Delta(\$|;)")
    :put ($test~"(^|;)andThis=")             ; # check if exhist the andThis property (no matter the value)
    :put ($test~"=isThis(\$|;)")             ; # check if exhist something with isThis value
    :put ($test~"(^|;)andThis=isThis(\$|;)") ; # check if exhist the andThis property with exactly isThis value
}

Nothing. I did and that's why I mentioned that one particular case.

:local myList {"admin"; "operator"; "guest"}
:if ([:find $myList "no match" -1] = nil) do={
	:put "Oh no!"
} else={
    :put "Match found"
}

Output:

Match found

This should work:

:local myList {"admin"; "operator"; "guest"}
:if ([:find $myList "no match" -1] >= 0) do={
    :put "Match found"
} else={
    :put "Oh no!"
}

Or if you prefer your if-else order:

:local myList {"admin"; "operator"; "guest"}
:if ([:find $myList "no match" -1] < 0) do={
	:put "Oh no!"
} else={
    :put "Match found"
}

One-liner for quick tests on console:

:local myList {"admin"; "operator"; "guest"}; :put ([:find $myList "no match" -1] >= 0)

Yes, it does work.

Last time I looked, that was not properly documented. I don't believe that the documentation even specifies that :find returns nil when nothing is found. There are positive examples where a match is found but none that properly explains the case where there is no match. There is only a comparison that hints at :nothing comparing to less than zero.

I wish the scripting pages where on Github where you could create pull requests with improvements…

Written this way, it's just an object that doesn't exist, internally converted to anonymous string...

Something else was meant...

:local myList {"admin"; "operator"; "guest"}
:if ([:find $myList "operator" -1] = [:nothing] ) do={
	:put "Oh no!"
} else={
    :put "Match found"
}

# OR

:local myList {"admin"; "operator"; "guest"}
:if ([:typeof [:find $myList "no match" -1]] = "nil") do={
	:put "Oh no!"
} else={
    :put "Match found"
}

But it's just the wrong method...
It depends on what you want to do...

{
    :local myList {"admin"; "operator"; "guest"}

    :if ($myList~"(^|;)operator(^|;)") do={
        :put "found"
    } else={
        :put "NOT found"
    }

    :if ($myList~"(^|;)test(^|;)") do={
        :put "found"
    } else={
        :put "NOT found"
    }
}

The examples here kinda validate that some in <array> be useful... :wink:

The OP correctly points out that a foreach also works, which avoids the complexities in find or regex, it just requires an extra variable.

I'll just note with newer V7 there is break... so it's bit less inefficient if you use that.

Actually no, because if it were to be CORRECTLY implemented it would be

"RegEx" in $array

so... what change between "in" and already existant "~"?

{
    :local myList {"admin"; "operator"; "guest"}

    :if ("(^|;)operator(^|;)" in $myList) do={
        :put "found"
    } else={
        :put "NOT found"
    }

    :if ("(^|;)test(^|;)" in $myList) do={
        :put "found"
    } else={
        :put "NOT found"
    }
}

How would you choose whether "in" should search only for parts, only for exact matches, or only for RegEx?

"a" in $myList = true or 2 ?
"ad" in $myList = true or 1 ?
"admin" in $myList = true or 1 ?
"notadmin" in $myList = false or 0 ?

and if not accept RegEx two tests at same time are not possible like "(admin|operator)" in $myList


Rather, it would be more useful if they fully implemented RegEx syntax allowing for case-insensitive searches and other cute RegEx syntax...