Good evening everyone.
There was a task to write your immersive CLI on node.js. Previously used for this purpose vorpal . This time, I wanted to do without unnecessary dependencies and, in addition to this, considered the possibility of accepting command arguments in a different way.
With vorpal commands were written as follows:
setValue -s 1 -v 0
Agree, every time to write -s
is not very convenient.
In the end, the team turned into the following:
set 1: 0
How it can be implemented - under the cut
To enter text I use readline
. In the following way we create an interface with auto-completion support:
let commandlist = []; commandlist.push("set", "get", "stored", "read", "description"); commandlist.push("watch", "unwatch"); commandlist.push("getbyte", "getitem", "progmode"); commandlist.push("ping", "state", "reset", "help"); function completer(line) { const hits = commandlist.filter(c => c.startsWith(line)); // show all completions if none found return [hits.length ? hits : commandlist, line]; } /// init repl const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "bobaos> ", completer: completer }); const console_out = msg => { process.stdout.clearLine(); process.stdout.cursorTo(0); console.log(msg); rl.prompt(true); };
console.log
works as expected, i.e. displays the text on the current line and transfers the line, and if it is called on any external event that does not depend on the text input, the data will be output to the input line. Therefore, we use the function console_out, which, after outputting to the console, calls the readline input line.
It would seem that the line can be divided into spaces, separate the individual parts and process. But then it will be impossible to transfer string parameters containing a space; and in any case it will be necessary to remove extra spaces and tabs.
Initially, the parser planned to implement itself, rewriting the descending recursive parser from the book of Herbert Shildt on C. In the process of writing, I found the ebnf package, and, becoming interested in and familiarizing myself with the BNF / EBNF syntax definition systems, I decided to use it in my application.
We make descriptions of commands and arguments in the grammar file.
First, let's define the following:
The input point is as follows:
command ::= (set|get|stored|read|description|getbyte|watch|unwatch|ping|state|reset|getitem|progmode|help) WS*
WS * means whitespace - space or tab characters. Described as follows:
WS ::= [#x20#x09#x0A#x0D]+
What does the space character, tab, or line break, occurring once and more.
Let's go to the teams.
The simplest, without arguments:
ping ::= "ping" WS* state ::= "state" WS* reset ::= "reset" WS* help ::= "help" WS*
Further, the commands that take as input a list of natural numbers separated by a space, or an array.
BEGIN_ARRAY ::= WS* #x5B WS* /* [ left square bracket */ END_ARRAY ::= WS* #x5D WS* /* ] right square bracket */ COMMA ::= WS* #x2C WS* /* , comma */ uint ::= [0-9]* UIntArray ::= BEGIN_ARRAY (uint WS* (COMMA uint)*) END_ARRAY UIntList ::= (uint WS*)* get ::= "get" WS* ( UIntList | UIntArray )
Thus, for the get command, the following examples would be correct:
get 1 get 1 2 3 5 get [1, 2, 3, 5, 10]
Next, the set command, which accepts a pair of id: value, or an array of values.
COLON ::= WS* ":" WS* Number ::= "-"? ("0" | [1-9] [0-9]*) ("." [0-9]+)? (("e" | "E") ( "-" | "+" )? ("0" | [1-9] [0-9]*))? String ::= '"' [^"]* '"' | "'" [^']* "'" Null ::= "null" Bool ::= "true" | "false" Value ::= Number | String | Null | Bool DatapointValue ::= uint COLON Value DatapointValueArray ::= BEGIN_ARRAY (DatapointValue WS* (COMMA DatapointValue)*)? END_ARRAY set ::= "set" WS* ( DatapointValue | DatapointValueArray )
Thus, for the set command, the following entry forms are correct:
set 1: true set 2: 255 set 3: 21.42 set [1: false, 999: "hello, friend"]
Read the file, create a parser object.
const grammar = fs.readFileSync(`${__dirname}/grammar`, "utf8"); const parser = new Grammars.W3C.Parser(grammar);
Further, during data entry, the readline object instance is signaled by the line event, which is processed with the following function:
let parseCmd = line => { let res = parser.getAST(line.trim()); if (res.type === "command") { let cmdObject = res.children[0]; return processCmd(cmdObject); } };
If the command was written correctly, the parser returns a tree, where each element has a type field, children, and a text field. The type field takes the value of the type of the current element. Those. if we send the ping command to the parser, the tree will look like a trace. in the following way:
{ "type": "command", "text": "ping", "children": [{ "type": "ping", "text": "ping", "children": [] }] }
We write in the form:
command ping Text = "ping"
For the "get 1 2 3" command,
command get UIntList uint Text = "1" uint Text = "2" uint Text = "3"
Next, we process each command, do the necessary actions, and output the result to the console.
The result is a very user-friendly interface that speeds up work with a minimum of dependencies. Will explain:
in the graphical interface (ETS) to read the group addresses (for example), you must enter one group address in the input field, then click (or several TABs) to send a request.
In the interface implemented via vorpal, the command is as follows:
readValue -s 1
Or:
readValues -s "1, 3"
With the use of the parser, it is possible to avoid unnecessary "-s" elements and quotes.
read 1 3
Source: https://habr.com/ru/post/426615/
All Articles