cli: add OpenWrt CLI
This provides an easy to use modular CLI that can be used to interact with OpenWrt services. It has full support for context sensitive tab completion and help. Extra modules can be provided by packages and can extend the existing node structure in any place. Signed-off-by: Felix Fietkau <nbd@nbd.name>
This commit is contained in:
741
package/utils/cli/files/usr/sbin/cli
Executable file
741
package/utils/cli/files/usr/sbin/cli
Executable file
@@ -0,0 +1,741 @@
|
||||
#!/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;
|
||||
let script_mode;
|
||||
|
||||
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();
|
||||
},
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 = "";
|
||||
|
||||
add += sprintf("%-"+len+"s", entry.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 ];
|
||||
|
||||
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" ]
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
60
package/utils/cli/files/usr/share/ucode/cli/cache.uc
Normal file
60
package/utils/cli/files/usr/share/ucode/cli/cache.uc
Normal file
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
|
||||
'use strict';
|
||||
|
||||
const CACHE_DEFAULT_TIMEOUT = 5;
|
||||
|
||||
function cache_get(key, fn, timeout)
|
||||
{
|
||||
let now = time();
|
||||
let entry = this.entries[key];
|
||||
if (entry) {
|
||||
if (now < entry.timeout)
|
||||
return entry.data;
|
||||
|
||||
if (!fn)
|
||||
delete this.entries[key];
|
||||
}
|
||||
|
||||
if (!fn)
|
||||
return;
|
||||
|
||||
let data = fn();
|
||||
if (!entry)
|
||||
this.entries[key] = entry = {};
|
||||
timeout ??= CACHE_DEFAULT_TIMEOUT;
|
||||
entry.timeout = now + timeout;
|
||||
entry.data = data;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function cache_remove(key)
|
||||
{
|
||||
delete this.entries[key];
|
||||
}
|
||||
|
||||
function cache_gc() {
|
||||
let now = time();
|
||||
for (let key, entry in this.entries)
|
||||
if (now > entry.timeout)
|
||||
delete this.entries[key];
|
||||
}
|
||||
|
||||
const cache_proto = {
|
||||
get: cache_get,
|
||||
remove: cache_remove,
|
||||
gc: cache_gc,
|
||||
};
|
||||
|
||||
export function new(model) {
|
||||
model.cache_proto ??= { model, ...cache_proto };
|
||||
let cache = proto({
|
||||
entries: {},
|
||||
}, model.cache_proto);
|
||||
cache.gc_interval = model.uloop.interval(10000, () => {
|
||||
cache.gc();
|
||||
});
|
||||
|
||||
return cache;
|
||||
};
|
||||
65
package/utils/cli/files/usr/share/ucode/cli/color.uc
Normal file
65
package/utils/cli/files/usr/share/ucode/cli/color.uc
Normal file
@@ -0,0 +1,65 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
|
||||
'use strict';
|
||||
|
||||
const color_codes = {
|
||||
black: 30,
|
||||
red: 31,
|
||||
green: 32,
|
||||
yellow: 33,
|
||||
blue: 34,
|
||||
magenta: 35,
|
||||
cyan: 36,
|
||||
white: 37,
|
||||
default: 39
|
||||
};
|
||||
|
||||
function color_str(n)
|
||||
{
|
||||
return "\e["+n+"m";
|
||||
}
|
||||
|
||||
function color_code(str)
|
||||
{
|
||||
let n = 0;
|
||||
if (substr(str, 0, 7) == "bright_") {
|
||||
str = substr(str, 7);
|
||||
n += 60;
|
||||
}
|
||||
if (!color_codes[str])
|
||||
return;
|
||||
|
||||
n += color_codes[str];
|
||||
return n;
|
||||
}
|
||||
|
||||
export function color_fg(name, str)
|
||||
{
|
||||
let n = color_code(name);
|
||||
if (!n)
|
||||
return str;
|
||||
|
||||
let ret = color_str(n);
|
||||
if (str != null)
|
||||
ret += str + color_str(39);
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
export function color_bg(name, str)
|
||||
{
|
||||
let n = color_code(name);
|
||||
if (!n)
|
||||
return str;
|
||||
|
||||
let ret = color_str(n + 10);
|
||||
if (str != null)
|
||||
ret += str + color_str(49);
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
export function bold(str)
|
||||
{
|
||||
return color_str(1) + str + color_str(0);
|
||||
};
|
||||
126
package/utils/cli/files/usr/share/ucode/cli/context-call.uc
Normal file
126
package/utils/cli/files/usr/share/ucode/cli/context-call.uc
Normal file
@@ -0,0 +1,126 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
|
||||
'use strict';
|
||||
|
||||
function default_result()
|
||||
{
|
||||
return {
|
||||
errors: [],
|
||||
ok: false
|
||||
};
|
||||
}
|
||||
|
||||
function context_clone()
|
||||
{
|
||||
let ret = { ...this };
|
||||
ret.result = default_result();
|
||||
ret.data = { ...ret.data };
|
||||
return proto(ret, proto(this));
|
||||
}
|
||||
|
||||
function call_select(...args)
|
||||
{
|
||||
this.result.ctx = this.node_ctx.select(args);
|
||||
}
|
||||
|
||||
function call_ok(msg)
|
||||
{
|
||||
this.result.ok = true;
|
||||
if (msg)
|
||||
this.result.status_msg = msg;
|
||||
return true;
|
||||
}
|
||||
|
||||
function call_error(code, msg, ...args)
|
||||
{
|
||||
msg ??= "Unknown error";
|
||||
msg = sprintf(msg, ...args);
|
||||
let error = {
|
||||
code, msg, args
|
||||
};
|
||||
push(this.result.errors, error);
|
||||
}
|
||||
|
||||
function call_generic(ctx, name, type, val)
|
||||
{
|
||||
ctx.result.type = type;
|
||||
ctx.result.name = name;
|
||||
ctx.result.data = val;
|
||||
return ctx.ok();
|
||||
}
|
||||
|
||||
function call_multi_table(name, val)
|
||||
{
|
||||
return call_generic(this, name, "multi_table", val);
|
||||
}
|
||||
|
||||
function call_table(name, val)
|
||||
{
|
||||
return call_generic(this, name, "table", val);
|
||||
}
|
||||
|
||||
function call_list(name, val)
|
||||
{
|
||||
return call_generic(this, name, "list", val);
|
||||
}
|
||||
|
||||
function call_string(name, val)
|
||||
{
|
||||
return call_generic(this, name, "string", val);
|
||||
}
|
||||
|
||||
function call_json(name, val)
|
||||
{
|
||||
return call_generic(this, name, "json", val);
|
||||
}
|
||||
|
||||
function call_apply_defaults(named_args, args)
|
||||
{
|
||||
let entry = this.entry;
|
||||
named_args ??= entry.named_args;
|
||||
args ??= this.named_args;
|
||||
for (let name, arg in named_args)
|
||||
if (arg.default != null && !(name in args))
|
||||
args[name] ??= arg.default;
|
||||
}
|
||||
|
||||
export const callctx_error_proto = {
|
||||
missing_argument: function(msg, ...args) {
|
||||
return this.error("MISSING_ARGUMENT", msg ?? "Missing argument", ...args);
|
||||
},
|
||||
invalid_argument: function(msg, ...args) {
|
||||
return this.error("INVALID_ARGUMENT", msg ?? "Invalid argument", ...args);
|
||||
},
|
||||
unknown_error: function(msg, ...args) {
|
||||
return this.error("UNKNOWN_ERROR", msg ?? "Unknown error", ...args);
|
||||
},
|
||||
not_found: function(msg, ...args) {
|
||||
return this.error("NOT_FOUND", msg ?? "Not found", ...args);
|
||||
},
|
||||
command_failed: function(msg, ...args) {
|
||||
return this.error("COMMAND_FAILEDu", msg ?? "Command failed", ...args);
|
||||
},
|
||||
};
|
||||
|
||||
const callctx_proto = {
|
||||
clone: context_clone,
|
||||
select: call_select,
|
||||
apply_defaults: call_apply_defaults,
|
||||
ok: call_ok,
|
||||
list: call_list,
|
||||
table: call_table,
|
||||
multi_table: call_multi_table,
|
||||
string: call_string,
|
||||
json: call_json,
|
||||
|
||||
error: call_error,
|
||||
...callctx_error_proto,
|
||||
};
|
||||
|
||||
export function new(model, ctx) {
|
||||
let node_ctx = ctx;
|
||||
let data = ctx.data;
|
||||
model.callctx_proto ??= { model, ...callctx_proto };
|
||||
let result = default_result();
|
||||
return proto({ node_ctx, data, result }, model.callctx_proto);
|
||||
};
|
||||
679
package/utils/cli/files/usr/share/ucode/cli/context.uc
Normal file
679
package/utils/cli/files/usr/share/ucode/cli/context.uc
Normal file
@@ -0,0 +1,679 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
|
||||
'use strict';
|
||||
|
||||
import * as callctx from "cli.context-call";
|
||||
|
||||
function prefix_match(prefix, str, icase)
|
||||
{
|
||||
if (icase) {
|
||||
str = lc(str);
|
||||
prefix = lc(prefix);
|
||||
}
|
||||
return substr(str, 0, length(prefix)) == prefix;
|
||||
}
|
||||
|
||||
function context_clone()
|
||||
{
|
||||
let ret = { ...this };
|
||||
ret.prompt = [ ...ret.prompt ];
|
||||
ret.data = { ...ret.data };
|
||||
ret.hooks = {};
|
||||
return proto(ret, proto(this));
|
||||
}
|
||||
|
||||
function context_entries()
|
||||
{
|
||||
return keys(this.node)
|
||||
}
|
||||
|
||||
function context_help(entry)
|
||||
{
|
||||
if (entry)
|
||||
return this.node[entry].help;
|
||||
|
||||
let ret = {};
|
||||
for (let name, val in this.node)
|
||||
ret[name] = val.help ?? "";
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function context_add_hook(type, cb)
|
||||
{
|
||||
this.hooks[type] ??= [];
|
||||
push(this.hooks[type], cb);
|
||||
}
|
||||
|
||||
function context_select_error(code, msg, ...args)
|
||||
{
|
||||
msg ??= "Unknown error";
|
||||
msg = sprintf(msg, ...args);
|
||||
let error = {
|
||||
code, msg, args
|
||||
};
|
||||
this.errors ??= [];
|
||||
push(this.errors, error);
|
||||
}
|
||||
|
||||
function context_set(prompt, data)
|
||||
{
|
||||
if (prompt)
|
||||
this.cur_prompt = prompt;
|
||||
if (data)
|
||||
this.data = { ...this.data, ...data };
|
||||
return true;
|
||||
}
|
||||
|
||||
const context_select_proto = {
|
||||
add_hook: context_add_hook,
|
||||
error: context_select_error,
|
||||
set: context_set,
|
||||
...callctx.callctx_error_proto,
|
||||
};
|
||||
|
||||
function __context_select(ctx, name, args)
|
||||
{
|
||||
let entry = ctx.node[name];
|
||||
if (!entry || !entry.select_node)
|
||||
return;
|
||||
|
||||
let node = ctx.model.node[entry.select_node];
|
||||
if (!node)
|
||||
return;
|
||||
|
||||
let ret = proto(ctx.clone(), ctx.model.context_select_proto);
|
||||
ret.cur_prompt = name;
|
||||
ret.node = node;
|
||||
try {
|
||||
if (entry.select &&
|
||||
!call(entry.select, entry, ctx.model.scope, ret, args))
|
||||
ret.errors ??= [];
|
||||
} catch (e) {
|
||||
ctx.model.exception(e);
|
||||
return;
|
||||
}
|
||||
|
||||
push(ret.prompt, ret.cur_prompt);
|
||||
ret.prev = ctx;
|
||||
proto(ret, proto(ctx));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function context_run_hooks(ctx, name)
|
||||
{
|
||||
try {
|
||||
while (length(ctx.hooks[name]) > 0) {
|
||||
let hook = ctx.hooks[name][0];
|
||||
|
||||
let ret = call(hook, ctx, ctx.model.scope);
|
||||
if (!ret)
|
||||
return false;
|
||||
|
||||
shift(ctx.hooks.exit);
|
||||
}
|
||||
} catch (e) {
|
||||
ctx.model.exception(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function context_prev(ctx, skip_hooks)
|
||||
{
|
||||
if (!skip_hooks && !context_run_hooks(ctx, "exit"))
|
||||
return;
|
||||
return ctx.prev;
|
||||
}
|
||||
|
||||
function context_top(ctx, skip_hooks)
|
||||
{
|
||||
while (ctx && ctx.prev)
|
||||
ctx = context_prev(ctx, skip_hooks);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function prepare_spec(e, ctx, spec, argv)
|
||||
{
|
||||
if (type(spec) != "function")
|
||||
return spec;
|
||||
|
||||
return call(spec, e, ctx.model.scope, ctx, argv);
|
||||
}
|
||||
|
||||
function prepare_default(e, ctx, spec, argv, named_args)
|
||||
{
|
||||
if (type(spec) != "object" || type(spec.default) != "function")
|
||||
return;
|
||||
|
||||
try {
|
||||
spec.default = call(spec.default, e, ctx.model.scope, ctx, argv, named_args, spec);
|
||||
} catch (e) {
|
||||
model.exception(e);
|
||||
}
|
||||
}
|
||||
|
||||
function prepare_attr_spec(e, ctx, spec, argv, named_args)
|
||||
{
|
||||
if (type(spec) != "object")
|
||||
return spec;
|
||||
|
||||
let t = ctx.model.types[spec.type];
|
||||
if (t)
|
||||
spec = { ...t, ...spec };
|
||||
else
|
||||
spec = { ...spec };
|
||||
|
||||
prepare_default(e, ctx, spec, argv, named_args, spec);
|
||||
if (type(spec.value) == "function")
|
||||
try {
|
||||
spec.value = call(spec.value, e, ctx.model.scope, ctx, argv, named_args, spec);
|
||||
} catch (e) {
|
||||
ctx.model.exception(e);
|
||||
spec.value = [];
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
function parse_arg(ctx, name, spec, val)
|
||||
{
|
||||
let t;
|
||||
|
||||
if (val == null) {
|
||||
ctx.invalid_argument("Missing argument %s", name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type(spec) == "object" && spec.type)
|
||||
t = ctx.model.types[spec.type];
|
||||
if (!t) {
|
||||
ctx.invalid_argument("Invalid type in argument: %s", name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!t.parse)
|
||||
return val;
|
||||
|
||||
return call(t.parse, spec, ctx.model.scope, ctx, name, val);
|
||||
}
|
||||
|
||||
const context_defaults = {
|
||||
up: [ "Return to previous node", context_prev ],
|
||||
exit: [ "Return to previous node", context_prev ],
|
||||
main: [ "Return to main node", context_top ],
|
||||
};
|
||||
|
||||
const context_default_order = [ "up", "exit", "main" ];
|
||||
|
||||
function context_select(args, completion)
|
||||
{
|
||||
let ctx = this;
|
||||
|
||||
while (length(args) > completion ? 1 : 0) {
|
||||
let name = args[0];
|
||||
let entry = ctx.node[name];
|
||||
|
||||
if (!entry) {
|
||||
let e = context_defaults[name];
|
||||
if (!e)
|
||||
return ctx;
|
||||
|
||||
shift(args);
|
||||
ctx = e[1](ctx, completion);
|
||||
if (!ctx)
|
||||
return;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.select_node)
|
||||
return ctx;
|
||||
|
||||
let num_args = length(entry.args);
|
||||
if (completion && num_args + 1 >= length(args))
|
||||
return ctx;
|
||||
|
||||
shift(args);
|
||||
let argv = [];
|
||||
let parse_ctx = callctx.new(this.model, ctx);
|
||||
if (num_args > 0) {
|
||||
let cur_argv = slice(args, 0, num_args);
|
||||
for (let i = 0; i < num_args; i++) {
|
||||
let arg = shift(args);
|
||||
let spec = entry.args[i];
|
||||
|
||||
spec = prepare_attr_spec(entry, ctx, spec, cur_argv, {});
|
||||
if (arg != null)
|
||||
arg = parse_arg(parse_ctx, spec.name, spec, arg);
|
||||
|
||||
if (arg != null)
|
||||
push(argv, arg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (entry.no_subcommands && length(args) > 0)
|
||||
parse_ctx.invalid_argument("command %s does not support subcommands", name);
|
||||
|
||||
if (length(parse_ctx.result.errors) > 0) {
|
||||
ctx = ctx.clone();
|
||||
ctx.errors = parse_ctx.result.errors;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
ctx = __context_select(ctx, name, argv);
|
||||
if (type(ctx) != "object" || ctx.errors)
|
||||
break;
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function complete_named_params(ctx, entry, obj, name, argv, named_params)
|
||||
{
|
||||
let data = [];
|
||||
let empty = "";
|
||||
|
||||
if (substr(name, 0, 1) == "-") {
|
||||
empty = "-";
|
||||
name = substr(name, 1);
|
||||
}
|
||||
|
||||
let defaults = {};
|
||||
callctx.new(ctx.model, ctx).apply_defaults(obj, defaults);
|
||||
for (let cur_name in sort(keys(obj))) {
|
||||
let val = obj[cur_name];
|
||||
|
||||
if (!prefix_match(name, cur_name) || val.no_complete)
|
||||
continue;
|
||||
|
||||
if (empty && !(val.allow_empty ?? entry.allow_empty))
|
||||
continue;
|
||||
|
||||
if (!val.multiple && named_params[cur_name] != null)
|
||||
continue;
|
||||
|
||||
if (type(val.available) == "function" &&
|
||||
!call(val.available, val, ctx.model.scope, ctx, argv, named_params))
|
||||
continue;
|
||||
|
||||
val = {
|
||||
name: empty + cur_name,
|
||||
...val,
|
||||
};
|
||||
push(data, val);
|
||||
}
|
||||
|
||||
return {
|
||||
type: "keywords",
|
||||
name: "parameter",
|
||||
help: "Parameter name",
|
||||
value: data
|
||||
};
|
||||
}
|
||||
|
||||
function complete_param(e, ctx, cur, val, args, named_args)
|
||||
{
|
||||
cur = prepare_attr_spec(e, ctx, cur, args, named_args);
|
||||
|
||||
if (type(cur.value) == "object") {
|
||||
let ret = [];
|
||||
for (let key in sort(keys(cur.value)))
|
||||
if (prefix_match(val, key, cur.ignore_case))
|
||||
push(ret, {
|
||||
name: key,
|
||||
help: cur.value[key]
|
||||
});
|
||||
|
||||
cur.value = ret;
|
||||
return cur;
|
||||
}
|
||||
|
||||
if (type(cur.value) == "array") {
|
||||
cur.value = map(sort(filter(cur.value, (v) => prefix_match(val, v, cur.ignore_case))), (v) => ({ name: v }));
|
||||
return cur;
|
||||
}
|
||||
|
||||
let type_info = ctx.model.types[cur.type];
|
||||
if (!type_info || !type_info.complete)
|
||||
return cur;
|
||||
|
||||
cur.value = call(type_info.complete, cur, ctx.model.scope, ctx, val);
|
||||
|
||||
return cur;
|
||||
}
|
||||
|
||||
function complete_arg_list(e, ctx, arg_info, args, base_args, named_args)
|
||||
{
|
||||
let cur_idx = length(args) - 1;
|
||||
let cur = arg_info[cur_idx];
|
||||
let val;
|
||||
|
||||
for (let i = 0; i <= cur_idx; i++)
|
||||
val = shift(args);
|
||||
|
||||
return complete_param(e, ctx, cur, val, base_args, named_args);
|
||||
}
|
||||
|
||||
function handle_empty_param(entry, spec, name, argv, named_args)
|
||||
{
|
||||
if (substr(name, 0, 1) != "-")
|
||||
return;
|
||||
|
||||
name = substr(name, 1);
|
||||
let cur = spec[name];
|
||||
if (!cur)
|
||||
return;
|
||||
|
||||
if (cur.default == null &&
|
||||
!(cur.allow_empty ?? entry.allow_empty))
|
||||
return;
|
||||
|
||||
if (cur.required) {
|
||||
cur = { ...cur };
|
||||
prepare_default(e, ctx, cur, argv, named_args, cur);
|
||||
named_args[name] = cur.default;
|
||||
} else {
|
||||
named_args[name] = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function default_complete(ctx, args)
|
||||
{
|
||||
let num_args = length(this.args);
|
||||
let named_args = {};
|
||||
let cur_args;
|
||||
|
||||
if (length(args) <= num_args)
|
||||
return complete_arg_list(this, ctx, this.args, args, [ ...args ], named_args);
|
||||
|
||||
let spec = prepare_spec(this, ctx, this.named_args, args);
|
||||
if (!spec)
|
||||
return;
|
||||
|
||||
let base_args = slice(args, 0, num_args);
|
||||
for (let i = 0; i < num_args; i++)
|
||||
shift(args);
|
||||
|
||||
while (length(args) > 0) {
|
||||
let name = args[0];
|
||||
|
||||
if (length(args) == 1)
|
||||
return complete_named_params(ctx, this, spec, name, base_args, named_args);
|
||||
|
||||
shift(args);
|
||||
let cur = spec[name];
|
||||
if (!cur) {
|
||||
if (handle_empty_param(this, spec, name, base_args, named_args))
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cur.args) {
|
||||
named_args[name] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let val;
|
||||
let cur_spec = cur.args;
|
||||
if (type(cur_spec) != "array") {
|
||||
cur_spec = [{
|
||||
name,
|
||||
help: cur.help,
|
||||
...cur_spec
|
||||
}];
|
||||
named_args[name] = shift(args);
|
||||
val = [ named_args[name] ];
|
||||
} else {
|
||||
let num_args = length(cur_spec);
|
||||
let val = [];
|
||||
for (let i = 0; i < num_args; i++)
|
||||
push(val, shift(args));
|
||||
named_args[name] = val;
|
||||
}
|
||||
|
||||
if (!length(args))
|
||||
return complete_arg_list(this, ctx, cur_spec, val, base_args, named_args);
|
||||
}
|
||||
}
|
||||
|
||||
function context_complete(args)
|
||||
{
|
||||
let ctx = this.select(args, true);
|
||||
if (!ctx || ctx.errors)
|
||||
return;
|
||||
|
||||
if (ctx != this) {
|
||||
ctx = ctx.clone();
|
||||
ctx.skip_default_complete = true;
|
||||
}
|
||||
|
||||
if (length(args) > 1) {
|
||||
let name = shift(args);
|
||||
let entry = ctx.node[name];
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
try {
|
||||
if (!entry.available || call(entry.available, entry, ctx.model.scope, ctx, args))
|
||||
return call(entry.complete ?? default_complete, entry, ctx.model.scope, ctx, args);
|
||||
} catch (e) {
|
||||
this.model.exception(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let name = shift(args) ?? "";
|
||||
let prefix_len = length(name);
|
||||
let data = [];
|
||||
let default_data = {};
|
||||
for (let cur_name in sort(keys(ctx.node))) {
|
||||
let val = ctx.node[cur_name];
|
||||
|
||||
if (substr(cur_name, 0, prefix_len) != name)
|
||||
continue;
|
||||
|
||||
if (val.available && !call(val.available, val, ctx.model.scope, ctx, args))
|
||||
continue;
|
||||
|
||||
let cur = {
|
||||
name: cur_name,
|
||||
help: val.help,
|
||||
category: val.select_node ? "Object" : "Action",
|
||||
};
|
||||
if (context_defaults[cur_name])
|
||||
default_data[cur_name] = cur;
|
||||
else
|
||||
push(data, cur);
|
||||
}
|
||||
|
||||
for (let cur_name in context_default_order) {
|
||||
if (substr(cur_name, 0, prefix_len) != name)
|
||||
continue;
|
||||
|
||||
let val = default_data[cur_name];
|
||||
if (!val) {
|
||||
if (!ctx.prev || ctx.skip_default_complete)
|
||||
continue;
|
||||
val = {
|
||||
name: cur_name,
|
||||
help: context_defaults[cur_name][0],
|
||||
category: "Navigation",
|
||||
};
|
||||
}
|
||||
|
||||
push(data, val);
|
||||
}
|
||||
|
||||
return {
|
||||
type: "enum",
|
||||
name: "command",
|
||||
help: "Command",
|
||||
value: data
|
||||
};
|
||||
}
|
||||
|
||||
function context_call(args)
|
||||
{
|
||||
let ctx = this.select(args);
|
||||
if (!ctx || !length(args))
|
||||
return;
|
||||
|
||||
let name = shift(args);
|
||||
let entry = ctx.node[name];
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
if (!entry.call)
|
||||
return;
|
||||
|
||||
let named_args = {};
|
||||
let num_args = length(entry.args);
|
||||
let cur_argv = slice(args, 0, num_args);
|
||||
let argv = [];
|
||||
let skip = {};
|
||||
|
||||
ctx = callctx.new(this.model, ctx);
|
||||
ctx.entry = entry;
|
||||
ctx.named_args = named_args;
|
||||
|
||||
for (let i = 0; i < num_args; i++) {
|
||||
let arg = shift(args);
|
||||
let spec = entry.args[i];
|
||||
|
||||
spec = prepare_attr_spec(entry, ctx, spec, cur_argv, named_args);
|
||||
if (arg != null)
|
||||
arg = parse_arg(ctx, spec.name, spec, arg);
|
||||
|
||||
if (spec.required && !length(arg)) {
|
||||
if (spec.default)
|
||||
arg = spec.default;
|
||||
else
|
||||
ctx.missing_argument("Missing argument %d: %s", i + 1, spec.name);
|
||||
}
|
||||
|
||||
if (arg != null)
|
||||
push(argv, arg);
|
||||
}
|
||||
|
||||
let spec = prepare_spec(entry, ctx, entry.named_args, argv) ?? {};
|
||||
let defaults = {};
|
||||
ctx.apply_defaults(spec, defaults);
|
||||
while (length(args) > 0) {
|
||||
let name = shift(args);
|
||||
let cur = spec[name];
|
||||
try {
|
||||
if (cur && type(cur.available) == "function" &&
|
||||
!call(cur.available, cur, ctx.model.scope, ctx, argv, { ...defaults, ...named_args }))
|
||||
cur = null;
|
||||
} catch (e) {
|
||||
ctx.model.exception(e);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!cur) {
|
||||
if (handle_empty_param(entry, spec, name, argv, named_args))
|
||||
continue;
|
||||
ctx.invalid_argument("Invalid argument: %s", name);
|
||||
return ctx.result;
|
||||
}
|
||||
|
||||
if (!cur.args) {
|
||||
named_args[name] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let val;
|
||||
let cur_spec = cur.args;
|
||||
if (type(cur.args) == "array") {
|
||||
val = [];
|
||||
for (let spec in cur.args) {
|
||||
spec = prepare_attr_spec(entry, ctx, spec, argv, named_args);
|
||||
let cur = parse_arg(ctx, name, spec, shift(args));
|
||||
if (cur == null)
|
||||
return ctx.result;
|
||||
|
||||
push(val, cur);
|
||||
}
|
||||
} else {
|
||||
let spec = prepare_attr_spec(entry, ctx, cur.args, argv, named_args);
|
||||
val = parse_arg(ctx, name, spec, shift(args));
|
||||
if (val == null)
|
||||
return ctx.result;
|
||||
}
|
||||
if (cur.multiple) {
|
||||
named_args[name] ??= [];
|
||||
push(named_args[name], val);
|
||||
} else {
|
||||
named_args[name] = val;
|
||||
}
|
||||
}
|
||||
|
||||
for (let name, arg in spec) {
|
||||
if (!arg.required || named_args[name] != null)
|
||||
continue;
|
||||
|
||||
try {
|
||||
if (type(arg.available) == "function" &&
|
||||
!call(arg.available, arg, ctx.model.scope, ctx, argv, named_args))
|
||||
continue;
|
||||
} catch (e) {
|
||||
ctx.model.exception(e);
|
||||
continue;
|
||||
}
|
||||
|
||||
let spec = { ...arg };
|
||||
prepare_default(entry, ctx, spec, argv, named_args);
|
||||
if (spec.default != null)
|
||||
named_args[name] = spec.default;
|
||||
else
|
||||
ctx.missing_argument("Missing argument: %s", name);
|
||||
}
|
||||
|
||||
if (length(ctx.result.errors) > 0)
|
||||
return ctx.result;
|
||||
|
||||
if (entry.available && !call(entry.available, entry, ctx.model.scope, ctx))
|
||||
return ctx.result;
|
||||
|
||||
try {
|
||||
if (!entry.validate || call(entry.validate, entry, ctx.model.scope, ctx, argv, named_args))
|
||||
call(entry.call, entry, ctx.model.scope, ctx, argv, named_args);
|
||||
} catch (e) {
|
||||
this.model.exception(e);
|
||||
return;
|
||||
}
|
||||
return ctx.result;
|
||||
}
|
||||
|
||||
const context_proto = {
|
||||
clone: context_clone,
|
||||
entries: context_entries,
|
||||
help: context_help,
|
||||
select: context_select,
|
||||
call: context_call,
|
||||
complete: context_complete,
|
||||
add_hook: context_add_hook,
|
||||
};
|
||||
|
||||
export function new(model) {
|
||||
model.context_proto ??= {
|
||||
model,
|
||||
...context_proto
|
||||
};
|
||||
model.context_select_proto ??= {
|
||||
model,
|
||||
...context_select_proto
|
||||
};
|
||||
return proto({
|
||||
prompt: [],
|
||||
node: model.node.Root,
|
||||
hooks: {},
|
||||
data: {}
|
||||
}, model.context_proto);
|
||||
};
|
||||
175
package/utils/cli/files/usr/share/ucode/cli/datamodel.uc
Normal file
175
package/utils/cli/files/usr/share/ucode/cli/datamodel.uc
Normal file
@@ -0,0 +1,175 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
|
||||
'use strict';
|
||||
|
||||
import * as context from "cli.context";
|
||||
import * as cache from "cli.cache";
|
||||
import * as libubus from "ubus";
|
||||
import * as uloop from "uloop";
|
||||
import { glob, dirname } from "fs";
|
||||
let types = require("cli.types");
|
||||
|
||||
uloop.init();
|
||||
let ubus = libubus.connect();
|
||||
|
||||
function status_msg(msg)
|
||||
{
|
||||
if (this.cb.status_msg)
|
||||
call(this.cb.status_msg, this, this.scope, msg);
|
||||
}
|
||||
|
||||
function poll_key(keys, prompt)
|
||||
{
|
||||
if (!model.cb.poll_key)
|
||||
return;
|
||||
|
||||
if (prompt)
|
||||
warn(prompt);
|
||||
|
||||
while (1) {
|
||||
let key = lc(model.cb.poll_key());
|
||||
if (!key || key == "\x03")
|
||||
return;
|
||||
|
||||
if (index(keys, key) >= 0)
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
function merge_object(obj, add)
|
||||
{
|
||||
for (let name, entry in add)
|
||||
obj[name] = entry;
|
||||
}
|
||||
|
||||
function add_node(name, node)
|
||||
{
|
||||
let obj = this.node;
|
||||
|
||||
if (obj[name])
|
||||
merge_object(obj[name], node);
|
||||
else
|
||||
obj[name] = { ...node };
|
||||
|
||||
return obj[name];
|
||||
}
|
||||
|
||||
function add_nodes(add)
|
||||
{
|
||||
for (let name, val in add)
|
||||
this.add_node(name, val);
|
||||
}
|
||||
|
||||
function add_hook(name, val)
|
||||
{
|
||||
let obj = this.hooks;
|
||||
|
||||
if (type(val) == "function")
|
||||
val = [ val ];
|
||||
obj[name] ??= [];
|
||||
push(obj[name], ...val);
|
||||
}
|
||||
|
||||
function add_hooks(add)
|
||||
{
|
||||
for (let name, val in add)
|
||||
this.add_hook(name, val);
|
||||
}
|
||||
|
||||
function add_type(name, val)
|
||||
{
|
||||
this.type[name] = val;
|
||||
}
|
||||
|
||||
function add_types(add)
|
||||
{
|
||||
for (let name, val in add)
|
||||
this.add_type(name, val);
|
||||
}
|
||||
|
||||
function add_module(path)
|
||||
{
|
||||
if (substr(path, 0, 1) != "/")
|
||||
path = dirname(sourcepath()) + "/modules/" + path;
|
||||
|
||||
let mod;
|
||||
try {
|
||||
let fn = loadfile(path, {
|
||||
raw_mode: true,
|
||||
strict_declarations: true,
|
||||
});
|
||||
mod = call(fn, this, this.scope);
|
||||
} catch (e) {
|
||||
this.warn(`${e}\n${e.stacktrace[0].context}\nFailed to open module ${path}.\n`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function add_modules(path)
|
||||
{
|
||||
path ??= "*.uc";
|
||||
if (substr(path, 0, 1) != "/")
|
||||
path = dirname(sourcepath()) + "/modules/" + path;
|
||||
|
||||
for (let mod in glob(path))
|
||||
this.add_module(mod);
|
||||
}
|
||||
|
||||
function run_hook(name, ...args)
|
||||
{
|
||||
let hooks = this.hooks[name];
|
||||
if (!hooks)
|
||||
return;
|
||||
|
||||
for (let hook in hooks)
|
||||
call(hook, this, {}, ...args);
|
||||
}
|
||||
|
||||
function init()
|
||||
{
|
||||
this.run_hook("init");
|
||||
}
|
||||
|
||||
function context_new()
|
||||
{
|
||||
return context.new(this);
|
||||
}
|
||||
|
||||
function exception(e)
|
||||
{
|
||||
this.warn(`${e}\n${e.stacktrace[0].context}`);
|
||||
}
|
||||
|
||||
const data_proto = {
|
||||
warn, exception,
|
||||
poll_key,
|
||||
add_module,
|
||||
add_modules,
|
||||
add_node,
|
||||
add_nodes,
|
||||
add_type,
|
||||
add_types,
|
||||
add_hook,
|
||||
add_hooks,
|
||||
run_hook,
|
||||
init,
|
||||
status_msg,
|
||||
context: context_new,
|
||||
};
|
||||
|
||||
export function new(cb) {
|
||||
cb ??= {};
|
||||
let model = proto({
|
||||
libubus, ubus, uloop,
|
||||
cb,
|
||||
hooks: {},
|
||||
node: {
|
||||
Root: {}
|
||||
},
|
||||
warnings: {},
|
||||
types: { ...types },
|
||||
}, data_proto);
|
||||
model.scope = proto({ model }, global);
|
||||
model.cache = cache.new(model);
|
||||
return model;
|
||||
};
|
||||
138
package/utils/cli/files/usr/share/ucode/cli/modules/network.uc
Normal file
138
package/utils/cli/files/usr/share/ucode/cli/modules/network.uc
Normal file
@@ -0,0 +1,138 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
|
||||
'use strict';
|
||||
|
||||
import { time_format } from "cli.utils";
|
||||
|
||||
function get_interfaces()
|
||||
{
|
||||
let data = model.ubus.call("network.interface", "dump");
|
||||
if (!data)
|
||||
return {};
|
||||
|
||||
let ret = {};
|
||||
for (let iface in data.interface)
|
||||
ret[iface.interface] = iface;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function interface_validate(ctx, argv)
|
||||
{
|
||||
let name = argv[0];
|
||||
if (!name)
|
||||
return ctx.missing_argument("Missing argument: %s", "name");
|
||||
|
||||
if (index(get_interfaces(), name) < 0)
|
||||
return ctx.not_found("Interface not found: %s", name);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const interface_args = [
|
||||
{
|
||||
name: "interface",
|
||||
help: "Interface name",
|
||||
type: "enum",
|
||||
value: (ctx) => keys(get_interfaces())
|
||||
}
|
||||
];
|
||||
|
||||
function interface_status(data)
|
||||
{
|
||||
if (data.up)
|
||||
return "up";
|
||||
if (!data.autostart)
|
||||
return "down";
|
||||
if (!data.available)
|
||||
return "unavailable";
|
||||
return "pending";
|
||||
}
|
||||
|
||||
const Network = {
|
||||
list: {
|
||||
help: "List interfaces",
|
||||
call: function(ctx, argv) {
|
||||
return ctx.list("Interfaces", keys(get_interfaces()));
|
||||
}
|
||||
},
|
||||
reload: {
|
||||
help: "Reload network config",
|
||||
call: function(ctx, argv) {
|
||||
model.ubus.call("network", "reload");
|
||||
return ctx.ok("Configuration reloaded");
|
||||
}
|
||||
},
|
||||
restart: {
|
||||
help: "Restart interface",
|
||||
validate: interface_validate,
|
||||
args: interface_args,
|
||||
call: function(ctx, argv) {
|
||||
let name = shift(argv);
|
||||
model.ubus.call("network.interface."+name, "down");
|
||||
model.ubus.call("network.interface."+name, "up");
|
||||
return ctx.ok("Interface restarted");
|
||||
}
|
||||
},
|
||||
start: {
|
||||
help: "Start interface",
|
||||
validate: interface_validate,
|
||||
args: interface_args,
|
||||
call: function(ctx, argv) {
|
||||
let name = shift(argv);
|
||||
model.ubus.call("network.interface."+name, "up");
|
||||
return ctx.ok("Interface started");
|
||||
}
|
||||
},
|
||||
stop: {
|
||||
help: "Stop interface",
|
||||
validate: interface_validate,
|
||||
args: interface_args,
|
||||
call: function(ctx, argv) {
|
||||
let name = shift(argv);
|
||||
model.ubus.call("network.interface."+name, "down");
|
||||
return ctx.ok("Interface stopped");
|
||||
}
|
||||
},
|
||||
status: {
|
||||
help: "Interface status",
|
||||
args: interface_args,
|
||||
call: function(ctx, argv) {
|
||||
let name = shift(argv);
|
||||
let status = get_interfaces();
|
||||
if (!name) {
|
||||
let data = {};
|
||||
for (let iface, ifdata in status)
|
||||
data[iface] = interface_status(ifdata);
|
||||
|
||||
return ctx.table("Status", data);
|
||||
}
|
||||
|
||||
let ifdata = status[name];
|
||||
let data = {
|
||||
Status: interface_status(ifdata),
|
||||
};
|
||||
if (ifdata.up)
|
||||
data.Uptime = time_format(ifdata.uptime);
|
||||
|
||||
if (length(ifdata["ipv4-address"]) > 0)
|
||||
data.IPv4 = join(", ", map(ifdata["ipv4-address"], (v) => v.address + "/" + v.mask));
|
||||
if (length(ifdata["ipv6-address"]) > 0)
|
||||
data.IPv6 = join(", ", map(ifdata["ipv6-address"], (v) => v.address + "/" + v.mask));
|
||||
if (length(ifdata["dns-server"]) > 0)
|
||||
data.DNS = join(", ", ifdata["dns-server"]);
|
||||
if (length(ifdata["route"]) > 0)
|
||||
data.Routes = join(", ", map(ifdata["route"], (v) => (v.mask == 0 ? "Default" : `${v.target}/${v.mask}`) + ": " + v.nexthop));
|
||||
return ctx.table("Status", data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Root = {
|
||||
network: {
|
||||
help: "Network interface configuration",
|
||||
select_node: "Network",
|
||||
}
|
||||
};
|
||||
|
||||
model.add_nodes({ Root, Network });
|
||||
174
package/utils/cli/files/usr/share/ucode/cli/modules/service.uc
Normal file
174
package/utils/cli/files/usr/share/ucode/cli/modules/service.uc
Normal file
@@ -0,0 +1,174 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
|
||||
'use strict';
|
||||
|
||||
import { glob, access, basename } from "fs";
|
||||
|
||||
function get_services()
|
||||
{
|
||||
return model.cache.get("init_service_list", () => {
|
||||
let services = glob("/etc/init.d/*");
|
||||
services = filter(services, (v) => !system([ "grep", "-q", "start_service()", v ]));
|
||||
services = map(services, basename);
|
||||
return sort(services);
|
||||
});
|
||||
}
|
||||
|
||||
function get_service_status( name)
|
||||
{
|
||||
return model.ubus.call("service", "list", (name ? { name } : null));
|
||||
}
|
||||
|
||||
function service_running(name)
|
||||
{
|
||||
let status = get_service_status(name);
|
||||
return !!(status && status[name]);
|
||||
}
|
||||
|
||||
function __service_cmd(name, cmd)
|
||||
{
|
||||
return system([ "/etc/init.d/" + name, cmd ]) == 0;
|
||||
}
|
||||
|
||||
function service_cmd(ctx, name, cmd, msg)
|
||||
{
|
||||
if (__service_cmd(name, cmd))
|
||||
return ctx.ok(msg);
|
||||
else
|
||||
return ctx.command_failed("Command failed");
|
||||
}
|
||||
|
||||
function service_validate(ctx, argv)
|
||||
{
|
||||
let name = argv[0];
|
||||
if (!name)
|
||||
return ctx.missing_argument("Missing argument: %s", "name");
|
||||
|
||||
if (index(get_services(), name) < 0)
|
||||
return ctx.not_found("Service not found: %s", name);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const service_args = [
|
||||
{
|
||||
name: "name",
|
||||
help: "Service name",
|
||||
type: "enum",
|
||||
value: (ctx) => get_services()
|
||||
}
|
||||
];
|
||||
|
||||
const service_settings = {
|
||||
enabled: {
|
||||
help: "Service enabled at system boot",
|
||||
},
|
||||
disabled: {
|
||||
help: "Service disabled at system boot",
|
||||
}
|
||||
};
|
||||
|
||||
const SystemService = {
|
||||
list: {
|
||||
help: "List services",
|
||||
call: function(ctx, argv) {
|
||||
return ctx.list("Services", get_services());
|
||||
}
|
||||
},
|
||||
reload: {
|
||||
help: "Reload service",
|
||||
validate: service_validate,
|
||||
args: service_args,
|
||||
call: function(ctx, argv) {
|
||||
return service_cmd(ctx, shift(argv), "reload", "Service reloaded");
|
||||
}
|
||||
},
|
||||
restart: {
|
||||
help: "Restart service",
|
||||
validate: service_validate,
|
||||
args: service_args,
|
||||
call: function(ctx, argv) {
|
||||
return service_cmd(ctx, shift(argv), "restart", "Service restarted");
|
||||
}
|
||||
},
|
||||
set: {
|
||||
help: "Change service settings",
|
||||
validate: service_validate,
|
||||
args: service_args,
|
||||
named_args: service_settings,
|
||||
call: function(ctx, argv, param) {
|
||||
if (!length(param))
|
||||
return ctx.invalid_argument("No settings provided");
|
||||
|
||||
if (param.enabled && param.disabled)
|
||||
return ctx.invalid_argument("enabled and disabled cannot be set at the same time");
|
||||
|
||||
if (param.enabled && !__service_cmd(name, "enable"))
|
||||
ctx.command_failed("Command failed: %s", "enable");
|
||||
|
||||
if (param.disabled && !__service_cmd(name, "disable"))
|
||||
ctx.command_failed("Command failed: %s", "disable");
|
||||
|
||||
return ctx.ok("Settings changed");
|
||||
}
|
||||
},
|
||||
start: {
|
||||
help: "Start service",
|
||||
validate: service_validate,
|
||||
args: service_args,
|
||||
call: function(ctx, argv) {
|
||||
let name = shift(argv);
|
||||
|
||||
if (service_running(name))
|
||||
return ctx.invalid_argument("Service already running", name);
|
||||
|
||||
return service_cmd(ctx, name, "start", "Service started");
|
||||
}
|
||||
},
|
||||
stop: {
|
||||
help: "Stop service",
|
||||
validate: service_validate,
|
||||
args: service_args,
|
||||
call: function(ctx, argv) {
|
||||
let name = shift(argv);
|
||||
|
||||
if (!service_running(name))
|
||||
return ctx.invalid_argument("Service not running", name);
|
||||
|
||||
return service_cmd(ctx, name, "stop", "Service stopped");
|
||||
}
|
||||
},
|
||||
status: {
|
||||
help: "Service status",
|
||||
args: service_args,
|
||||
call: function(ctx, argv) {
|
||||
let name = shift(argv);
|
||||
if (!name) {
|
||||
let data = {};
|
||||
for (let service in get_services()) {
|
||||
let running = service_running(service);
|
||||
data[service] = running ? "running" : "not running";
|
||||
}
|
||||
return ctx.table("Status", data);
|
||||
}
|
||||
|
||||
if (index(get_services(), name) < 0)
|
||||
return ctx.not_found("Service not found: %s", name);
|
||||
|
||||
let data = {
|
||||
"Running": service_running(name),
|
||||
"Enabled": __service_cmd(name, "enabled"),
|
||||
};
|
||||
return ctx.table("Status", data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Root = {
|
||||
service: {
|
||||
help: "System service configuration",
|
||||
select_node: "SystemService",
|
||||
}
|
||||
};
|
||||
|
||||
model.add_nodes({ Root, SystemService });
|
||||
546
package/utils/cli/files/usr/share/ucode/cli/object-editor.uc
Normal file
546
package/utils/cli/files/usr/share/ucode/cli/object-editor.uc
Normal file
@@ -0,0 +1,546 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
|
||||
'use strict';
|
||||
|
||||
function __get_edit_object(ctx, entry, argv, name)
|
||||
{
|
||||
if (type(entry.edit_object) == "function")
|
||||
return call(entry.edit_object, entry, ctx.model.scope, ctx, argv);
|
||||
|
||||
if (name)
|
||||
return ctx.data.edit[name];
|
||||
|
||||
return ctx.data.edit;
|
||||
}
|
||||
|
||||
function get_edit_object(ctx, entry, argv, name)
|
||||
{
|
||||
let obj = __get_edit_object(ctx, entry, argv, name);
|
||||
if (!obj)
|
||||
ctx.invalid_argument();
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function get_param_object(ctx, obj, spec, argv)
|
||||
{
|
||||
if (type(spec.get_object) != "function")
|
||||
return obj;
|
||||
|
||||
return call(spec.get_object, spec, ctx.model.scope, ctx, spec, obj, argv);
|
||||
}
|
||||
|
||||
function call_change_cb(ctx, entry, argv, named)
|
||||
{
|
||||
if (!length(named) || type(entry.change_cb) != "function")
|
||||
return;
|
||||
|
||||
call(entry.change_cb, entry, ctx.model.scope, ctx, argv);
|
||||
}
|
||||
|
||||
function check_duplicate(ctx, val, new_val)
|
||||
{
|
||||
for (let i = 0; i < length(new_val); i++) {
|
||||
let v = new_val[i];
|
||||
if ((val && index(val, v) >= 0) ||
|
||||
(i && index(slice(new_val, 0, i), v) >= 0)) {
|
||||
ctx.invalid_argument("Duplicate value: %s", v);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function add_call(ctx, argv, named)
|
||||
{
|
||||
let spec = this.named_args;
|
||||
let obj = get_edit_object(ctx, this, argv);
|
||||
if (!obj)
|
||||
return;
|
||||
|
||||
for (let name, val in named) {
|
||||
let cur = spec[name];
|
||||
if (type(cur.add) == "function") {
|
||||
call(cur.add, cur, ctx.model.scope, ctx, val);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cur.attribute)
|
||||
name = cur.attribute;
|
||||
|
||||
let cur_obj = get_param_object(ctx, obj, cur, argv);
|
||||
cur_obj[name] ??= [];
|
||||
if (!cur.allow_duplicate &&
|
||||
check_duplicate(ctx, obj[name], val))
|
||||
return;
|
||||
push(cur_obj[name], ...val);
|
||||
}
|
||||
call_change_cb(ctx, this, argv, named);
|
||||
return ctx.ok();
|
||||
};
|
||||
|
||||
export function set_call(ctx, argv, named)
|
||||
{
|
||||
let spec = this.named_args;
|
||||
let obj = get_edit_object(ctx, this, argv);
|
||||
if (!obj)
|
||||
return;
|
||||
|
||||
for (let name, val in named) {
|
||||
let cur = spec[name];
|
||||
if (!cur)
|
||||
continue;
|
||||
|
||||
if (type(cur.set) == "function") {
|
||||
call(cur.set, cur, ctx.model.scope, ctx, val);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cur.attribute)
|
||||
name = cur.attribute;
|
||||
|
||||
let cur_obj = get_param_object(ctx, obj, cur, argv);
|
||||
if (val == null) {
|
||||
delete cur_obj[name];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cur.multiple && !cur.allow_duplicate &&
|
||||
check_duplicate(ctx, obj[name], val))
|
||||
return;
|
||||
cur_obj[name] = val;
|
||||
}
|
||||
call_change_cb(ctx, this, argv, named);
|
||||
return ctx.ok();
|
||||
};
|
||||
|
||||
export function remove_call(ctx, argv, named)
|
||||
{
|
||||
let spec = this.named_args;
|
||||
let obj = get_edit_object(ctx, this, argv);
|
||||
if (!obj)
|
||||
return;
|
||||
|
||||
for (let name, val in named) {
|
||||
let cur = spec[name];
|
||||
if (type(cur.remove) == "function") {
|
||||
call(cur.remove, cur, ctx.model.scope, ctx, val);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cur.attribute)
|
||||
name = cur.attribute;
|
||||
|
||||
let cur_obj = get_param_object(ctx, obj, cur, argv);
|
||||
let data = cur_obj[name];
|
||||
if (!data)
|
||||
continue;
|
||||
|
||||
for (let idx in val)
|
||||
data[+idx - 1] = null;
|
||||
|
||||
cur_obj[name] = filter(data, (v) => v != null);
|
||||
if (cur.attribute_allow_empty && !length(cur_obj[name]))
|
||||
delete cur_obj[name];
|
||||
}
|
||||
call_change_cb(ctx, this, argv, named);
|
||||
return ctx.ok();
|
||||
};
|
||||
|
||||
export function show_call(ctx, argv, named)
|
||||
{
|
||||
let obj = get_edit_object(ctx, this, argv);
|
||||
if (!obj)
|
||||
return;
|
||||
|
||||
let data = {};
|
||||
for (let name, spec in this.attribute_info) {
|
||||
let val;
|
||||
if (type(spec.get) == "function") {
|
||||
val = call(spec.get, spec, ctx.model.scope, ctx);
|
||||
} else {
|
||||
let cur_obj = get_param_object(ctx, obj, spec, argv);
|
||||
val = cur_obj[spec.attribute ?? name];
|
||||
}
|
||||
val ??= spec.default;
|
||||
|
||||
if (val != null)
|
||||
data[name] = val;
|
||||
}
|
||||
|
||||
return ctx.table("Values", data);
|
||||
};
|
||||
|
||||
function param_values(ctx, argv, named_args, spec)
|
||||
{
|
||||
let obj = get_edit_object(ctx, this, argv);
|
||||
if (!obj)
|
||||
return;
|
||||
|
||||
let values;
|
||||
if (type(spec.get) == "function")
|
||||
values = call(spec.get, spec, ctx.model.scope, ctx);
|
||||
else {
|
||||
let cur_obj = get_param_object(ctx, obj, spec, argv);
|
||||
values = cur_obj[spec.attribute];
|
||||
}
|
||||
|
||||
let ret = {};
|
||||
let idx = 0;
|
||||
for (let value in values)
|
||||
ret["" + (++idx)] = value;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function add_params(orig_params)
|
||||
{
|
||||
let params = {};
|
||||
|
||||
for (let name, val in orig_params) {
|
||||
if (!val.multiple)
|
||||
continue;
|
||||
|
||||
val = { ...val };
|
||||
delete val.required;
|
||||
delete val.allow_empty;
|
||||
params[name] = val;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function set_params(orig_params)
|
||||
{
|
||||
let params = {};
|
||||
|
||||
for (let name, val in orig_params) {
|
||||
val = { ...val };
|
||||
if (!val.required)
|
||||
val.allow_empty = true;
|
||||
else
|
||||
delete val.allow_empty;
|
||||
|
||||
delete val.required;
|
||||
params[name] = val;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function remove_params(orig_params)
|
||||
{
|
||||
let params = {};
|
||||
|
||||
for (let name, val in orig_params) {
|
||||
if (!val.multiple)
|
||||
continue;
|
||||
|
||||
val = { ...val };
|
||||
val.attribute_allow_empty = val.allow_empty;
|
||||
delete val.required;
|
||||
delete val.allow_empty;
|
||||
val.args = {
|
||||
type: "enum",
|
||||
attribute: val.attribute ?? name,
|
||||
value: param_values,
|
||||
force_helptext: true,
|
||||
};
|
||||
|
||||
params[name] = val;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export function new(info, node)
|
||||
{
|
||||
let params = info.named_args;
|
||||
let ret = {
|
||||
add: {
|
||||
help: "Add list parameter entries",
|
||||
args: info.args,
|
||||
named_args: add_params(params),
|
||||
call: add_call,
|
||||
edit_object: info.edit_object,
|
||||
change_cb: info.change_cb,
|
||||
...(info.add ?? {}),
|
||||
},
|
||||
show: {
|
||||
help: "Show parameter values",
|
||||
args: info.args,
|
||||
call: show_call,
|
||||
attribute_info: params,
|
||||
...(info.show ?? {}),
|
||||
},
|
||||
set: {
|
||||
help: "Set parameter values",
|
||||
args: info.args,
|
||||
named_args: set_params(params),
|
||||
call: set_call,
|
||||
edit_object: info.edit_object,
|
||||
change_cb: info.change_cb,
|
||||
...(info.set ?? {}),
|
||||
},
|
||||
remove: {
|
||||
help: "Remove parameter values",
|
||||
args: info.args,
|
||||
named_args: remove_params(params),
|
||||
call: remove_call,
|
||||
edit_object: info.edit_object,
|
||||
change_cb: info.change_cb,
|
||||
...(info.remove ?? {}),
|
||||
}
|
||||
};
|
||||
|
||||
if (!length(ret.add.named_args)) {
|
||||
delete ret.add;
|
||||
delete ret.remove;
|
||||
}
|
||||
|
||||
if (node)
|
||||
for (let cmd, val in ret)
|
||||
node[cmd] = val;
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
export function object_destroy_call(ctx, argv, named)
|
||||
{
|
||||
let type_name = argv[0];
|
||||
if (!type_name)
|
||||
return ctx.invalid_argument();
|
||||
|
||||
let info = this.object_info;
|
||||
let type_info = info.types[type_name];
|
||||
if (!type_info)
|
||||
return ctx.invalid_argument();
|
||||
|
||||
let obj_name = type_info.object ?? type_name;
|
||||
|
||||
let name = argv[1];
|
||||
if (type_info.delete)
|
||||
return call(type_info.delete, info, ctx.model.scope, ctx, type, name);
|
||||
|
||||
let obj = ctx.data.object_edit[obj_name];
|
||||
if (!obj)
|
||||
return ctx.unknown_error();
|
||||
|
||||
if (!obj[name])
|
||||
return ctx.not_found();
|
||||
|
||||
delete obj[name];
|
||||
|
||||
if (info.change_cb)
|
||||
call(info.change_cb, info, ctx.model.scope, ctx, argv);
|
||||
|
||||
return ctx.ok(`Deleted ${argv[0]} '${name}'`);
|
||||
};
|
||||
|
||||
const create_edit_param = {
|
||||
help: "Edit object after creating",
|
||||
};
|
||||
|
||||
export function object_create_params(node)
|
||||
{
|
||||
if (!node.show)
|
||||
return {};
|
||||
|
||||
let orig_params = node.show.attribute_info;
|
||||
let params = {};
|
||||
|
||||
for (let name, val in orig_params) {
|
||||
if (val.change_only)
|
||||
continue;
|
||||
|
||||
params[name] = val;
|
||||
}
|
||||
params.edit ??= create_edit_param;
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
export function object_create_call(ctx, argv, named)
|
||||
{
|
||||
let type_name = argv[0];
|
||||
if (!type_name)
|
||||
return ctx.invalid_argument();
|
||||
|
||||
let info = this.object_info;
|
||||
let type_info = info.types[type_name];
|
||||
if (!type_info)
|
||||
return ctx.invalid_argument();
|
||||
|
||||
let obj_name = type_info.object ?? type_name;
|
||||
|
||||
let name = argv[1];
|
||||
let obj, data;
|
||||
if (type_info.add) {
|
||||
data = call(type_info.add, info, ctx.model.scope, ctx, type, name);
|
||||
if (!data)
|
||||
return;
|
||||
} else {
|
||||
data = {};
|
||||
}
|
||||
|
||||
ctx.data.object_edit[obj_name] ??= {};
|
||||
obj = ctx.data.object_edit[obj_name];
|
||||
|
||||
let entry = type_info.node.set;
|
||||
if (entry) {
|
||||
ctx.apply_defaults();
|
||||
let subctx = ctx.clone();
|
||||
subctx.data.edit = data;
|
||||
|
||||
try {
|
||||
call(entry.call, entry, ctx.model.scope, subctx, argv, named);
|
||||
} catch (e) {
|
||||
ctx.model.exception(e);
|
||||
return ctx.unknown_error();
|
||||
}
|
||||
|
||||
if (!subctx.result.ok) {
|
||||
ctx.result = subctx.result;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
obj[name] = data;
|
||||
|
||||
if (named.edit)
|
||||
ctx.select(type_name, name);
|
||||
|
||||
return ctx.ok(`Added ${type_name} '${name}'`);
|
||||
};
|
||||
|
||||
function object_lookup(ctx, entry, type_name)
|
||||
{
|
||||
let info = entry.object_info;
|
||||
let type_info = info.types[type_name];
|
||||
if (!type_info)
|
||||
return [];
|
||||
|
||||
let obj_name = type_info.object ?? type_name;
|
||||
|
||||
return ctx.data.object_edit[obj_name];
|
||||
}
|
||||
|
||||
function object_values(ctx, entry, type_name)
|
||||
{
|
||||
let obj = object_lookup(ctx, entry, type_name);
|
||||
if (!obj)
|
||||
return [];
|
||||
|
||||
return keys(obj);
|
||||
}
|
||||
|
||||
export function object_list_call(ctx, argv, named)
|
||||
{
|
||||
return ctx.list(argv[0] + " list", object_values(ctx, this, argv[0]));
|
||||
};
|
||||
|
||||
export function edit_create_destroy(info, node)
|
||||
{
|
||||
let type_arg = {
|
||||
name: "type",
|
||||
help: "Type",
|
||||
type: "enum",
|
||||
required: true,
|
||||
value: keys(info.types),
|
||||
};
|
||||
let name_arg = {
|
||||
name: "name",
|
||||
help: "Name",
|
||||
type: "string",
|
||||
required: true,
|
||||
};
|
||||
let delete_name_arg = {
|
||||
...name_arg,
|
||||
type: "enum",
|
||||
value: function(ctx, argv) {
|
||||
return object_values(ctx, this, argv[0]);
|
||||
}
|
||||
};
|
||||
|
||||
let create_params = {};
|
||||
for (let name, val in info.types)
|
||||
create_params[name] = object_create_params(val.node);
|
||||
|
||||
let types_info = " (" + join(", ", keys(info.types)) + ")";
|
||||
let cmds = {
|
||||
destroy: {
|
||||
object_info: info,
|
||||
help: "Delete object" + types_info,
|
||||
args: [ type_arg, delete_name_arg ],
|
||||
call: object_destroy_call,
|
||||
},
|
||||
list: {
|
||||
object_info: info,
|
||||
help: "List objects" + types_info,
|
||||
args: [ type_arg ],
|
||||
call: object_list_call,
|
||||
},
|
||||
create: {
|
||||
object_info: info,
|
||||
help: "Create object" + types_info,
|
||||
args: [ type_arg, name_arg ],
|
||||
type_params: create_params,
|
||||
named_args: function(ctx, argv) {
|
||||
if (!argv[0])
|
||||
return;
|
||||
return this.type_params[argv[0]];
|
||||
},
|
||||
call: object_create_call,
|
||||
},
|
||||
};
|
||||
|
||||
for (let name, val in info.types) {
|
||||
cmds[name] = {
|
||||
object_name: name,
|
||||
object_info: info,
|
||||
help: "Edit " + name,
|
||||
args: [
|
||||
{
|
||||
...name_arg,
|
||||
type: "enum",
|
||||
value: function(ctx, argv) {
|
||||
return object_values(ctx, this, this.object_name);
|
||||
}
|
||||
}
|
||||
],
|
||||
select_node: val.node_name,
|
||||
select: function(ctx, argv) {
|
||||
let name = argv[0];
|
||||
if (!name) {
|
||||
warn(`Missing argument\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
let obj = object_lookup(ctx, this, this.object_name);
|
||||
if (!obj) {
|
||||
warn(`Object not found\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
let entry = obj[name];
|
||||
if (!entry) {
|
||||
warn(`${name} not found\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
let info = this.object_info;
|
||||
let type_info = info.types[this.object_name];
|
||||
return ctx.set(`${this.object_name} "${name}"`, {
|
||||
name,
|
||||
edit: entry,
|
||||
object_edit: entry,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (node)
|
||||
for (let cmd, val in cmds)
|
||||
node[cmd] = val;
|
||||
|
||||
return cmds;
|
||||
};
|
||||
182
package/utils/cli/files/usr/share/ucode/cli/types.uc
Normal file
182
package/utils/cli/files/usr/share/ucode/cli/types.uc
Normal file
@@ -0,0 +1,182 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
|
||||
'use strict';
|
||||
|
||||
import { access, basename, dirname, opendir, stat } from "fs";
|
||||
|
||||
function is_directory(path)
|
||||
{
|
||||
let s = stat(path);
|
||||
return s && s.type == "directory";
|
||||
}
|
||||
|
||||
const types = {
|
||||
bool: {
|
||||
value: [ "0", "1" ],
|
||||
parse: function(ctx, name, val) {
|
||||
if (val == "1")
|
||||
return true;
|
||||
if (val == "0")
|
||||
return false;
|
||||
ctx.invalid_argument("value for %s must be 0 or 1", name);
|
||||
return;
|
||||
},
|
||||
},
|
||||
int: {
|
||||
parse: function(ctx, name, strval) {
|
||||
let val = +strval;
|
||||
if (substr(strval, 0, 1) == "-")
|
||||
strval = substr(strval, 1);
|
||||
if (match(strval, /[^0-9]/)) {
|
||||
ctx.invalid_argument("value for %s is not a number", name);
|
||||
return;
|
||||
}
|
||||
if ((this.min == null || val >= this.min) &&
|
||||
(this.max == null || val <= this.max))
|
||||
return val;
|
||||
if (this.min != null && this.max != null)
|
||||
ctx.invalid_argument(`value for %s must be between ${this.min} and ${this.max}`, name);
|
||||
else if (this.min != null)
|
||||
ctx.invalid_argument(`value for %s must be at least ${this.min}`, name);
|
||||
else
|
||||
ctx.invalid_argument(`value for %s must not be bigger than ${this.max}`, name);
|
||||
return;
|
||||
}
|
||||
},
|
||||
string: {
|
||||
parse: function(ctx, name, val) {
|
||||
let len = length(val);
|
||||
if ((this.min == null || len >= this.min) &&
|
||||
(this.max == null || len <= this.max))
|
||||
return val;
|
||||
if (this.min != null && this.max != null)
|
||||
ctx.invalid_argument(`String value %s must be between ${this.min} and ${this.max} characters`, name);
|
||||
else if (this.min != null)
|
||||
ctx.invalid_argument(`String value %s must be at least ${this.min} characters long`, name);
|
||||
else
|
||||
ctx.invalid_argument(`String value %s must not be longer than ${this.max} characters`, name);
|
||||
return;
|
||||
}
|
||||
},
|
||||
enum: {
|
||||
parse: function(ctx, name, val) {
|
||||
if (this.no_validate)
|
||||
return val;
|
||||
|
||||
let list = this.value;
|
||||
if (this.ignore_case) {
|
||||
val = lc(val);
|
||||
val = filter(list, (v) => val == lc(v))[0];
|
||||
} else {
|
||||
if (index(list, val) < 0)
|
||||
val = null;
|
||||
}
|
||||
|
||||
if (val == null)
|
||||
ctx.invalid_argument("Invalid value for %s", name);
|
||||
|
||||
return val;
|
||||
}
|
||||
},
|
||||
path: {
|
||||
complete: function(ctx, val) {
|
||||
let ret = [];
|
||||
|
||||
let dir = split(val, "/");
|
||||
let prefix = pop(dir);
|
||||
push(dir, "");
|
||||
dir = join("/", dir);
|
||||
let prefix_len = length(prefix);
|
||||
let d = opendir(length(dir) ? dir : ".");
|
||||
if (!d)
|
||||
return ret;
|
||||
|
||||
let cur;
|
||||
while (cur = d.read()) {
|
||||
if (cur == "." || cur == "..")
|
||||
continue;
|
||||
|
||||
if (substr(cur, 0, prefix_len) != prefix)
|
||||
continue;
|
||||
|
||||
let path = dir + cur;
|
||||
let incomplete = false;
|
||||
if (is_directory(path)) {
|
||||
path += "/";
|
||||
incomplete = true;
|
||||
}
|
||||
|
||||
push(ret, { name: path, incomplete });
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
parse: function(ctx, name, val) {
|
||||
if (this.new_path) {
|
||||
let dir = dirname(val);
|
||||
let s = stat(dir);
|
||||
if (!is_directory(dir)) {
|
||||
ctx.invalid_argument("Path '%s' is not a directory", dir);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!access(val, "r")) {
|
||||
ctx.invalid_argument("Path '%s' does not exist", val);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
},
|
||||
host: {
|
||||
parse: function(ctx, name, val) {
|
||||
if (length(iptoarr(val)) != 0)
|
||||
return val;
|
||||
if (length(val) > 255)
|
||||
return;
|
||||
let labels = split(val, ".");
|
||||
if (length(filter(labels, label => !match(label, /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])$/))) == 0 && length(labels) > 0)
|
||||
return val;
|
||||
ctx.invalid_argument("value for %s is not an valid IP or hostname", name);
|
||||
return;
|
||||
}
|
||||
},
|
||||
macaddr: {
|
||||
parse: function(ctx, name, val) {
|
||||
val = lc(val);
|
||||
let arr = split(val, ":");
|
||||
if (length(arr) != 6 || length(filter(arr, (v) => !match(v, /^[0-9a-f][0-9a-f]$/))))
|
||||
return ctx.invalid_argument("value for %s is not an MAC address", name);
|
||||
return val;
|
||||
}
|
||||
},
|
||||
ipv4: {
|
||||
parse: function(ctx, name, val) {
|
||||
if (length(iptoarr(val)) == 4)
|
||||
return val;
|
||||
ctx.invalid_argument("value for %s is not an IPv4", name);
|
||||
return;
|
||||
}
|
||||
},
|
||||
ipv6: {
|
||||
parse: function(ctx, name, val) {
|
||||
if (length(iptoarr(val)) == 16)
|
||||
return val;
|
||||
ctx.invalid_argument("value for %s is not an IPv6", name);
|
||||
return;
|
||||
}
|
||||
},
|
||||
cidr4: {
|
||||
parse: function(ctx, name, val) {
|
||||
let m = split(val, '/', 2);
|
||||
if (m && +m[1] <= 32 &&
|
||||
((m[0] == "auto" && this.allow_auto) ||
|
||||
length(iptoarr(m[0])) == 4))
|
||||
return val;
|
||||
ctx.invalid_argument("value for %s is not cidr4 (e.g. 192.168.1.1/24)", name);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return types;
|
||||
26
package/utils/cli/files/usr/share/ucode/cli/utils.uc
Normal file
26
package/utils/cli/files/usr/share/ucode/cli/utils.uc
Normal file
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
|
||||
'use strict';
|
||||
|
||||
export function time_format(val)
|
||||
{
|
||||
let ret = `${val % 60}s`;
|
||||
|
||||
val /= 60;
|
||||
if (!val)
|
||||
return ret;
|
||||
|
||||
ret = `${val % 60}m ${ret}`;
|
||||
|
||||
val /= 60;
|
||||
if (!val)
|
||||
return ret;
|
||||
|
||||
ret = `${val % 24 }h ${ret}`;
|
||||
|
||||
val /= 24;
|
||||
if (!val)
|
||||
return ret;
|
||||
|
||||
return `${val}d ${ret}`;
|
||||
};
|
||||
Reference in New Issue
Block a user