
Добрый вечер всем.
Возникла задача написать свой immersive CLI на node.js. Ранее для этой цели использовал vorpal. В этот раз захотелось обойтись без лишних зависимостей и, помимо этого, рассматривал возможность по-другому принимать аргументы команд.
С vorpal команды писались следующим образом:
setValue -s 1 -v 0
Согласитесь, каждый раз писать -s — не очень удобно.
В конце концов, команда преобразовалась в следующую:
set 1: 0
Каким образом это можно реализовать — под катом
- Так же хорошим бонусом реализована передача нескольких аргументов в виде списка значений, разделенных пробелом и в виде массива.
ввод текста
Для ввода текста использую 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, решил использовать в своем приложении.
грамматика
Описания команд и аргументов делаем в файле грамматики.
Для начала, определим следующее:
- Выражение состоит из одной строки. Больше чем две строки нам обрабатывать не надо.
- В начале выражения идет идентификатор команды. Далее аргументы.
- Команд существует ограниченное количество, поэтому каждую из них прописываем в файле грамматики.
Входная точка выглядит следующим образом:
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
ссылки
- https://github.com/bobaoskit/bobaos.tool — репозиторий проекта. Можно глянуть код.
- http://menduz.com/ebnf-highlighter/ — можно редактировать и проверять грамматику на лету.