Добрый вечер всем.


Возникла задача написать свой immersive CLI на node.js. Ранее для этой цели использовал vorpal. В этот раз захотелось обойтись без лишних зависимостей и, помимо этого, рассматривал возможность по-другому принимать аргументы команд.


С vorpal команды писались следующим образом:


setValue -s 1 -v 0

Согласитесь, каждый раз писать -s — не очень удобно.


В конце концов, команда преобразовалась в следующую:


set 1: 0

Каким образом это можно реализовать — под катом


  1. Так же хорошим бонусом реализована передача нескольких аргументов в виде списка значений, разделенных пробелом и в виде массива.

ввод текста


Для ввода текста использую readline. Следующим образом создаем интерфейс c поддержкой автодополнения:


  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.


парсер


Казалось бы, можно строку разделить по пробелам, отделить отдельные части и обработать. Но тогда будет невозможна передача строковых параметров, содержащий пробел; и в любом случае необходимо будет убирать лишние пробелы и символы табуляции.


Изначально парсер планировал реализовать сам, переписав на JS нисходящий рекурсивный парсер из книги Герберта Шилдта по языку C. В ходе выполнения парсер решено было упростить, но, в итоге самому реализовать не удалось, т.к. в процессе написания нашел пакет ebnf, и, заинтересовавшись и ознакомившись с системами определения синтаксиса BNF/EBNF, решил использовать в своем приложении.


грамматика


Описания команд и аргументов делаем в файле грамматики.
Для начала, определим следующее:


  1. Выражение состоит из одной строки. Больше чем две строки нам обрабатывать не надо.
  2. В начале выражения идет идентификатор команды. Далее аргументы.
  3. Команд существует ограниченное количество, поэтому каждую из них прописываем в файле грамматики.

Входная точка выглядит следующим образом:


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

WS* означает whitespace — символы пробела или табуляции. Описывается следующим образом:


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 сигнализирует событием line, которое обрабатываем следующей функцией:


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

    return processCmd(cmdObject);
  }
};

Если команда была написана правильно, парсер возвращает дерево, где каждый элемент имеер поле type, children и поле text. Поле 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/ — можно редактировать и проверять грамматику на лету.

Комментарии (3)


  1. rbobot
    07.11.2018 18:22

    А какой в этом смысл?


  1. webdevium
    08.11.2018 12:51

    commander.js отлично справляется, без лишних телодвижений. И документация хорошая.


    1. bobalus Автор
      08.11.2018 13:04

      commander, насколько я понял, парсит аргументы, передаваемые командной строкой.

      В статье о том, как реализовать readline(https://nodejs.org/api/readline.html) интерфейс, с поддержкой EBNF.

      UPD: да, можно передать в Command#parse() команду, считанную с readline. Спасибо за информацию.