Escrevendo uma CLI no NodeJS


Boa noite a todos.


Ocorreu um problema ao gravar sua CLI imersiva em node.js. Vorpal usado anteriormente para esse fim. Dessa vez, queria passar sem dependências desnecessárias e, além disso, considerei a possibilidade de assumir argumentos de comando de maneira diferente.


Com vorpal, os comandos foram escritos da seguinte maneira:


setValue -s 1 -v 0 

Concordo, escrever sempre não é muito conveniente.


No final, a equipe se transformou no seguinte:


 set 1: 0 

Como pode ser implementado - sob o corte


  1. Além disso, um bom bônus é a transferência de vários argumentos na forma de uma lista de valores, separados por um espaço e na forma de uma matriz.

entrada de texto


Eu uso o readline para inserir texto. Da seguinte maneira, criamos uma interface com suporte para preenchimento automático:


  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 funciona conforme o esperado, ou seja, exibe o texto na linha atual e quebra a linha e, se chamado em algum evento externo independente da entrada de texto, os dados serão exibidos na linha de entrada. Portanto, usamos a função console_out, que, após saída para o console, chama a linha de entrada readline.


analisador


Parece que você pode dividir a sequência em espaços, separar as partes individuais e processá-la. Mas será impossível passar parâmetros de string contendo um espaço; e, em qualquer caso, será necessário remover espaços e guias extras.


Inicialmente, ele planejou implementar o analisador, reescrevendo o analisador recursivo descendente do livro de Herbert Schildt sobre a linguagem C. Em JS. Durante a execução, foi decidido simplificar o analisador, mas no final não foi possível implementá-lo, porque no processo de redação, encontrei o pacote ebnf e, tendo me interessado e familiarizado com os sistemas de definição de sintaxe BNF / EBNF, decidi usá-lo em meu aplicativo.


gramática


Nós descrevemos os comandos e argumentos no arquivo de gramática.
Para começar, defina o seguinte:


  1. A expressão consiste em uma linha. Não precisamos processar mais de duas linhas.
  2. No início da expressão está o identificador de comando. Mais argumentos.
  3. Há um número limitado de comandos, portanto cada um deles é gravado no arquivo de gramática.

O ponto de entrada é o seguinte:


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

WS * significa espaço em branco - caracteres de espaço ou tabulação. É descrito da seguinte maneira:


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

O que significa que o espaço de caracteres, tabulação ou quebra de linha ocorre uma e mais vezes.


Vamos para as equipes.
O mais simples, sem argumentos:


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

Além disso, os comandos que levam uma lista de números naturais separados por um espaço ou uma matriz.


 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 ) 

Portanto, os seguintes exemplos estão corretos para o comando get:


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

Em seguida, o comando set, que recebe um ID do par de entrada: value ou uma matriz de valores.


 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 ) 

Portanto, para o comando set, os seguintes formulários de notação estão corretos:


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

processo em js


Lemos o arquivo, criamos o objeto analisador.


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

Além disso, ao inserir dados, uma instância do objeto readline sinaliza o evento de linha, que é processado pela seguinte função:


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

Se o comando foi escrito corretamente, o analisador retornará uma árvore, na qual cada elemento possui um tipo, um campo filho e um campo de texto. O campo de tipo assume o valor de tipo do elemento atual. I.e. se passarmos o comando ping para o analisador, a árvore parecerá um rastreio. caminho:


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

Escrevemos no formulário:


 command ping Text = "ping" 

Para o comando "obter 1 2 3",


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

Em seguida, processamos cada comando, executamos as ações necessárias e exibimos o resultado no console.


O resultado é uma interface muito conveniente que acelera o trabalho com um mínimo de dependências. Vou explicar:


na interface gráfica (ETS) para ler endereços de grupo (por exemplo), é necessário inserir um endereço de grupo no campo de entrada e clicar em (ou várias TABs) para enviar uma solicitação.


Na interface implementada através do vorpal, o comando é o seguinte:


 readValue -s 1 

Ou:


 readValues -s "1, 3" 

Usando o analisador, você pode evitar elementos "-s" e aspas desnecessários.


 read 1 3 

ligações


  1. https://github.com/bobaoskit/bobaos.tool - repositório do projeto. Você pode olhar para o código.
  2. http://menduz.com/ebnf-highlighter/ - você pode editar e verificar a gramática em tempo real.

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


All Articles