So my $INQUIRE function is poor-man port of Inquirer.JS for RouterOS script. The RSC $INQUIRE function code uses the nifty "inline function" syntax e.g. the "op type" (>[]) to emulate JavaScripts callbacks (see viewtopic.php?t=170591&hilit=op#p1008177)
To use the $INQUIRE function, you can create an "array-of-arrays" with the questions. Specifically the outer array is just a list of questions, with each element being an associative array with some value $INQUIRE uses to build/validate/return the prompts.
For example, if you define $myquestions like so...
example user prompts... code
:global myquestions { { text="What is your name?"; defval=""; validate="str" min=0; max=16; key="name" }; { text="What is your favorite number?"; defval="42"; validate="num"; min=0; max=100; key="favnum" }; { text="Pick a random IPv4 address..."; defval="$[:rndnum from=1 to=254].$[:rndnum from=0 to=255].$[:rndnum from=0 to=255].$[:rndnum from=0 to=255]"; validate="ip" key="rndip" } }
Assuming the $INQUIRE function and $myquestions are loaded, it works like this...
$INQUIRE can also take "callback function" to pretty print response and skip array output with "as-value"...
Code: Select all
$INQUIRE $myquestions (>[:put "$($1->"name")'s favorite number is $($1->"favnum")"]) as-value
Or using the same $myquestions from above, it can be stored as an array for later use:
Code: Select all
:global myanswers [$INQUIRE $myquestions as-value]
:put ($myanswers->"name")
More complex example... Here we ask the user to confirm (or change) the RouterOS "system identity". As shown below, the questions themselves can be provided directly on the function.
This one shows the use of an "action=" that get called after each question (if defined), with $0 being the "answer" given. Also the "validator=" is an "inline function" here, so any "custom validator" can be used, if "true" is returns it mean input was okay, otherwise a string error can be returned (which is displayed to the user & user is re-prompted the same question until input is valid).
Code: Select all
$INQUIRE ({
{ text="Router name:";
defval=(>[:return "$[/system/identity/get name]"]);
validate=(>[:if ([:tostr $0] ~ "^[a-zA-Z0-9_\\-]*\$" ) do={:return true} else={:return "invalid name"}]);
action=(>[/system/identity/set name=$0]);
key="sysid"
}}) (>[:put "New system name is: $($1->"sysid")"]) as-value
Anyway, enough examples. The needed code below can be loaded by cut-and-paste initially, or via /system/script. Once loaded, you can use $INQUIRE to invoke with an array defined like the "myquestions" above...
INQUIRE function code
# $INQUIRE - prompt for values using array with questions # usage: # $INQUIRE <question_array> [<callback_function>] [as-value] # returns: associative array with key= as the index, and answer as value # param - <question_array>: index array, containing one or more associative arrays e.g. { # { text=<str>; #question to ask # [key=<str>]; #key in returned $answer # [defval=<str|num|op|function>]; #default value, default is "" # [action=<op|function>]; #function to call after validated input # [validate=<op|function|"str"|"num"|"ip">]; #perform validation # [min=<num>]; #min num or string length # [max=<num>] #max num or string length # } # } # param - <callback_function>: called after ALL questions have been asked # with $1 arg to function containing all answers/same as return # param - as-value: if not provided, the answers will be output in as an array string :global INQUIRE do={ # store questions/prompts as $qr :local qr $1 # variable to store answers to return :local answers [:toarray ""] # use array to map "ASCII code" (e.g. num type) to a "char" (e.g. str type of len=1) :local "asciimap" { "\00";"\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"; "\20";"\21";"\22";"\23";"\24";"\25";"\26";"\27";"\28";"\29";"\2A";"\2B";"\2C";"\2D";"\2E";"\2F"; "\30";"\31";"\32";"\33";"\34";"\35";"\36";"\37";"\38";"\39";"\3A";"\3B";"\3C";"\3D";"\3E";"\3F"; "\40";"\41";"\42";"\43";"\44";"\45";"\46";"\47";"\48";"\49";"\4A";"\4B";"\4C";"\4D";"\4E";"\4F"; "\50";"\51";"\52";"\53";"\54";"\55";"\56";"\57";"\58";"\59";"\5A";"\5B";"\5C";"\5D";"\5E";"\5F"; "\60";"\61";"\62";"\63";"\64";"\65";"\66";"\67";"\68";"\69";"\6A";"\6B";"\6C";"\6D";"\6E";"\6F"; "\70";"\71";"\72";"\73";"\74";"\75";"\76";"\77";"\78";"\79";"\7A";"\7B";"\7C";"\7D";"\7E";"\7F"; "\80";"\81";"\82";"\83";"\84";"\85";"\86";"\87";"\88";"\89";"\8A";"\8B";"\8C";"\8D";"\8E";"\8F"; "\90";"\91";"\92";"\93";"\94";"\95";"\96";"\97";"\98";"\99";"\9A";"\9B";"\9C";"\9D";"\9E";"\9F"; "\A0";"\A1";"\A2";"\A3";"\A4";"\A5";"\A6";"\A7";"\A8";"\A9";"\AA";"\AB";"\AC";"\AD";"\AE";"\AF"; "\B0";"\B1";"\B2";"\B3";"\B4";"\B5";"\B6";"\B7";"\B8";"\B9";"\BA";"\BB";"\BC";"\BD";"\BE";"\BF"; "\C0";"\C1";"\C2";"\C3";"\C4";"\C5";"\C6";"\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" } # some ANSI tricks are used in output to format input and errors :local "ansi-bright-blue" "\1B[94m" :local "ansi-reset" "\1B[0m" :local "ansi-dim-start" "\1B[2m" :local "ansi-dim-end" "\1B[22m" :local "ansi-clear-to-end" "\1B[0K" # main loop - ask each question provided in the $1/$qr array :for iq from=0 to=([:len $qr]-1) do={ # define the current answer and use "defval" to populate :local ans ($qr->$iq->"defval") # if "defval" is inline function, call it to get default value :if ([:typeof $ans] ~ "op|array") do={ :set ans [$ans ($qr->$iq)] } # ask the question, using an default in $ans :put " $($qr->$iq->"text") $($"ansi-bright-blue") $ans $($"ansi-reset") " # last char code received :local kin 0 # keep looking for input from terminal while $inputmode = true :local inputmode true :while ($inputmode) do={ # re-use same terminal line /terminal cuu # get keyboard input, one char :set kin [/terminal/inkey] # if NOT enter/return key, add char to the current answer in $ans :if ($kin != 0x0D) do={ # use ascii map to convert num to str/"char" :set ans "$ans$($asciimap->$kin)" } else={ # got enter/return, stop input :set inputmode false } # if backspace/delete, remove the control code & last char :if ($kin = 0x08 || $kin =0x7F) do={ :set ans [:pick $ans 0 ([:len $ans]-2)] } # assume input is valud :local isvalid true :local errortext "" # unless validate= is defined... # if validate=(>[]) is inline function :if ([:typeof ($qr->$iq->"validate")] ~ "op|array") do={ # call question's validator function :set isvalid [($qr->$iq->"validate") $ans] } # if validate="num", make sure it a num type :if (($qr->$iq->"validate") = "num") do={ # see if casting to num is num :if ([:typeof [:tonum $ans]] = "num") do={ # store as num type :set ans [:tonum $ans] # valid so far :set isvalid true # if a min= is defined, check it :if ([:typeof ($qr->$iq->"min")] = "num") do={ :if ($ans>($qr->$iq->"min")) do={ :set isvalid true } else={ :set isvalid "too small, must be > $($qr->$iq->"min") " } } # if a max= is defined, check it :if ([:typeof ($qr->$iq->"max")] = "num") do={ # if already invalid, use that text first e.g. too small :if ($isvalid = true) do={ :if ($ans<($qr->$iq->"max") && isvalid = true) do={ :set isvalid true } else={ :set isvalid "too big, must be < $($qr->$iq->"max") " } } } } else={ :set isvalid "must be a number" } } # if there is min= or max= but no validate=, assume validate str lengths :if ([:typeof ($qr->$iq->"validate")] ~ "nil|nothing") do={ :if (([:typeof ($qr->$iq->"min")] = "num") || ([:typeof ($qr->$iq->"max")] = "num")) do={ :set ($qr->$iq->"validate") "str" } } # if validate="str", make sure it's a str type :if (($qr->$iq->"validate") = "str") do={ :if ([:typeof [:tostr $ans]] = "str") do={ # save answer as str :set ans [:tostr $ans] :set isvalid true # if min=, check length in range :if ([:typeof ($qr->$iq->"min")] = "num") do={ :if ([:len $ans]>($qr->$iq->"min")) do={ :set isvalid true } else={ :set isvalid "too short, must be > $($qr->$iq->"min") " } } # if max=, check length in range :if ([:typeof ($qr->$iq->"max")] = "num") do={ :if ($isvalid = true) do={ :if ([:len $ans]<($qr->$iq->"max")) do={ :set isvalid true } else={ :set isvalid "too long, must be < $($qr->$iq->"max") " } } } } else={ :set isvalid "must be a string" } } # if validate="ip", make sure it valid IP address # note: IPv6 is not supported :if (($qr->$iq->"validate") = "ip") do={ # make sure it's num.num.num.num BEFORE using :toip to avoid .0 being appended :if ($ans ~ "^[0-9]+[\\.][0-9]+[\\.][0-9]+[\\.][0-9]+\$") do={ # then check it parsable using :toip :if ([:typeof [:toip $ans]] = "ip") do={ :set ans [:toip $ans] :set isvalid true } else={ :set isvalid "IP address not valid" } } else={ :set isvalid "bad IP address format, must be x.y.z.a" } } # if answer is valid, store it in the $answers array :if ($isvalid = true) do={ # if a key="mykeyname" is used, that becomes the key in array map :if ([:typeof ($qr->$iq->"key")] = "str") do={ :set ($answers->"$($qr->$iq->"key")") $ans } else={ # otherwise the key in returned array map is "promptN" where N is index :set ($answers->"prompt$iq") $ans } :set errortext "" } else={ # if no valid... report the error, and continue input mode :set errortext $isvalid :set inputmode true } # finally output the question, using ANSI formatting :put " $($qr->$iq->"text") $($"ansi-bright-blue") $ans $($"ansi-reset") $($"ansi-dim-start") $errortext $($"ansi-dim-end") $($"ansi-clear-to-end")" # if action= is defined & validated - call the action :if ($kin = 0x0D && isvalid = true) do={ :if ([:typeof ($qr->$iq->"action")] ~ "op|array") do={ [($qr->$iq->"action") $ans] } } } } # end of questions # if 2nd arg is a function or "op" (e.g. inline function), call that with the $answers :if ([:typeof $2] ~ "op|array") do={ [$2 $answers] } # if 2nd or 3rd arg is "as-value", do not print the results to terminal :if (($2 = "as-value") || ($3 = "as-value")) do={ :return $answers } else={ :put $answers :return $answers } }
I was more trying to play around with the inline functions $(>[]), so maybe bugs... But comment below anyone uses this and has issues.
Known Issues
- using "arrow keys" to move inline does NOT work
- ideally there be some "choice" type like the original Inquirer.JS code
- no support for IPv6 or "bool" types
- cursor shows on line below, although edit happens on line above
TODO's
- rename defval= to just val= to match $CHOICE array
- if validate/defval/action/when are fns, provide $answers and $questions as args
- add "type=" e.g. support non-text inputs like $CHOICE
- validate based type= automatically to avoid needing validate=
- add new "when=<function>|<bool>", default true but if false question skips question
- add type="choice" as type & add "choices=" as optional
- type=choice should support a "multiselect=yes" to allow multiple val= as array in answers
- keyboard shortcuts for type=choices
- add type=ipprefix
- add type=ip6
- add type=confirm + confirm=yesno|bool|num for a simple yes/no
- add type=password to mask password input
- add type=seperator to insert blank or text= (from inquirer.js)
- support a type="goto" and goto=<num-index>|<str=key> e.g. with when= also for a conditional jump