Écriture d'une CLI sur NodeJS


Bonsoir à tous.


Un problème est survenu lors de l'écriture de votre CLI immersive sur node.js. Vorpal précédemment utilisé à cet effet. Cette fois, je voulais me passer de dépendances inutiles et, en plus, j'ai envisagé la possibilité de prendre les arguments de commande différemment.


Avec vorpal, les commandes ont été écrites comme suit:


setValue -s 1 -v 0 

D'accord, écrire -s chaque fois n'est pas très pratique.


Au final, l'équipe s'est transformée en ce qui suit:


 set 1: 0 

Comment il peut être mis en œuvre - sous la coupe


  1. De plus, un bon bonus est le transfert de plusieurs arguments sous la forme d'une liste de valeurs, séparés par un espace et sous la forme d'un tableau.

saisie de texte


J'utilise readline pour saisir du texte. De la manière suivante, nous créons une interface avec prise en charge de l'auto-complétion:


  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 fonctionne comme prévu, c'est-à-dire affiche le texte sur la ligne actuelle et encapsule la ligne, et s'il est appelé sur un événement externe indépendant de l'entrée de texte, les données seront affichées sur la ligne d'entrée. Par conséquent, nous utilisons la fonction console_out, qui, après sortie sur la console, appelle la ligne d'entrée readline.


analyseur


Il semblerait que vous pouvez diviser la chaîne en espaces, séparer les différentes parties et la traiter. Mais alors il sera impossible de passer des paramètres de chaîne contenant un espace; et dans tous les cas, il sera nécessaire de supprimer les espaces et les tabulations supplémentaires.


Initialement, il avait prévu d'implémenter l'analyseur lui-même, en réécrivant l'analyseur récursif descendant du livre d'Herbert Schildt sur le langage C. En JS. Au cours de l'exécution, il a été décidé de simplifier l'analyseur, mais finalement il n'a pas été possible de l'implémenter, car en cours d'écriture, j'ai trouvé le paquet ebnf , et, devenu intéressé et familiarisé avec les systèmes de définition de syntaxe BNF / EBNF, j'ai décidé de l'utiliser dans mon application.


grammaire


Nous décrivons les commandes et les arguments dans le fichier de grammaire.
Pour commencer, définissez les éléments suivants:


  1. L'expression se compose d'une ligne. Nous n'avons pas besoin de traiter plus de deux lignes.
  2. Au début de l'expression se trouve l'identifiant de commande. Arguments supplémentaires.
  3. Le nombre de commandes étant limité, chacune d'elles est écrite dans le fichier de grammaire.

Le point d'entrée est le suivant:


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

WS * signifie des espaces - des espaces ou des tabulations. Il est décrit comme suit:


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

Ce qui signifie que l'espace de caractère, la tabulation ou le saut de ligne se produisent de nouveau.


Passons aux équipes.
Le plus simple, sans arguments:


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

De plus, les commandes qui prennent une liste de nombres naturels séparés par un espace ou un tableau.


 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 ) 

Ainsi, les exemples suivants sont corrects pour la commande get:


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

Ensuite, la commande set, qui prend une paire d'entrée id: value, ou un tableau de valeurs.


 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 ) 

Ainsi, pour la commande set, les formes de notation suivantes sont correctes:


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

processus en js


Nous lisons le fichier, créons l'objet analyseur.


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

De plus, lors de la saisie de données, une instance de l'objet readline signale l'événement line, qui est traité par la fonction suivante:


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

Si la commande a été écrite correctement, l'analyseur renvoie un arbre, où chaque élément a un type, un champ enfant et un champ texte. Le champ type prend la valeur de type de l'élément courant. C'est-à-dire si nous passons la commande ping à l'analyseur, l'arborescence ressemblera à une trace. façon:


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

Nous écrivons sous la forme:


 command ping Text = "ping" 

Pour la commande "get 1 2 3",


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

Ensuite, nous traitons chaque commande, effectuons les actions nécessaires et affichons le résultat dans la console.


Le résultat est une interface très pratique qui accélère le travail avec un minimum de dépendances. Je vais vous expliquer:


dans l'interface graphique (ETS) pour lire les adresses de groupe (par exemple), vous devez entrer une adresse de groupe dans le champ de saisie, puis utiliser le bouton de la souris (ou plusieurs TAB) pour envoyer une demande.


Dans l'interface implémentée via vorpal, la commande est la suivante:


 readValue -s 1 

Ou:


 readValues -s "1, 3" 

En utilisant l'analyseur, vous pouvez éviter les éléments "-s" inutiles et les guillemets.


 read 1 3 

liens


  1. https://github.com/bobaoskit/bobaos.tool - référentiel de projet. Vous pouvez regarder le code.
  2. http://menduz.com/ebnf-highlighter/ - vous pouvez modifier et vérifier la grammaire à la volée.

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


All Articles