Добрый вечер всем.
Возникла задача написать свой 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/ — можно редактировать и проверять грамматику на лету.
Комментарии (3)
webdevium
08.11.2018 12:51commander.js отлично справляется, без лишних телодвижений. И документация хорошая.
bobalus Автор
08.11.2018 13:04commander, насколько я понял, парсит аргументы, передаваемые командной строкой.
В статье о том, как реализовать readline(https://nodejs.org/api/readline.html) интерфейс, с поддержкой EBNF.
UPD: да, можно передать в Command#parse() команду, считанную с readline. Спасибо за информацию.
rbobot
А какой в этом смысл?