| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750 |
- #!/usr/bin/env ucode
- // SPDX-License-Identifier: GPL-2.0-or-later
- // Copyright (C) 2025 Felix Fietkau <[email protected]>
- '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);
- }
|