$INQUIRE - prompt user for input using arrays +$CHOICES +$QKEYS

There is a node.js/JavaScript project called “inquirer.js” that takes some JSON with some “questions”/“prompts”, and returns the calling JavaScript an JS object with all the answers. It’s pretty handy for getting user input from the terminal. See https://github.com/SBoudrias/Inquirer.js.

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 http://forum.mikrotik.com/t/persistent-environment-variables/145326/1)

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…
: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”…

$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:

: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).

$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

It’s important to understand that action=, defval= and validate= are “inline functions” (>) which are called dynamically during user prompting. So the default value is obtained each time $INQUIRE is called, not just when the “questions array” is defined.


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 - 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=; #question to ask

[key=]; #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=]; #min num or string length

[max=] #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=|”, 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=|<str=key> e.g. with when= also for a conditional jump

Bravo!!! Ti ci sei fatto male :laughing:

http://forum.mikrotik.com/t/can-i-make-interactive-script/104361/1 :laughing: :laughing: :laughing:


I see the “GIF”, not tested, ty to make the input at the same line, is more readable…
http://forum.mikrotik.com/t/can-i-make-interactive-script/104361/1

The “do=[:return]” trick to collect input works … just hated the ugly “value:” prompt it uses – but yeah it’s dozens of lines of script to avoid that :wink:. But the instant feedback on bad input is pretty nifty, I thought at least.

In fairness, 90% of the code is dealing with types and validation. And nothing with script var types is easy.

Very cool !
Practically a preceptron of a neural network. A little more and there will be artificial intelligence on Mikrotik. :smiley:

If you look at the input screen as a CRT TV then you can redrawn the page with the lines and entered data once on confirmed line, or even every key. Then you can correct previous entered data by using the cursor buttons by using the [:te cuu] for example to go up after redrawn page.

The page with lines and data is then like a canvas, and you move from field to field by the cursor keys and then edit. An other advantage is that see all the questions up front and then start with answering the first one.

By the way, what is the /terminal cu command and when did it appear?

Also, the Amm0 script is good as an example of using (>). Question to the author: now what do you think when in practice it is appropriate to use the new data type op (>[]) that you explored?

Press TAB for options or F1 for help:

@ > [:terminal/

.. -- go up to root
cuu -- move cursor up
el -- erase line
inkey -- read key
style -- set output text style

Small menu example with active help and (audio) feedback on error:

{
:local readKeyString do={
# written by msatter 2020-2021
# keyFlag show if the input was caused by a key pressing, firstDisplay is showing if the input was pre-provided by $4 
# Default characters allowed to use.
:set $errorName "characters"; #default error name.
:set $sound "\a"; # Warning sound. Put a "#" in front to silence it.
:set $upKey  false; # set $upKey to false to not indicate default a manual change of a earlier entry. 
:set $endKey false; # set $endKey to false
 :set $ASCI " !\" \$ &'()*+,-./0123456789:;<=>\?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}"; # allowed characters to be typed.
 :set $control [:pick $2 ([:find $2 ":"]) [:len $2]]; :set $2 [:pick $2 0 ([:find $2 ":"]+1)]; # Fill $control with the provided values in $2
 :if [:find $control *] do={:set $hide true;:set $unHide false} else={:set $hide false}
 :if [:find $control #] do={:set $allowedASCI "0123456789";:set $controlASCI "#";:set $errorName "numbers"};	# numbers allowed.
 :if [:find $control @] do={:set $allowedASCI "+-.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";:set $controlASCI "@";:set $endCheck true;:set $endCheckRegex "^[a-z0-9.+_-]+\\@[a-z]+\\.[a-z]{2,7}\$"}; # e-mail address.
 :if [:find $control /] do={:set $allowedASCI "!\$&()*+,-./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";:set $controlASCI "/";:set $endCheck true;:set $endCheckRegex "[\\.|^][a-z0-9._-]+\\.[a-z]{2,7}[/]{0,1}[a-zA-Z0-9\$_.+!*(),-]*"}; # web address.
 :if [:find $control I] do={:set $allowedASCI "/.0123456789";:set $controlASCI "I";:set $endCheck true;:set $errorName "IPv4 address";
 :set $endCheckRegex "^(25[0-5]|2[0-4][0-9]|[01][0-9][0-9]|[0-9][0-9]|[[0-9])\\.(25[0-5]|2[0-4][0-9]|[01][0-9][0-9]|[0-9][0-9]|[[0-9])\\.(25[0-5]|2[0-4][0-9]|[01][0-9][0-9]|[0-9][0-9]|[[0-9])\\.(25[0-5]|2[0-4][0-9]|[01][0-9][0-9]|[0-9][0-9]|[[0-9])(/(3[0-2]|2[0-9]|[1][0-9]|[0-9]))\?\$"}; # numbers and special signs allowed.
     
 :if [:find $control 6] do={:set $allowedASCI " :/0123456789";:set $controlASCI "#";:set $errorName "IPv6 address"}; # numbers and special signs allowed.
 
 # (3[0-2]|2[0-9]|[1][0-9]|[0-9]) CIDR Or :put [:typeof (255.168.21.7/24 in 0.0.0.0/0)] bool is ok and if nothing the the IP address was invalid.
  
 :set $minLength [:pick $3 0 [:find $3 "-"]]
 :set $maxLength [:pick $3 ([:find $3 "-"]+1) [:len $3]]
 :if  $hide   do={:set $1 "$1 TAB key shows entry."}; # Add to help text that the hidden content can be made visible
 # Adds the minimal an maximal length displayed in the Help line. If no length is defined then nothing is displayed.  
 :if ($minLength = $maxLength && $minLength != 0) do={:set $1 "$1 (Set length: $maxLength )"} else={
   :if ($minLength=0 && $maxLength>0 && $maxLength!=255)	do={:set $1 "$1 (Maximal length: $maxLength)"}
   :if ($minLength>0 && $maxLength>0 && $maxLength!=255)	do={:set $1 "$1 (Minimal length: $minLength, maximal length: $maxLength)"}
   :if ($minLength>0 && $maxLength=255) 			do={:set $1 "$1 (Minimal length: $minLength)"} }   
 :te st varname-local; :te el; :put "> Help:\t $1"; :te cuu; :te cuu; :te st syntax-noterm; # Displays the (adapted) help text
 :local i 0xFFFF; :set $keyFlag false; [:te el];:set $firstDisplay true;
 :put "$2 _"; # write first time the label with a cursor (_)
 
 :if ([:len $4] > $maxLength) do={[:te cuu;:te st varname]; :put "$2 $display \t Ignored the provided entry. Enter one yourself.$sound"; [:te st none];:set $4;:set $keyFlag false}
 :if ([:len $4] < $minLength && [:len $4] > 0) do={[:te cuu;:te st varname]; :put "$2 $4 \t Error: the pre-provided entry is to short. Add to, or replace this entry.$sound"; [:te st none]; :set $display $4;:set $readString $4;:set $4}
 
 :do {
  
  :while ( (!$endKey) ) do={
    :if ([:len $readString] > 0) do={    
      :if ($i=8) do={:set $keyFlag false; :set $readString "$[:pick $readString 0 ([:len $readString]-1)]"; :set $display "$[:pick $display 0 ([:len $readString])]"}      
      :if (($i=9) && $hide && (!$unHide))	do={:set $unHide true; :set $tempDisplay $display;:set $keyFlag false;:set $i}
      :if (($i=9) && $unHide && $hide)		do={:set $unHide false;:set $display [:pick "$tempDisplay******************" 0 [:len $readString]];:set $keyFlag false;:set $i} }            
    :if $keyFlag 			do={:set $temp "$[:pick $ASCI ($i-32) ($i-31)]";}; :set $i;
   
    :if [:find $control $controlASCI]	do={:if ([:find $allowedASCI $temp] >= 0) do={} else={:set $temp} }
    
    :if (([:typeof $temp]~"(nil|nothing)" && $keyFlag) && (!$firstDisplay)) do={[:te cuu;:te el;:te st varname];:put "$2 $display \t ERROR: ignored invalid key input.$sound"; [:te st none];:set $keyFlag false}
    
    :if (!firstDisplay) do={       
    :if (([:len $readString] >= $maxLength) && $keyFlag) do={[:te cuu;:te st varname]; :put "$2 $display \t Error: maximal $maxLength $errorName allowed.$sound";	[:te st none]; :set $temp; :set $keyFlag false} }    
    :if $keyFlag do={:set $readString "$readString$temp"};	                                        # Here the accepted character is added to $readString as long the length is less than maximum 
    :if ($hide) do={:if $keyFlag do={:set $display "$display*"};} else={:set $display $readString};	# Sets the displayed text 
    :if ($unHide) do={:set $display $readString};	                                                # Unhide text that is hidden when pressing the TAB key
    :if $firstDisplay do={:set $display $4;:set $readString $5;:set $firstDisplay false;:set $4;:set $5};	# Sets display to the provided string if it is the first view
   
 # Loop that displays and wait for a key to be pressed         
   :do {[:te cuu]; :put "$2 $display_ "; :set $i [:te in 500ms]; :if ($i=0xFFFF) do={[:te cuu;]; :put "$2 $display  "; :set $i [:te in 500ms]}} while=($i=0xFFFF); :set $keyFlag true; 
   [:te cuu;:te el]; :put "$2 $display  "; # erases any error messages shown
   :if ($i=60932) do={:set $i 13}; # The arrow down key is the same here as the Enter key and any changes will be checked this way.
   :if ($i=13 || $i=60931) do={:set $endKey true}; # This will enable to use one value to indicate the completion of the entry or go for an other entry up to for editing.
  }; #while=( (!$endKey)....
  :if ($i != 60931) do={ # Skip checks because these values where already checked before.
   :if ($unHide) do={[:te cuu;:te el]; :put "$2 $tempDisplay"; :set $display $tempDisplay}; # On exit hide the text again, which was made visible by using the TAB key.   
   :if (([:len $readString]>0)&&([:len $readString]<$minLength)) do={[:te cuu;:te st varname]; :put "$2 $display \t Error: not enough $errorName entered. Minimal length is: $minLength.$sound";[:te st none]; :set $endkey false;:set $keyFlag false}
   :if (([:len $readString] = 0) && ($minLength > 0)) do={[:te cuu;:te st varname]; :put "$2 $display \t Error: this entry can't be left empty.$sound";	[:te st none]; :set $endKey false;:set $keyFlag false}  
   :if ($endCheck) do={
    :if ($readString ~ $endCheckRegex) do={:set $endCheck false} else={[:te cuu;:te st varname]; :put "$2 $display \t Error: format of entry is incorrect. See example in help underneath.$sound"; [:te st none]; :set $keyFlag false; :set $endKey false} }; # :if ($endcheck..
  } else={:set $upKey true}; # Skip the checks if change up to an other entry. $readString should be destroyed and ignored on return.
 :set $endKey false
 } while ((([:len $readString] < $minLength) || $endCheck) && $i != 60931 );  #while check minium
 #:log info "$upKey $i"
 :return {$readString;$display;$upKey}
}; # End of function readKeyString, the value where returned to the caller, saved there in an array

#####################################################################################################################
# defining variables and assign to them values if needed.
#:local readKeyString; # needed if is called to the :global
:global arrayResult   [:toarray ""]; :local arrayLabel [:toarray ""]; :local arrayControl [:toarray ""]; :global arrayDisplay [:toarray ""]
:set $keyString "123456789abcdefghijklmnopqrstuvwxyz"; # Limits the number of menu items and acts a sequencer of the menuitems.
# Store menu in an array
# The number/letters defines the sequence of in the menu, description/help and then the show menu item with always a ":".
# If needed a minimal and maximal length that should be entered and then control characters:
# @ = e-mail addres, / = web/url, # = numbers and * = should not be displayed 
:set $arrayLabel { "7 IPv4 address 192.168.0.1"="IPv4 address:I";
		   "1 Login name which you want to use for this device."="Login name:1-15";
                   "2 Enter or change password."="Your password:4-8*";
                   "3 Pin"="Pin:4-4#*";
                   "4 Remark"="Remark 2:"
                   "z Are all entries correct. If not use ARROW-UP to correct. Then press ENTER."="Ready:"}
                   #"5 Webpage Example: (www.)mikrotik.com"="Webpage:/";
                   #"6 e-mail address (example: you@mail.com)"="E-mail:@"

# The control characters used in $arrayLabel should be present in $arraControl. Manual entry needed in the key requests and error messages.                   
 :set $arrayControl {"/";"@";"#";"*";"I";"P"}; # URL e-mail number hidden	#"8 IPv6 address 192.168.0.1"="IPv4 address:P";}                  
# Fill the support array with their assinged values.
 :for i from=0 to=([:len $arrayLabel]-1) do={:set $a [:pick $arrayLabel $i];:set $a "$[:pick $keyString $i ($i+1)]$[:pick $a 0 ([:find $a ":"]+1)]";:set ($arrayDisplay->"$a") ""}; # Fill $arraydisplay with labels
 :for i from=0 to=([:len $arrayDisplay]-1) do={:set $h $i; :set $a [:tostr [:pick $arrayDisplay $i ($i+1)]]; :set $a [:pick $a 0 [:find $a "="]]}
 :set $count 0; # set to default value                 
# Prints the header of the menue
 :put "- Menu testing -"
 :put "----------------"
# Writes out the full menu and then return to the first line of th menu
 :foreach label,value in $arrayDisplay do={:put "$[:pick $label 1 [:len $label]] $value"};	# Shows the whole menu.
 :for i from=0 to=([:len $arrayDisplay]-1) do={:te cuu};	                                # Goes to the top of the shown menu. 
# Starting the calls to the readkey function.
:while ($count <= ([:len $arrayLabel]-1)) do={

:set $a [:tostr [:pick $arrayDisplay $Count ($count+1)]]; :set $label [:pick $a 1 [:find $a "="]]; # Gets the $label of the next menu-item and cleans it up.
:if (($returnArray->2) && $count = 0) do={} else={:put $label}; # Only display the label if it is a first display
:set $label	[:pic $arrayLabel $count]; # Gets the $label
:set $descriptLabel [:tostr [:pick $arrayLabel $count ($count+1)]];	# gets the array string including the key.
:set $descript [:pick $descriptLabel 2 [:find $descriptLabel "="]];	# Get key of array...why is that made so difficult. Also removes the first two index characters in one go.

# getting control characters and this can be simplified by adding a extra parameter to the called function
 :set $length;
 :set $control [:pick $label ([:find $label ":"]+1) [:len $label]]
 :set $label [:pick $label 0 ([:find $label ":"]+1)]; # restore the label without the number for length
 :foreach findControl in=$arrayControl do={
  :foreach findControl in=$arrayControl do={:if ([:typeof [:find $control $findControl ([:len $control]-2)]] != "nil") do={:set $control [:pick $control 0 ([:len $control]-1)]; :set $label "$label$findControl"}} 
 }; # nibling control away, to obtain only the $length and add at the same time those to $label
 :set $length $control; # The leftover is the lenght if defined.
 :set $control; # Erase $control
 
#:put "Label: $label / control: $control / length: $length / descript: $descript \n\n\n\n"
#:error ""

#:set $descript [:pick $descript 2 ([:len $descript])]; # Remove the first two characters of the string. These are only used to order the fields in the array.
 :if ([:len $length] = 0) do={:set $length "0-255"}; # Always set a length even if it is zero zero
#:log info "count: $count / return: $($returnArray->2)"
# Call to function
#:set $show "PassWord"
 :set $returnArray [$readKeyString $descript $label $length [:pick $arrayDisplay $count] [:pick $arrayResult $count]]; 	# Call to function
 :if (($returnArray->2) && $count > 0) do={[:te el]; :put "$[:pick $label 0 ([:find $label :]+1)] $[:pick $arrayDisplay $count]";[:te cuu;:te cuu];:set $count (count-1); :if ($count > 0) do={[:te cuu]} } else={
   :set $resultString	($returnArray->0);:set $displayString	($returnArray->1); # Get the result and display values from the array 
# put a number/letter from $keyString in front of the label so that sequence in which the array stays as entered. (no auto sort)
   :set $label	"$[:pick $keyString $count ($count+1)]$label";
   :set $label	[:pick $label 0 ([:find $label ":"]+1)]; # Remove any returned control characters
# store the returned entries and displayed
   :set ($arrayResult->"$label") $resultString
   :set ($arrayDisplay->"$label") $displayString
   :if ($returnArray->2) do={} else={:set $count ($count + 1)}; # Add one to the counter in While if the entry was ignored and the the arrow up key was pressed
 }; # On $upKey all the not confirmed by and ENTER are ignored and the previous entry is selected
};

# And show the entered values
 [:te style syntax-no]; :put "Returned entries:"
 :foreach label,value in=$arrayResult do={
  [:te st syntax-no];   :put "$[:pick $label 1 ([:find $label ":"]+1)] ------------------";	# "1" Removes the sort letter and the "+1" keeps the ":".
  [:te cuu;:te st esc]; :put "\t\t $value                                                "
  :terminal style none
 }; # foreach label,string
 :put "\n"
}

Have fun :wink:

Thanks a lot. Come in handy.

gatto-felice.jpg
Bravi!!! Tutti, mi piace quando c’è collaborazione!!!

The (>) is undocumented, so always a risk. While it provides some handy shortcut to define a function inline, it be easy to use the “normal” way like set ($myquestion->0->“validate”) do={} – just not as clean as doing in a single array definition. Now… (>) use as a “inline function” as an argument to another function call is kinda handy – otherwise you’d have to define the function in another line.

My guess is it’s artifact of their parser – op_erators_ like greater-than > are still functions internally but take input from the left side – most function take the argument from the right side of fn name…but since left side is nil/nothing/null, it somehow causes compare operator to “escape” (and returned). My bet is they have a lot of bugs to fix outside of mucking with the bowls of their script interpreter code, so likely “safe”.

Great work. Nice sound effects… now if only [:beep] worked using the terminal. I should fixup up the cursor position & borrow some of your validation. (Although theoretically UTF-8 input passthrough if terminal supported it - why it’s not limited to lower ASCII)

But part of what I was going for is simplicity to get one question, so

:local iname [$INQUIRE ({{text="name?"}}) as-value]

works in a larger script to get some input.

One of the feature of the original JS library was a “choice” question type, essentially the equivalent of a “radio selector” in windows. But to show a menu is kinda different that collecting input, I wrote a seperate function to do it. I don’t have time to merge it with the $INQUIRE right now – e.g. so the questions array can use a type=“choice” and choices={“one”;“two”}. But posting the code both so I don’t forget & might be useful independent of $INQUIRE function anyway.

The $CHOICES function takes an array as 1st argument, and presents that as a selectable list in the terminal. Then, one of the items can be selected by moving around the UP/DOWN arrow keys. A value is selected by hitting enter/return on the selected (shown in reversed text). The selected item’s string (or val=, see below) is the return as the value of the function. e.g.

:put [$CHOICES ({"Dog"; "Cat"; "Snake"; "Pig"; "Horse"; "Bird"; "Llama"})]

          Dog      
          Cat      
          Snake            
          **Pig**      
          Horse            
          Bird             
          Llama            
Pig

The provided array list can populated with “str” or contain inner associative array with val=, text=, and help= attributes. If just a string is used, the return value and what’s displayed are the same. However, when an associative array, text= is what’s displayed on the terminal in list, while val= is what will be returned by the function. With a help= that shows some string next to the selectable item e.g. in dimmed text, like a “tooltip”

:put [$CHOICES ({{val="yes";text="Yes";help="go ahead"};{val="no";text="No";help="skip it"}})]

The return value can be stored in a variable to use in other script code:

{
    :local d40 [$CHOICES ({{val="Yup"};{text="Nope"}})]
    :put "$d40 - good choice!"
}

Note: As shown above, the input array with the “choices” for $CHOICES, will try “fix up” empty values… So if you set just text=, that’s what will be returned if selected even without a val=. Similar with only a val=, that becomes the text=.





Eventually, I’ll update $INQUIRE to use it. But here is the $CHOICES function as a standalone function:

:global CHOICES do={
# :global CHALK
:local lchoices $1
:local isel 0
:if ([:len $lchoices]>0) do={:set isel 0} else={:error “error - no choices”}
:for uchoice from=0 to=([:len $lchoices]-1) do={
# convert string list to array
:if ([:typeof ($lchoices->$uchoice)]=“str”) do={
:set ($lchoices->$uchoice) {val=($lchoices->$uchoice);text=($lchoices->$uchoice)}
}
# if array, regularize it
:if ([:typeof ($lchoices->$uchoice)]=“array”) do={
:if ([:typeof ($lchoices->$uchoice->“val”)]~“str|num”) do={
:if (([:typeof ($lchoices->$uchoice->“text”)]~“str|num”)) do={
# both text= and val=
} else={
# val= but NO text=
:set ($lchoices->$uchoice->“text”) ($lchoices->$uchoice->“val”)
}
} else={
# NO val=
:if (([:typeof ($lchoices->$uchoice->“text”)]~“str|num”)) do={
# use text= as val=
:set ($lchoices->$uchoice->“val”) ($lchoices->$uchoice->“text”)
} else={
#invalid
:set ($lchoices->$uchoice) {val=“invalid$uchoice”;text=“$[:tostr ($lchoices->$uchoice)] [invalid$uchoice]”}
}
}
} else={
# neither string nor array
:set ($lchoices->$uchoice) {val=“invalid$uchoice”;text=“$[:tostr ($lchoices->$uchoice)] [invalid$uchoice]”}
}
}
:if ([:typeof $selected] = “str”) do={
:set isel [:find $lchoicesnames ]
}
:local lkey 0
:local bsel “”
:while ($lkey != 13) do={
:if ($lkey != 0) do={:for icuu from=0 to=([:len $lchoices]-1) do={/terminal/cuu}}
:for ichoice from=0 to=([:len $lchoices]-1) do={
# avoid CHALK dependency
#:if ($isel = $ichoice) do={:set bsel “yes”} else={:set bsel “no”}
#:put “\t$[$CHALK blue inverse=$bsel] $($lchoices->$ichoice->“text”) $[$CHALK reset]\t$[$CHALK grey dim=yes]$($lchoices->$ichoice->“help”)$[$CHALK reset]”
:if ($isel = $ichoice) do={:set bsel “7;”} else={:set bsel “”}
:put “\t \1B[$($bsel)34;49m $($lchoices->$ichoice->“text”) \1B[0m \t \1B[90;49m $($lchoices->$ichoice->“help”) \1B[0m”
}
:set lkey [/terminal/inkey]
:if ($lkey = 60931) do={
:set isel ($isel-1)
:if ($isel < 0) do={:set isel 0}
}
:if ($lkey = 60932) do={
:set isel ($isel+1)
:if ($isel > ([:len $lchoices]-1)) do={:set isel 0}
}
}
:return ($lchoices->$isel->“val”)
}

Do you ultimately want to create a better “QuickSet”? :laughing:

Close, but not exactly. [ @mrz put the kibosh to my feature request for CLI/API to quickset: Is QuickSet available via the API? - so this be one solution for a better QuickSet :wink: ]

It’s /container actually where I run into these “user input” needs. Today I use a script per container that manage a particular container. But it’s hard to be generic, so the script requires a bunch of variable in the code – that might need to be changed for a particular router – and editing a script isn’t very “user friendly”. For example, if you look at my serial2http container, you can see how complex installation is to describe. A few user prompts to install avoid a lot of writing: GitHub - tikoci/serial2http: RouterOS container that proxies serial port via HTTP using TCP serial (RFC-2217)

For example, what a container’s root-dir should be is not that easy to abstract/guess, so be good to get a user to “confirm” the path. And if you think each question in $INQUIRE’s array is mapped to each on of a container’s environment variables, to confirm or alter how the container runs, the logic here might make more sense.

BTW, It’s actually the “ipprefix” type I need to add my INQUIRE script – going to have to borrow your :parse technique for that one (since there still isn’t a :toipprefix :wink: )

I made just some useful things… :wink:

:laughing:

Well, you can put a wizard over your extensive encoding and converter libraries…




{
    :local iftrans do={
        :global CHOICES
        :local gostspecs {"OST 8483";"GOST 16876-71 (1973";"T SEV 1362 (1978)";"GOST 7.79-2000 (2002)";"GOST 52535.1-2006 (2006)";"ALA-LC";"ISO/R 9";"ISO 9"}
        :if ($1 = "Latin-Transliterated Cyrillic") do={
            :put "There are many standards for transliteration...which one?"
            :return [$CHOICES $gostspecs]
        } else={:return [:nothing]} 
    }

    :local encodings {"Urlencoded";"Base64";"HexString";"UCS2";"GSM7";"UTF8";{text="CP1252 / Latin-1";val="CP1252";help="RouterOS default"};"CP1291";{val="ASCII";text="US ASCII"; help="us-ascii"};"Latin-Transliterated Cyrillic"}
    :local inouts {"global/local variable";"file";"escaped string text";"PDU field (big-endian,semi-octets)"}
    :put "How is the text already encoded?"
    :local inencoding [$CHOICES $encodings]
    :local ingosts [$iftrans $inencoding]
    :put "Where is it stored currently?"
    :local insrc [$CHOICES $inouts]
    :put "What encoding to you need output?"
    :local outencoding [$CHOICES $encodings]
    :local outgosts [$iftrans $outencoding] 
    :put "Which output do you need?"
    :local outdest [$CHOICES $inouts]

    :put "..."
    :put "SpamGPT says:"
    :put "..."
    :put "Help @rextended! I need $inencoding $ingosts stored in a $insrc, for output in $outencoding $outgosts to $outdest."
    :put "..."
    :put "@reextended says:"  
    :put "Do you not know how to search? \1B]8;;$http://forum.mikrotik.com/search.php?keywords=$($inencoding)to$($outencoding)\07http://forum.mikrotik.com/search.php?keywords=$($inencoding)to$($outencoding)\1B]8;;\07"  
    :put ""
}

edit1: minor typos in output text
edit2/3: added “clickable URL” and expected response

Too bad there is no rep system here.

You definitely would have received a positive one for that last post :laughing: