1
0
mirror of https://github.com/cjdelisle/openwrt.git synced 2025-10-07 11:19:56 +00:00
Files
openwrt/package/utils/cli/files/usr/sbin/cli
Felix Fietkau 1735da8e4c cli: add explicit option for pretty printing command result data
No-op for now, but allows making output more machine readable

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2025-04-30 11:04:14 +02:00

751 lines
14 KiB
Plaintext
Executable File

#!/usr/bin/env ucode
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
'use strict';
import * as datamodel from "cli.datamodel";
import { bold, color_fg } from "cli.color";
import * as uline from "uline";
import { basename, stdin } from "fs";
let history = [];
let history_edit;
let history_idx = -1;
let cur_line;
let interactive, script_mode, raw_mode;
while (length(ARGV) > 0) {
let cmd = ARGV[0];
if (substr(cmd, 0, 1) != "-")
break;
shift(ARGV);
switch (cmd) {
case '-i':
interactive = true;
break;
case '-s':
script_mode = true;
break;
case '-R':
raw_mode = true;
break;
}
}
let el;
let model = datamodel.new({
getpass: uline.getpass,
poll_key: (timeout) => el.poll_key(timeout),
status_msg: (msg) => {
el.hide_prompt();
warn(msg + "\n");
el.refresh_prompt();
},
opt_pretty_print: !raw_mode
});
let uloop = model.uloop;
model.add_modules();
let ctx = model.context();
let parser = uline.arg_parser({
line_separator: ";"
});
let base_prompt = [ "cli" ];
model.add_nodes({
Root: {
exit: {
help: "Exit the CLI",
call: function(ctx) {
el.close();
uloop.end();
interactive = false;
return ctx.ok();
}
}
}
});
model.init();
function update_prompt() {
el.set_state({
prompt: bold(join(" ", [ ...base_prompt, ...ctx.prompt ]) + "> "),
});
}
let cur_completion, tab_arg, tab_arg_len, tab_prefix, tab_suffix, tab_prefix_len, tab_quote, tab_ctx;
function max_len(list, len)
{
for (let entry in list)
if (length(entry) > len)
len = length(entry);
return len + 3;
}
function sort_completion(data)
{
let categories = {};
for (let entry in data) {
let cat = entry.category ?? " ";
categories[cat] ??= [];
push(categories[cat], entry);
}
return categories;
}
function val_str(val)
{
if (type(val) == "array")
return join(", ", val);
return val;
}
function helptext_list_str(cur, str)
{
let data = cur.value;
let categories = sort_completion(data);
let cat_len = max_len(keys(categories));
let has_categories = length(categories) > 1 || !categories[" "];
let len = max_len(map(data, (v) => v.name), 10);
if (has_categories || str == null)
str = "";
for (let cat, cdata in categories) {
if (has_categories && cat != " ") {
if (length(str) > 0)
str += "\n";
str += `${cat}:\n`;
}
for (let val in cdata) {
let name = val.name;
let help = val.help ?? "";
let extra = [];
if (val.multiple)
push(extra, "multiple");
if (val.required)
push(extra, "required");
if (val.default)
push(extra, "default: " + val_str(val.default));
if (length(extra) > 0)
help += " (" + join(", ", extra) + ")";
if (length(help) > 0)
name += ":";
str += sprintf(" %-" + len + "s %s\n", name, help);
}
}
return str;
}
function helptext(cur) {
if (!cur) {
el.set_hint(`\n No help information available\n`);
return true;
}
let str = `${cur.help}: `;
let data = cur.value;
if (type(data) != "array") {
str += `<${cur.type}>\n`;
} else if (length(data) > 0) {
str += "\n";
str = helptext_list_str(cur, str);
} else {
str += " (no match)\n";
}
el.set_hint(str);
return true;
}
function completion_ctx(arg_info)
{
let cur_ctx = ctx;
for (let args in arg_info.args) {
let sel = cur_ctx.select(args, true);
if (!length(args))
cur_ctx = sel;
if (type(sel) != "object" || sel.errors)
return;
}
return cur_ctx;
}
function completion_replace_arg(val, incomplete, skip_space)
{
let ref = substr(tab_prefix, -tab_prefix_len);
val = parser.escape(val, ref);
if (incomplete) {
let last = substr(val, -1);
if (last == '"' || last == "'")
val = substr(val, 0, -1);
} else if (!skip_space) {
val += " ";
}
let line = tab_prefix;
if (tab_prefix_len)
line = substr(tab_prefix, 0, -tab_prefix_len);
line += val;
let pos = length(line);
line += tab_suffix;
el.set_state({ line, pos });
}
function completion_check_prefix(data)
{
let prefix = data[0].name;
let prefix_len = length(prefix);
for (let entry in data) {
entry = entry.name;
if (prefix_len > length(entry))
prefix_len = length(entry);
}
prefix = substr(prefix, 0, prefix_len);
for (let entry in data) {
entry = substr(entry.name, 0, prefix_len);
while (entry != prefix) {
prefix_len--;
prefix = substr(prefix, 0, prefix_len);
entry = substr(entry, 0, prefix_len);
}
}
completion_replace_arg(prefix, true);
}
function completion(count) {
if (count < 2) {
let line_data = el.get_line();
let line = line_data.line;
let pos = line_data.pos;
tab_suffix = substr(line, pos);
if (length(tab_suffix) > 0 &&
substr(tab_suffix, 0, 1) != " ") {
let idx = index(tab_suffix, " ");
if (idx < 0 || !idx)
pos += length(tab_suffix);
else
pos += idx;
tab_suffix = substr(line, pos);
}
tab_prefix = substr(line, 0, pos);
let arg_info = parser.parse(tab_prefix);
let is_open = arg_info.missing != null;
if (arg_info.missing == "\\\"")
tab_quote = "\"";
else
tab_quote = arg_info.missing ?? "";
let args = pop(arg_info.args);
let arg_pos = pop(arg_info.pos);
if (!is_open && substr(tab_prefix, -1) == " ")
push(args, "");
let tab_arg_pos = arg_pos[length(args) - 1];
tab_arg = args[length(args) - 1];
if (tab_arg_pos)
tab_prefix_len = tab_arg_pos[1] - tab_arg_pos[0];
else
tab_prefix_len = 0;
tab_ctx = completion_ctx(arg_info);
if (!tab_ctx)
return;
cur_completion = tab_ctx.complete([...args]);
}
if (!tab_ctx)
return;
if (count < 0 || (cur_completion && cur_completion.force_helptext))
return helptext(cur_completion);
let cur = cur_completion;
if (!cur || !cur.value) {
if (!tab_prefix_len) {
el.set_hint("");
return;
}
cur = {
value: [{
name: tab_arg,
}]
};
}
let data = cur.value;
if (length(data) == 0) {
el.set_hint(` (no match)`);
return;
}
if (length(data) == 1) {
completion_replace_arg(data[0].name, data[0].incomplete);
el.set_hint("");
el.reset_key_input();
return;
}
if (count == 1)
completion_check_prefix(data);
if (count > 1) {
let idx = (count - 2) % length(data);
completion_replace_arg(data[idx].name, false, true);
}
let win = el.get_window();
let str = "";
let x = 0;
let categories = sort_completion(data);
let cat_len = max_len(keys(categories));
let len = max_len(map(data, (v) => v.name));
let has_categories = length(categories) > 1 || !categories[" "];
for (let cat, cdata in categories) {
let cat_start = cat != " ";
if (cat_start)
cat += ": ";
if (x) {
str += "\n";
x = 0;
}
for (let entry in cdata) {
let add;
if (!x && has_categories)
add = sprintf(" %-"+cat_len+"s", cat);
else
add = " ";
cat = "";
let name = entry.name;
if (entry.incomplete)
name += "...";
add += sprintf("%-"+len+"s", name);
str += add;
x += length(add);
if (x + length(add) < win.x)
continue;
str += "\n";
x = 0;
}
}
el.set_hint(str);
}
function format_entry(val)
{
if (type(val) == "bool")
val = val ? "yes" : "no";
return val;
}
function format_multiline(prefix, val)
{
let prefix2 = replace(prefix, /./g, " ");
let prefix_len = length(prefix);
let win = el.get_window();
let x = 0;
if (type(val) != "array")
val = [ val ];
if (length(val) == 0)
val = [ "<none>" ];
for (let cur in val) {
cur = format_entry(cur);
let cur_lines = split(cur, "\n");
if (length(cur_lines) > 1) {
if (x) {
warn(',\n');
x = 0;
}
cur = join("\n" + prefix2, cur_lines);
warn(cur);
x = win.x;
prefix = null;
continue;
}
if (x && (x + length(cur) > win.x - 3)) {
warn(',\n');
x = 0;
}
if (!x) {
warn(prefix ?? prefix2);
prefix = null;
x = prefix_len;
} else {
warn(', ');
x += 2;
}
warn(cur);
x += length(cur);
}
warn('\n');
}
function format_table(table)
{
let data = table;
let len = max_len(map(data, (v) => v[0]), 8);
for (let line in data) {
let name = line[0];
let val = line[1];
let prefix = sprintf(" %-" + len + "s ", name + ":");
format_multiline(prefix, val);
}
}
function convert_table(val)
{
if (type(val) == "array")
return val;
let data = [];
for (let name in sort(keys(val)))
push(data, [ name, val[name] ]);
return data;
}
function convert_multi_table(val)
{
if (type(val) != "array") {
let data = [];
for (let name in sort(keys(val)))
push(data, [ val[name], name ]);
val = data;
}
for (let line in val)
line[0] = convert_table(line[0]);
return val;
}
function format_result(res)
{
if (!res) {
warn(color_fg("red", "Unknown command") + "\n");
return;
}
if (!res.ok) {
for (let err in res.errors) {
warn(color_fg("red", "Error: "+ err.msg) + "\n");
}
if (!length(res.errors))
warn(color_fg("red", "Failed") + "\n");
return;
}
if (res.status_msg)
warn(color_fg("green", res.status_msg) + "\n");
if (res.name)
warn(res.name + ": ");
let data = res.data;
switch (res.type) {
case "multi_table":
data = convert_multi_table(data);
warn("\n");
for (let table in data) {
if (table[1])
warn("\n" + table[1] + ":\n");
format_table(table[0]);
warn("\n");
}
break;
case "table":
data = convert_table(data);
warn("\n");
format_table(data);
break;
case "list":
warn("\n");
for (let entry in data)
warn(" - " + entry + "\n");
break;
case "string":
warn(res.data + "\n");
break;
case "json":
warn(sprintf("%.J\n", res.data));
break;
}
}
function line_history_reset()
{
history_idx = -1;
history_edit = null;
cur_line = null;
}
function line_history(dir)
{
let min_idx = cur_line == null ? 0 : -1;
let new_idx = history_idx + dir;
if (new_idx < min_idx || new_idx >= length(history))
return;
let line = el.get_line().line;
let cur_history = history_edit ?? history;
if (history_idx == -1)
cur_line = line;
else if (cur_history[history_idx] != line) {
history_edit ??= [ ...history ];
history_edit[history_idx] = line;
cur_history = history_edit;
}
history_idx = new_idx;
if (history_idx < 0)
line = cur_line;
else
line = cur_history[history_idx];
let pos = length(line);
el.set_state({ line, pos });
}
let rev_search, rev_search_results, rev_search_index;
function reverse_search_update(line)
{
if (line) {
rev_search = line;
rev_search_results = filter(history, (l) => index(l, line) >= 0);
rev_search_index = 0;
}
let prompt = "reverse-search: ";
if (line && !length(rev_search_results))
prompt = "failing " + prompt;
el.set_state({
line2_prompt: prompt,
});
if (line && length(rev_search_results)) {
line = rev_search_results[0];
let pos = length(line);
el.set_state({ line, pos });
}
}
function reverse_search_reset() {
if (rev_search == null)
return;
rev_search = null;
rev_search_results = null;
rev_search_index = 0;
el.set_state({
line2_prompt: null
});
}
function reverse_search()
{
if (rev_search == null) {
reverse_search_update("");
return;
}
if (!length(rev_search_results))
return;
rev_search_index = (rev_search_index + 1) % length(rev_search_results);
let line = rev_search_results[rev_search_index];
let pos = length(line);
el.set_state({ line, pos });
}
function line_cb(line)
{
reverse_search_reset();
line_history_reset();
unshift(history, line);
let arg_info = parser.parse(line);
if (!arg_info)
return;
for (let cmd in arg_info.args) {
let orig_cmd = [ ...cmd ];
// convenience hack
if (cmd[0] == "cd" && cmd[1] == "..") {
shift(cmd);
cmd[0] = "up";
} else if (cmd[0] == "ls") {
let compl = ctx.complete([""]);
if (!compl)
continue;
warn(helptext_list_str(compl));
continue;
}
let cur_ctx = ctx.select(cmd);
if (type(cur_ctx) != "object" || cur_ctx.errors) {
format_result(cur_ctx);
break;
}
if (!length(cmd)) {
ctx = cur_ctx;
update_prompt();
continue;
}
try {
let res = cur_ctx.call(cmd);
format_result(res);
if (res && res.ctx) {
ctx = res.ctx;
update_prompt();
}
} catch (e) {
model.exception(e);
}
}
}
const cb = {
eof: () => { warn(`\n`); uloop.end(); },
line_check: (line) => parser.check(line) == null,
line2_cursor: () => {
reverse_search_reset();
return false;
},
line2_update: reverse_search_update,
key_input: (c, count) => {
try {
switch(c) {
case "?":
if (parser.check(el.get_line().line) != null)
return false;
completion(-1);
return true;
case "\t":
reverse_search_reset();
completion(count);
return true;
case '\x03':
if (count < 2) {
el.set_state({ line: "", pos: 0 });
} else if (ctx.prev) {
warn(`\n`);
let cur_ctx = ctx.select([ "main" ]);
if (cur_ctx && !cur_ctx.errors)
ctx = cur_ctx;
update_prompt();
} else {
warn(`\n`);
el.poll_stop();
uloop.end();
}
return true;
case "\x12":
reverse_search();
return true;
}
} catch (e) {
warn(`${e}\n${e.stacktrace[0].context}`);
}
},
cursor_up: () => {
try {
line_history(1);
} catch (e) {
el.set_hint(`${e}\n${e.stacktrace[0].context}`);
}
},
cursor_down: () => {
try {
line_history(-1);
} catch (e) {
el.set_hint(`${e}\n${e.stacktrace[0].context}`);
}
},
};
el = uline.new({
utf8: true,
cb,
key_input_list: [ "?", "\t", "\x03", "\x12" ]
});
if (SCRIPT_NAME != "cli") {
let cur_ctx = ctx.select([ basename(SCRIPT_NAME) ]);
if (cur_ctx && cur_ctx != ctx && !cur_ctx.errors) {
ctx = cur_ctx;
delete ctx.prev;
ctx.node.exit = model.node.Root.exit;
base_prompt = [];
}
}
while (length(ARGV) > 0) {
let cmd = ARGV;
let idx = index(ARGV, ":");
if (idx >= 0) {
cmd = slice(ARGV, 0, idx);
ARGV = slice(ARGV, idx + 1);
} else {
ARGV = [];
}
interactive ??= false;
let orig_cmd = [ ...cmd ];
let cur_ctx = ctx.select(cmd);
if (type(cur_ctx) != "object" || cur_ctx.errors) {
format_result(cur_ctx);
break;
}
if (!length(cmd)) {
ctx = cur_ctx;
continue;
}
let res = cur_ctx.call(cmd);
format_result(res);
}
if (script_mode) {
el.close();
while (!stdin.error()) {
let line = stdin.read("line");
line_cb(line);
}
exit(0);
}
if (interactive != false) {
warn("Welcome to the OpenWrt CLI. Press '?' for help on commands/arguments\n");
update_prompt();
el.set_uloop(line_cb);
uloop.run();
exit(0);
}