We write CLI on NodeJS


    Good evening everyone.


    There was a task to write your immersive CLI on node.js. Earlier for this purpose used 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, to write every time -sis not very convenient.


    In the end, the team turned into the following:


    set1: 0

    How it can be implemented - under the cut


    1. Also a good bonus is the transfer of several arguments in the form of a list of values, separated by a space and in the form of an array.

    text input


    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");
      functioncompleter(line) {
        const hits = commandlist.filter(c => c.startsWith(line));
        // show all completions if none foundreturn [hits.length ? hits : commandlist, line];
      }
      /// init replconst 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.logIt works as it should, i.e. displays the text on the current line and transfers the line, and if it is triggered by 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 output to the console, calls the readline input line.


    parser


    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 was planning to implement itself, rewriting the descending recursive parser from the book of Herbert Schild on C into JS. 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.


    grammar


    We make descriptions of commands and arguments in the grammar file.
    To begin with, we define the following:


    1. The expression consists of a single line. We do not need to process more than two lines.
    2. At the beginning of the expression is the command identifier. Further arguments.
    3. There are a limited number of commands, so each of them is written in the grammar file.

    The entry 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:


    get1get1235get [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 forms of writing will be correct:


    set1: trueset2: 255set3: 21.42set [1: false, 999: "hello, friend"]

    we process in js


    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 by 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 pass 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"

    Then 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 user 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, you can avoid unnecessary "-s" elements and quotes.


    read13

    links


    1. https://github.com/bobaoskit/bobaos.tool - project repository. You can look at the code.
    2. http://menduz.com/ebnf-highlighter/ - you can edit and check the grammar on the fly.

    Also popular now: