Schreiben einer CLI auf NodeJS


Guten Abend allerseits.


Es gab ein Problem beim Schreiben Ihrer immersiven CLI auf node.js. Zuvor für diesen Zweck vorpal verwendet. Diesmal wollte ich auf unnötige Abhängigkeiten verzichten und habe darüber hinaus die Möglichkeit in Betracht gezogen, Befehlsargumente anders zu nehmen.


Mit vorpal wurden Befehle wie folgt geschrieben:


setValue -s 1 -v 0 

Stimmen Sie zu, jedes Mal zu schreiben ist nicht sehr bequem.


Am Ende verwandelte sich das Team in Folgendes:


 set 1: 0 

Wie es umgesetzt werden kann - unter dem Schnitt


  1. Ein guter Bonus ist auch die Übertragung mehrerer Argumente in Form einer Liste von Werten, die durch ein Leerzeichen getrennt sind, und in Form eines Arrays.

Texteingabe


Ich benutze readline , um Text einzugeben. Auf folgende Weise erstellen wir eine Schnittstelle mit Unterstützung für die automatische Vervollständigung:


  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 funktioniert wie erwartet, d. console.log Zeigt den Text in der aktuellen Zeile an und umschließt die Zeile. Wenn ein externes Ereignis aufgerufen wird, das von der Texteingabe unabhängig ist, werden die Daten in der Eingabezeile angezeigt. Daher verwenden wir die Funktion console_out, die nach der Ausgabe an die Konsole die Eingabezeile readline aufruft.


Parser


Es scheint, dass Sie die Zeichenfolge in Leerzeichen unterteilen, die einzelnen Teile trennen und verarbeiten können. Dann ist es jedoch unmöglich, Zeichenfolgenparameter zu übergeben, die ein Leerzeichen enthalten. In jedem Fall müssen zusätzliche Leerzeichen und Tabulatoren entfernt werden.


Zunächst plante er, den Parser selbst zu implementieren und den absteigenden rekursiven Parser aus Herbert Schildts Buch über die C-Sprache in JS umzuschreiben. Während der Ausführung wurde beschlossen, den Parser zu vereinfachen, aber am Ende war es nicht möglich, ihn zu implementieren, weil Während des Schreibens fand ich das ebnf- Paket. Nachdem ich mich für die BNF / EBNF-Syntaxdefinitionssysteme interessiert und damit vertraut gemacht hatte, entschied ich mich, es in meiner Anwendung zu verwenden.


Grammatik


Wir beschreiben die Befehle und Argumente in der Grammatikdatei.
Definieren Sie zunächst Folgendes:


  1. Der Ausdruck besteht aus einer Zeile. Wir müssen nicht mehr als zwei Zeilen verarbeiten.
  2. Am Anfang des Ausdrucks steht die Befehlskennung. Weitere Argumente.
  3. Es gibt eine begrenzte Anzahl von Befehlen, daher wird jeder von ihnen in die Grammatikdatei geschrieben.

Der Einstiegspunkt ist wie folgt:


 command ::= (set|get|stored|read|description|getbyte|watch|unwatch|ping|state|reset|getitem|progmode|help) WS* 

WS * bedeutet Leerzeichen - Leerzeichen oder Tabulatorzeichen. Es wird wie folgt beschrieben:


 WS ::= [#x20#x09#x0A#x0D]+ 

Dies bedeutet, dass der Zeichenbereich, die Registerkarte oder der Zeilenumbruch immer wieder auftritt.


Gehen wir weiter zu den Teams.
Das einfachste ohne Argumente:


 ping ::= "ping" WS* state ::= "state" WS* reset ::= "reset" WS* help ::= "help" WS* 

Außerdem die Befehle, die eine Liste natürlicher Zahlen enthalten, die durch ein Leerzeichen oder ein Array getrennt sind.


 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 ) 

Daher sind die folgenden Beispiele für den Befehl get korrekt:


 get 1 get 1 2 3 5 get [1, 2, 3, 5, 10] 

Als nächstes der Befehl set, der eine Eingabepaar-ID: value oder ein Array von Werten annimmt.


 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 ) 

Für den Befehl set sind daher die folgenden Notationsformen korrekt:


 set 1: true set 2: 255 set 3: 21.42 set [1: false, 999: "hello, friend"] 

Prozess in js


Wir lesen die Datei, erstellen das Parser-Objekt.


 const grammar = fs.readFileSync(`${__dirname}/grammar`, "utf8"); const parser = new Grammars.W3C.Parser(grammar); 

Bei der Eingabe von Daten signalisiert eine Instanz des Readline-Objekts das Zeilenereignis, das von der folgenden Funktion verarbeitet wird:


 let parseCmd = line => { let res = parser.getAST(line.trim()); if (res.type === "command") { let cmdObject = res.children[0]; return processCmd(cmdObject); } }; 

Wenn der Befehl korrekt geschrieben wurde, gibt der Parser einen Baum zurück, in dem jedes Element einen Typ, ein untergeordnetes Feld und ein Textfeld hat. Das Typfeld nimmt den Typwert des aktuellen Elements an. Das heißt, Wenn wir den Ping-Befehl an den Parser übergeben, sieht der Baum wie eine Ablaufverfolgung aus. Weg:


 { "type": "command", "text": "ping", "children": [{ "type": "ping", "text": "ping", "children": [] }] } 

Wir schreiben in der Form:


 command ping Text = "ping" 

Für den Befehl "get 1 2 3",


 command get UIntList uint Text = "1" uint Text = "2" uint Text = "3" 

Als nächstes verarbeiten wir jeden Befehl, führen die erforderlichen Aktionen aus und zeigen das Ergebnis in der Konsole an.


Das Ergebnis ist eine sehr praktische Oberfläche, die die Arbeit mit einem Minimum an Abhängigkeiten beschleunigt. Ich werde erklären:


In der grafischen Oberfläche (ETS) zum Lesen von Gruppenadressen (zum Beispiel) müssen Sie eine Gruppenadresse in das Eingabefeld eingeben und dann auf (oder mehrere TABs) klicken, um eine Anforderung zu senden.


In der über vorpal implementierten Schnittstelle lautet der Befehl wie folgt:


 readValue -s 1 

Oder:


 readValues -s "1, 3" 

Mit dem Parser können Sie unnötige "-s" -Elemente und Anführungszeichen vermeiden.


 read 1 3 

Links


  1. https://github.com/bobaoskit/bobaos.tool - Projekt-Repository. Sie können sich den Code ansehen.
  2. http://menduz.com/ebnf-highlighter/ - Sie können die Grammatik im laufenden Betrieb bearbeiten und überprüfen.

Source: https://habr.com/ru/post/de426615/


All Articles