Felix Fietkau 248b66b44f 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>
2025-02-13 19:00:30 +01:00

14 KiB

Design of the cli module API

Structure:

The cli is organized as a set of nodes, which are ucode objects objects describing entries. Each entry can either implement a command, or select another node, optionally with parameters. Additionally, it contains helptext and full parameter descriptions, including everything needed for tab completion. The initial node on startup is Root, representing the main menu.

Simple example:

Code:

const Example = {
	hello: {
		help: "Example command",
		args: [
			{
				name: "name",
				type: "string",
				min: 3,
				max: 16,
				required: true,
			}
		],
		call: function(ctx, argv, named) {
			return ctx.ok("Hello, " + argv[0]);
		},
	},
	hello2: {
		help: "Example command (named_args version)",
		named_args: {
			name: {
				required: true,
				args: {
					type: "string",
					min: 3,
					max: 16,
				}
			}
		},
		call: function(ctx, argv, named) {
			return ctx.ok("Hello, " + named.name);
		},
	}
};

const Root = {
	example: {
		help: "Example node",
		select_node: "Example",
	}
};

model.add_nodes({ Root, Example });

Example interaction:

root@OpenWrt:~# cli
Welcome to the OpenWrt CLI. Press '?' for help on commands/arguments
cli> example 
cli example> hello
Error: Missing argument 1: name
cli example> hello foo
Hello, foo
cli example> hello2
Error: Missing argument: name
cli example> hello2 name foo2
Hello, foo2
cli example> 

API documentation:

Each module is placed in /usr/share/ucode/cli/modules on the root filesystem. When included by the cli code, the scope contains the model variable, which is the main cli API object. This variable is also present in the scope of the other callback functions described below.

model methods:

  • model.warn(msg): Pass a warning to the user (similar to the ucode warn function).
  • model.exception(e): Print an exception with stack trace.
  • model.add_module(path): Load a single module from path
  • model.add_modules(path): Load multiple modules from the path wildcard pattern
  • model.add_node(name, obj): Add a single node under the given name
  • model.add_nodes(nodes): Add multiple nodes with taking name and obj from the nodes object.
  • model.add_type(name, info): Add a data type with validation information,
  • model.add_types(types): Add multiple data types, taking name and info from the types object.
  • model.status_msg(msg): Print an asynchronous status message (should not be used from within a node call or select function).

Properties of an entry inside a node:

Each entry must have at least help and either call or select_node set.

  • help: Helptext describing the command
  • call: function(ctx, argv, named): main command handler function of the entry.
    • this: pointer to the entry
    • ctx: call context object (see below)
    • argv: array of positional arguments after the command name
    • named: object of named parameters passed to the command
    • Return value: either ctx.ok(msg) for successfull calls, or the result of an error function (see below).
  • select_node: (string) name of the node that this entry points to. Mutually exclusive with implementing call.
  • select: function(ctx, argv, named): function for selecting another node.
    • this: pointer to the entry
    • ctx: node context object (see below)
    • argv, named: see call
    • Return value: either ctx.set(prompt, data), true, or the result of an error function (see below).
  • args: array of positional arguments (see argument property description)
  • named_args: object of named parameters (see parameter property description)
  • available: function(ctx): function indicating if the entry can be used (affects tab completion and running commands)
    • this: pointer to the entry
    • ctx: node context object (see below)
    • Return value: true if available, false otherwise.
  • validate: function (ctx, argv, named): validate command arguments
    • Function parameters: see call

Named parameter properties:

  • help: Description of the named parameter's purpose
  • args: Either an array of argument objects, or an object with a single argument (see below). If not set, paramter will not take any arguments, and its value will be true if the parameter was specified on the command line.
  • available: function(ctx, argv, named): function indicating if the named parameter can be used (affects tab completion and argument validation). May depend on arguments/parameters specified before this one.
  • multiple (bool): indicates if an argument may be specified multiple times. Turns the value in named into an array.
  • required (bool): Parameter must be specified for the command
  • default: default value for the parameter.
  • allow_empty: empty values are allowed and can be specified on the command line using -param_name instead of param_name. The value in the named object will be null in that case.

Positional argument properties:

  • name: Short name of the argument
  • help: Longer description of the argument (used in helptext/completion)
  • type: data type name (see below)
  • required (bool): Value must not be empty
  • value: possible values for tab completion, one of:
    • array of objects with the following contents:
      • name: value string
      • help: help text for this value
    • function(ctx, argv, named) returning the above.
  • extra properties specific to the data type (see below)

Default data types:

  • int: Integer value. The valid range can be specified using the min and max properties.
  • string: String value. The valid string length can be specified using the min and max properties.
  • bool: Boolean value. Converts "1" and "0" to true and false
  • enum: String value that must match one entry of the list provided via the value property. Case-insensitive match can be enabled using the ignore_case property.
  • path: Local filesystem path. When the new_path property is set, only match directories for a file to be created.
  • host: Host name or IP address
  • macaddr: MAC address
  • ipv4: IPv4 address
  • ipv6: IPv6 address
  • cidr4: IPv4 address with netmask size, e.g. 192.168.1.1/24. Allows auto as value if the allow_auto property is set.

call context:

Passed as ctx argument to entry call functions.

  • ctx.data: Object containing any data passed via ctx.set() from a select context.
  • ctx.ok(msg): Indicates successful call, passes the message msg to the user.
  • ctx.select(...args): After completion, switch to a different node by running the command chain provided as function argument (only entries with .select_node are supported).
  • ctx.string(name, val): Passes a string to the caller as return value.
  • ctx.list(name, val): Passes a list of values to the caller as return value. val must be an array.
  • ctx.table(name, val): Passes a table as value to the caller. val can be an array [ column_1, column_2 ], where each member of the outer array describes a row in the table. It can also be an object, where the property name is the first column value, and the value the second column value.
  • ctx.multi_table(name, val): Passes multiple tables to the caller. Can be an array of [ title, table ], or an object.
  • Error functions (see below)

select context:

  • ctx.data: Object containing any data passed via parent ctx.set calls.
  • ctx.set(prompt, data): Modify the prompt and ctx.data for the child context. The string given in prompt is appended to the existing prompt. The data given in the data object is merged with the previous ctx.data value.
  • Error functions (see below)

Error functions:

All error messages accept a format string in msg, with arguments added after it.

  • ctx.invalid_argument(msg, ...args): Indicates that invalid arguments were provided.
  • ctx.missing_argument(msg, ...args): Indicates that an expected argument was missing.
  • ctx.command_failed(msg, ...args): Indicates that the command failed.
  • ctx.not_found(msg, ...args): Indicates that a given entry was not found.
  • ctx.unknown_error(msg, ...args): Indicates that the command failed for unknown or unspecified reasons.
  • ctx.error(id, msg, ...args): Generic error message with id specifying a machine readable error type string.

Editor API documentation

The editor API provides a layer of abstraction above node entries/calls in order to make it easy to edit properties of an object based on an attribute list, as well as create/destroy/show object instances using a consistent user interface.

Simple example:

import * as editor from "cli.object-editor";

let changed = false;
let data = {
	things: {
		foo: {
			label_str: [ "bar" ],
			id: 31337,
		}
	},
};

const thing_editor = {
	change_cb: function(ctx) {
		changed = true;
	},
	named_args: {
		label: {
			help: "Thing label",
			attribute: "label_str",
			multiple: true,
			args: {
				type: "string",
				min: 2,
				max: 16
			},
		},
		id: {
			help: "Thing id",
			required: true,
			args: {
				type: "int",
				min: 1,
			},
		},
	},
};
const ExampleThing = editor.new(thing_editor);

let Example = {
	dump: {
		help: "Dump current data",
		call: function(ctx, argv, named) {
			return ctx.json("Data", {
				changed,
				data
			});
		},
	}
};
const example_editor = {
	change_cb: function(ctx) {
		changed = true;
	},
	types: {
		thing: {
			node_name: "ExampleThing",
			node: ExampleThing,
			object: "things",
		},
	},
};
editor.edit_create_destroy(example_editor, Example);

const Root = {
	example: {
		help: "Example node",
		select_node: "Example",
		select: function(ctx, argv, named) {
			return ctx.set(null, {
				object_edit: data,
			});
		}
	}
};

model.add_nodes({ Root, Example, ExampleThing });

Example interaction:

root@OpenWrt:~# cli
Welcome to the OpenWrt CLI. Press '?' for help on commands/arguments
cli> example
cli example> dump
Data: {
        "changed": false,
        "data": {
                "things": {
                        "foo": {
                                "label_str": [
                                        "bar"
                                ],
                                "id": 31337
                        }
                }
        }
}
cli example> thing foo set id 1337
cli example> create thing bar id 168 label l1 label l2
Added thing 'bar'
cli example> thing bar show
Values: 
    id:         168
    label:      l1, l2
cli example> thing bar remove label 1
cli example> thing bar show
Values: 
    id:         168
    label:      l2
cli example> dump
Data: {
        "changed": true,
        "data": {
                "things": {
                        "foo": {
                                "label_str": [
                                        "bar"
                                ],
                                "id": 1337
                        },
                        "bar": {
                                "id": 168,
                                "label_str": [
                                        "l2"
                                ]
                        }
                }
        }
}
cli example> destroy thing foo 
Deleted thing 'foo'
cli example> 

API documentation

Prelude: import * as editor from "cli.object-editor";

Object editor:

For editing an object, the following user commands are defined:

  • set: Changes property values
  • show Show all values

If properties with multiple: true are defined, the following commands are also defined:

  • add: Add values to properties
  • remove Remove specific values from properties
Variant 1 (editor-only node):

const Node = editor.new(editor_data)

Variant 2 (merge with existing entries):

let Node = {}; editor.new(editor_data, Node);

The editor code assumes that the node that selects the editor node uses ctx.set() to set the edit field in ctx.data to the object being edited.

editor_data properties:

  • change_cb: function(ctx): Called whenever a property is changed by the user
  • named_args: Parameters for editing properties (based on entry named_args, see below)
  • add, set, show, remove: Object for overriding fields of the commands defined by the editor. Primarily used to override the helptext.

Instance editor named_args entry properties:

All entry named_args properties are supported, but the meaning is extended slightly:

  • multiple: Property array values can be added/removed
  • default: Default value when creating the object
  • allow_empty: Property can be deleted
  • required: Property is mandatory in the object.

Object instance editor:

For managing object instances, the following user commands are defined:

  • create <type> <name> <...>: Create a new instance. Also takes parameter values to be set on the object.
  • destroy <type> <name>: Delete an instance.
  • list <type> List all instances of a given type.

The instance editor code assumes that the node that selects the editor node uses ctx.set() to set the object_edit field in ctx.data to the object being edited.

Variant 1 (editor-only node):

const Node = editor.edit_create_destroy(instance_data);

Variant 2 (merge with existing entries):

let Node = {}; editor.edit_create_destroy(instance_data, Node);

instance_data properties:

  • change_cb: function(ctx): Called whenever an instance is added or deleted
  • types: Metadata about instances types (see below)

instance_data.types object properties:

  • node_name: name of the editor node belonging to the object instance.
  • node: The editor node itself.
  • object: Name of the type specific container object inside the object pointed to by ctx.data.object_edit.