Scripting Tips and Tricks

本页提供 MikroTik RouterOS 的脚本技巧,强调使用内部 ID 而不是字面值的重要性,以避免在对象重新编号时出错。它解释了如何通过 find 等命令和类型转换正确引用接口路由、IP 地址和其他网络元素。

脚本技巧与窍门

让我们从基础开始。在终端中工作时,你习惯使用以下语法访问参数:

[admin@r1] /interface> print
Flags: D - dynamic, X - disabled, R - running, S - slave
 #     NAME                                TYPE       ACTUAL-MTU L2MTU  MAX-L2MTU
 0  R  ether1                              ether            1500  1580      1022
[admin@r1] /interface> set 0 name=LAN

print 命令会临时保存带有 id 编号的缓冲区,这些编号引用内部 ID 编号,因此很明显,如果你尝试使用不存在的值,脚本将会失败,例如在此脚本中:

/system script add name=script1 source={
  /ip route set 1 gateway=3.3.3.3
}

脚本不知道 "1" 指的是什么,因此会抛出错误。正确的方式是使用内部 ID 编号,这些编号可以在执行 print as-value 时看到,或由 find 命令返回,例如:

[admin@r1] /ip route> :put [find where dst-address="10.0.0.0/8"]
*1

因此在这种情况下,正确的脚本应为:

/system script add name=script1 source={
  /ip route set *1 gateway=3.3.3.3
}

注意,不建议直接使用内部编号,因为项目可能被删除并重新添加;此时内部 ID 会改变,脚本将失败。请改在代码中使用 find 命令:

/system script add name=script1 source={
  /ip route set [find dst-address="0.0.0.0/0"] gateway=3.3.3.3
}

为什么即使指定了正确的值 find 也不工作?

假设我们想打印一个特定地址:

[admin@r1] /ip address> print where address=111.111.1.1/24
Flags: X - disabled, I - invalid, D - dynamic
 #   ADDRESS            NETWORK         INTERFACE

我们知道这样的地址存在,那为什么没有输出呢?

因为脚本引擎会尽可能积极地进行变量类型转换,但它并不总是成功。让我们仔细看看这个特定示例为何失败。首先,检查 address 是什么变量类型:

[admin@r1] /ip address> :put [:typeof ([print as-value]->0->"address")]
str

所以很明显,我们是在将 string 与 ip-prefix 进行比较,而从 ip-prefix 到 string 的转换不会自动发生。要解决此问题,我们需要将变量转换为正确的格式:

[admin@r1] /ip address> print where address=[:tostr 111.111.1.1/24]

Flags: X - disabled, I - invalid, D - dynamic
 #   ADDRESS            NETWORK         INTERFACE
 0   111.111.1.1/24     111.111.1.0     ether2

或者通过将查询值放在双引号中直接使用字符串:

[admin@r1] /ip address> print where address="111.111.1.1/24"

Flags: X - disabled, I - invalid, D - dynamic
 #   ADDRESS            NETWORK         INTERFACE
 0   111.111.1.1/24     111.111.1.0     ether2

显然,如果你从变量获取 IP 前缀,第二种方法就不合适;在这种情况下,请按第一个示例所示进行转换,或使用 "$myVar" 将变量写入字符串。

如何定义空数组

RouterOS 不允许以你认为应该的方式定义空数组:

[admin@1p_DUT_wAP ac] /interface> :global array {}
syntax error (line 1 column 17)

变通方法是将空字符串转换为数组:

[admin@r1] > :global array [:toarray ""]
[admin@r1] > :environment print
array={}

从这里开始,我们可以使用此数组来设置元素:

[admin@r1] > :set ($array->"el0") "el0_val"
[admin@r1] > :environment print
array={el0="el0_val"}

如何删除变量

你可以使用 /system script environment remove 删除未使用的变量;不过,首选方法是取消设置该变量。

为现有参数不设置值会取消设置它;例如:

[admin@MikroTik] /system script environment> :global myVar 1
[admin@MikroTik] /system script environment> print
 # NAME               VALUE
 0 myVar              1
[admin@MikroTik] /system script environment> :set myVar
[admin@MikroTik] /system script environment> print
 # NAME               VALUE
[admin@MikroTik] /system script environment>

当 'get' 命令不可用时获取属性值

例如,要从 /interface wireless info hw-info 命令获取可用于脚本的输出,请使用 as-value 参数:

[admin@1p_DUT_wAP ac] /interface wireless info> :put [hw-info wlan1 as-value ]
ranges=2312-2732/5/b;g;gn20;gn40;2484-2484/5/b;g;gn20;gn40;rx-chains=0;1;tx-chains=0;1

输出是一维数组,因此你可以轻松获取你感兴趣的属性值。

[admin@1p_DUT_wAP ac] /interface wireless info> :put ([hw-info wlan1 as-value ]->"tx-chains")
0;1

始终检查命令返回的值和类型

假设我们尝试使用 as-value 获取特定路由的 gateway,但脚本没有返回任何内容:

[admin@r1] /ip address> :put ([/ip route print as-value where gateway="ether1"]->"gateway")

首先检查 print 是否真的找到了任何内容:

[admin@r1] /ip address> :put ([/ip route print as-value where gateway="ether1"])

.id=*400ae12f;distance=255;dst-address=111.111.111.1/32;gateway=ether1;pref-src=111.111.111.1

该命令假定输出将是一个一维数组,我们可以从中提取名为 gateway 的元素。显然,变量或其类型有问题。让我们更仔细地检查一下:

[admin@r1] /ip address> :global aa ([/ip route print as-value where gateway="ether1"])
[admin@r1] /ip address> :environment print

aa={{.id=*400ae12f; distance=255; dst-address=111.111.111.1/32; gateway={"ether1"}; pref-src=111.11
1.111.1}}

现在清楚了,返回的值是包含一个元素的二维数组。因此提取 gateway 的正确顺序应该是:

  • 获取二维数组
  • 获取第一个元素
  • 从选取的元素中获取 gateway
[admin@r1] /ip address> :put ([:pick [/ip route print as-value where gateway="ether1"] 0]->"gateway")

ether1

将数组添加到字符串时要小心

如果你打印数组或将其追加到现有字符串,请务必小心,因为结果可能出乎意料。例如,使用包含两个元素的数组:

[admin@1p_DUT_wAP ac] /> :global array {"cccc", "ddddd"}
[admin@1p_DUT_wAP ac] /> :put ("array value is: " . $array )

array value is: cccc;array value is: ddddd

连接运算符(.)会将字符串添加到每个数组元素,然后打印输出。要解决此问题,我们需要先转换为字符串:

[admin@1p_DUT_wAP ac] /> :put ("array value is: " . [:tostr  $array] )

array value is: cccc;ddddd

获取/设置数组中的未命名元素

假设我们有一个包含三个元素的数组 { "el1"; "el2"; "el3" }。你可以使用 pick 命令选取元素,但更好的方式是通过索引直接访问元素:

[admin@1p_DUT_wAP ac] /> :global test { "el1"; "el2"; "el3" }
[admin@1p_DUT_wAP ac] /> :put ($test->1)

el2

同样的语法可用于设置值:

[admin@1p_DUT_wAP ac] /> :set ($test->2) "el3_changed"
[admin@1p_DUT_wAP ac] /> :environment print

test={"el1"; "el2"; "el3_changed"}

在二维数组中设置元素值

你也可以在二维数组中通过索引直接设置值:

[admin@1p_DUT_wAP ac] /> :global test {{"11";"12";"13"};{"21";"22";"23"}}
[admin@1p_DUT_wAP ac] > :set ($test->1->1) "22_changed"
[admin@1p_DUT_wAP ac] > :put [($test->1->1)]
22_changed
[admin@1p_DUT_wAP ac] > :environment print
test={{"11"; "12"; "13"}; {"21"; "22_changed"; "23"}}

读取在另一个脚本中定义的全局变量的值

假设我们有一个脚本声明了一个变量并设置其值:

/system script add name=script1 source={
  :global myVar "hello!"
}

如果我们想从另一个脚本将该变量的值写入日志,添加 /log info $myVar 将无法返回正确的值,因为第二个脚本对另一个脚本中定义的变量一无所知。要使其正常工作,需要定义该变量:

/system script add name=script2 source={
  :global myVar;
  :log info "value is: $myVar"
}

从函数访问全局变量

从逻辑上讲,你可能认为全局定义的变量应该可以在函数中访问,但事实并非如此。让我们看一个示例:

:global myVar "test"
:global myFunc do={
  :put "global var=$myVar"
}
[$myFunc]

输出是:

global var=

输出为空,因为无法直接访问全局变量。要使其正常工作,我们需要在函数内部声明全局变量:

:global myVar "test"
:global myFunc do={
  :global myVar;
  :put "global var=$myVar"
}
[$myFunc]

输出:

global var=test

从另一个函数运行函数

如果你想从另一个函数运行一个函数,那么它需要被声明。

:global test do={
  :return ($1 + 1)
}

:global testtest do={
  :local x 5
  :local y [$test $x]
  :put "typeof = $[:typeof $y]"
  :put "testets_res=$y"
}

上面的代码不会按预期工作,输出将是:

typeof = nil
testets_res=

要解决此问题,我们需要在 "testtest" 函数中声明全局的 "test":

:global testtest do={
  :global test
  :local x 5
  :local y [$test $x]
  :put "typeof = $[:typeof $y]"
  :put "testets_res=$y"
}

始终使用唯一的变量名

最常见的错误之一是使用不唯一的变量名。例如,在函数中定义的变量可能与全局变量同名,从而导致意外结果:

: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

另一个常见问题是用户定义的变量与 RouterOS 内置属性同名。例如,打印一条 dst-address 在变量中定义的路由:

[admin@1p_DUT_wAP ac] /ip route> :global "dst-address" "0.0.0.0/0"
[admin@1p_DUT_wAP ac] /ip route> print where dst-address=$"dst-address"
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, b - bgp, o - ospf, m - mme,
B - blackhole, U - unreachable, P - prohibit
 #      DST-ADDRESS        PREF-SRC        GATEWAY            DISTANCE
 0 ADS  0.0.0.0/0                          10.155.136.1              1
 1 ADC  10.155.136.0/24    10.155.136.41   ether1                    0

显然结果不符合预期,简单的解决办法是使用唯一的变量名:

[admin@1p_DUT_wAP ac] /ip route> :global myDst "0.0.0.0/0"
[admin@1p_DUT_wAP ac] /ip route> print where dst-address=$myDst
Flags: X - disabled, A - active, D - dynamic, C - connect, S - static, r - rip, b - bgp, o - ospf, m - mme,
B - blackhole, U - unreachable, P - prohibit
 #      DST-ADDRESS        PREF-SRC        GATEWAY            DISTANCE
 0 ADS  0.0.0.0/0                          10.155.136.1              1

从 "monitor" 等循环交互式命令获取值

一个常见问题是如何在脚本中获取 monitor 等交互式命令返回的值。第一个问题是这些命令会无限期运行,直到用户干预,而脚本无法做到这一点。请改用 once 参数运行它们,该参数会执行命令一次然后停止。另一个问题是检索返回的变量:没有 as-value,也没有 get,但有 do。它允许你访问命令返回的变量。例如:

[admin@1p_DUT_wAP ac] /interface> monitor-traffic ether1 once do={:global myBps $"rx-bits-per-second" }
...
[admin@1p_DUT_wAP ac] /interface> :environment print

myBps=71464

获取 fetch 工具接收到的文件内容

fetch 工具可以轻松地将文件内容下载到内存中,并从脚本访问它。为此,请使用 as-value 参数并配合 output=user:

[admin@rack1_b34_CCR1036] > :put ([/tool fetch ftp://admin:@10.155.136.41/test.txt
 output=user as-value ]->"data")

my file content

检查脚本权限

假设我们有一个创建文件并向其写入内容的脚本:

/system script add name=script1 policy=ftp,read,write source={
        /file print file=test;
        /file set test.txt contents="my content"
}

现在添加一个将尝试执行此脚本的调度器:

/system scheduler
add interval=10s name=test on-event=script1 policy=read,write

等待 10 秒后,我们可以看到文件没有创建。如果你仔细观察,脚本需要 "ftp" 策略才能创建文件,但调度器只有 "read" 和 "write" 策略。要解决此问题,请将调度器设置为使用正确的策略 "read,write,ftp" 运行。

当你从 netwatch、ppp on-event 和类似钩子运行脚本时也是如此,它们仅限于策略 read,write,test,reboot。在这种情况下,你无法执行创建备份或文件的高级脚本。

你可以使用 dont-require-permissions 来修复此限制,但向脚本授予不受限制的权限时请务必小心。

使用 dont-require-permissions 时要小心

可以使用 dont-require-permissions 参数设置脚本。基本上,它允许任何没有足够权限的人执行该脚本。例如,如果脚本具有策略 "read,write,test,sensitive",但执行该脚本的用户或应用权限较少(例如只有 "read,write"),那么通过设置 dont-require-permissions=yes,我们仍将允许运行该脚本。

这可能允许使用脚本更改敏感信息,即使执行该脚本的用户没有足够的权限。