Как стать автором
Обновить

Пишем CLI на NodeJS

Время на прочтение4 мин
Количество просмотров7.8K


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


Возникла задача написать свой 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/ — можно редактировать и проверять грамматику на лету.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+7
Комментарии3

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

PG Bootcamp 2024
Дата16 апреля
Время09:30 – 21:00
Место
МинскОнлайн
EvaConf 2024
Дата16 апреля
Время11:00 – 16:00
Место
МоскваОнлайн
Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн