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>
365 lines
14 KiB
Markdown
365 lines
14 KiB
Markdown
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`.
|
|
|