在NodeJS上编写CLI


大家晚上好。


在node.js上编写沉浸式CLI时出现问题。 以前为此目的使用了vorpal 。 这次我想做的是没有不必要的依赖关系,此外,我考虑了采用不同的命令参数的可能性。


使用vorpal时,命令编写如下:


setValue -s 1 -v 0 

同意,每次写-s都不是很方便。


最后,团队转变为以下团队:


 set 1: 0 

如何实施-削减


  1. 同样,一个不错的好处是,可以以值列表的形式(以空格分隔和以数组的形式)传输多个参数。

文字输入


我使用readline输入文本。 通过以下方式,我们创建一个支持自动完成的接口:


  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可以正常工作,即 在当前行上显示文本并进行换行,并且如果在独立于文本输入的某个外部事件上调用,则数据将显示在输入行上。 因此,我们使用console_out函数,该函数在输出到控制台后调用readline输入行。


解析器


似乎可以将字符串分成多个空格,分离各个部分并进行处理。 但是这样就不可能传递包含空格的字符串参数。 在任何情况下,都必须删除多余的空格和制表符。


最初,他计划自己实现解析器,将赫伯特·希尔尔德(Herbert Schildt)关于C语言的书中递归的递归解析器重写为JS。在执行过程中,决定简化该解析器,但最终却无法实现,因为 在编写过程中,我找到了ebnf软件包,并且由于对BNF / EBNF语法定义系统感兴趣并熟悉了它,因此决定在应用程序中使用它。


文法


我们在语法文件中描述命令和参数。
首先,定义以下内容:


  1. 表达式由一行组成。 我们不需要处理两个以上的行。
  2. 表达式的开头是命令标识符。 进一步的论点。
  3. 由于命令数量有限,因此每个命令都写在语法文件中。

入口点如下:


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

WS *表示空格-空格或制表符。 描述如下:


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

这意味着字符空间,制表符或换行符会重复出现一次。


让我们继续前进。
最简单,不带参数:


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

此外,这些命令采用以空格或数组分隔的自然数列表。


 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 ) 

因此,以下示例对于get命令是正确的:


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

接下来,使用set命令,该命令采用输入对id:value或值数组。


 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 ) 

因此,对于set命令,以下表示形式是正确的:


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

在js中处理


我们读取文件,创建解析器对象。


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

此外,在输入数据时,readline对象的实例会发出线事件的信号,该事件由以下功能处理:


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

如果正确编写了命令,则解析器将返回一棵树,其中的每个元素都有一个类型,子字段和文本字段。 type字段采用当前元素的类型值。 即 如果我们将ping命令传递给解析器,则树将看起来像跟踪。 方式:


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

我们以以下形式编写:


 command ping Text = "ping" 

对于命令“ get 1 2 3”,


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

接下来,我们处理每个命令,执行必要的操作并将结果显示在控制台中。


结果是非常方便的界面,以最小的依赖关系加快了工作速度。 我将解释:


在用于读取组地址的图形界面(ETS)中(例如),您需要在输入字段中输入一个组地址,然后使用鼠标按钮(或几个TAB)发送请求。


在通过vorpal实现的界面中,命令如下:


 readValue -s 1 

或者:


 readValues -s "1, 3" 

使用解析器,可以避免不必要的“ -s”元素和引号。


 read 1 3 

连结


  1. https://github.com/bobaoskit/bobaos.tool-项目存储库。 您可以看一下代码。
  2. http://menduz.com/ebnf-highlighter/-您可以即时编辑和检查语法。

Source: https://habr.com/ru/post/zh-CN426615/


All Articles