mirror of
https://github.com/cjdelisle/openwrt.git
synced 2025-10-07 11:19:56 +00:00
No-op for now, but allows making output more machine readable Signed-off-by: Felix Fietkau <nbd@nbd.name>
751 lines
14 KiB
Plaintext
Executable File
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);
|
|
}
|