Single-header CLI library intended for use in embedded systems (like STM32 or Arduino).
- Dynamic or static allocation
- Configurable memory usage
- Command-to-function binding with arguments support
- Live autocompletion (see demo above, can be disabled)
- Tab (jump to end of current autocompletion) and backspace (remove char) support
- History support (navigate with up and down keypress)
- Any byte-stream interface is supported (for example, UART)
- Single-header distribution
- You'll need to download embedded_cli.h single-header version from Releases tab. Or you can build it yourself (run
build-shl.py python script while inside
lib
directory) - Add embedded_cli.h to your project (to one of the include directories)
- Include embedded_cli.h header file in your code where you need cli functionality:
#include "embedded_cli.h"
- In one (and only one) compilation unit (.c/.cpp file) define macro to unwrap implementations:
#define EMBEDDED_CLI_IMPL
#include "embedded_cli.h"
To create CLI, you'll need to provide a desired config. Best way is to get default config and change desired values. For example, change maximum amount of command bindings:
EmbeddedCliConfig *config = embeddedCliDefaultConfig();
config->maxBindingCount = 16;
Create an instance of CLI:
EmbeddedCli *cli = embeddedCliNew(config);
If default arguments are good enough for you, you can create cli with default config:
EmbeddedCli *cli = embeddedCliNewDefault();
Provide a function that will be used to send chars to the other end:
void writeChar(EmbeddedCli *embeddedCli, char c);
// ...
cli->writeChar = writeChar;
After creation, provide desired bindings to CLI (can be provided at any point in runtime):
embeddedCliAddBinding(cli, {
"get-led", // command name (spaces are not allowed)
"Get led status", // Optional help for a command (NULL for no help)
false, // flag whether to tokenize arguments (see below)
nullptr, // optional pointer to any application context
onLed // binding function
});
embeddedCliAddBinding(cli, {
"get-adc",
"Read adc value",
true,
nullptr,
onAdc
});
Don't forget to create binding functions as well:
void onLed(EmbeddedCli *cli, char *args, void *context) {
// use args as raw null-terminated string of all arguments
}
void onAdc(EmbeddedCli *cli, char *args, void *context) {
// use args as list of tokens
}
CLI has functions to easily handle list of space separated arguments. If you have null-terminated string you can convert it to list of tokens with single call:
embeddedCliTokenizeArgs(args);
Note: This function will need to make args string double null-terminated Initial array will be modified (so it must be non const), do not use it after this call directly. After that you can count arguments or get single argument as a null-terminated string:
const char * arg = embeddedCliGetToken(args, pos); // args are counted from 1 (not from 0)
uint8_t pos = embeddedCliFindToken(args, argument);
uint8_t count = embeddedCliGetTokenCount(const char *tokenizedStr);
Examples of tokenization:
Input | Arg 1 | Arg 2 | Comments |
---|---|---|---|
abc def | abc | def | Space is treated as arg separator |
"abc def" | abc def | To use space inside arg surround arg with quotes | |
"abc\" d\\ef" | abc" d\ef | To use quotes or slashes escape them | |
"abc def" test | abc def | test | You can mix quoted args and non quoted |
"abc def"test | abc def | test | Space between quoted args is optional |
"abc def""test 2" | abc def | test 2 | Space between quoted args is optional |
At runtime you need to provide all received chars to cli:
// char c = Serial.read();
embeddedCliReceiveChar(cli, c);
This function can be called from normal code or from ISRs (but don't call it from multiple places). This call puts char into internal buffer, but no processing is done yet.
To do all the "hard" work, call process function periodically
embeddedCliProcess(cli);
Processing should be called from one place only and it shouldn't be inside ISRs. Otherwise, your internal state might get corrupted.
CLI can be used with statically allocated buffer for its internal structures. Required size of buffer depends on CLI
configuration. If size is not enough, NULL is returned from embeddedCliNew
. To get required size (in bytes) for
your config use this call:
uint16_t size = embeddedCliRequiredSize(config);
On some architectures (for example, on some ARM devices) it is important that allocated buffer is aligned properly.
Because of that, cliBuffer
uses type specific for used platform (16bit on AVR, 32bit on ARM, 64bit on amd64). Actual
integer type used to build cliBuffer
is defined in macro CLI_UINT
. Note that required size in bytes should be
divided by size of CLI_UINT
, or you can use macro BYTES_TO_CLI_UINTS(CLI_BUFFER_SIZE)
. Create a buffer of required
size and provide it to CLI:
CLI_UINT cliBuffer[BYTES_TO_CLI_UINTS(CLI_BUFFER_SIZE)];
// ...
config->cliBuffer = cliBuffer;
config->cliBufferSize = CLI_BUFFER_SIZE;
If cliBuffer
in config is NULL, dynamic allocation (with malloc) is used.
In such case size is computed automatically.
You'll need to begin communication (usually through a UART) with a device running a CLI. Terminal is required for correct experience. Following control sequences are reserved:
- \r or \n sends a command (\r\n is also supported)
- \b removes last typed character
- \t moves cursor to the end of autocompleted command
- Esc[A (key up) and Esc[B (key down) navigates through history
If you run CLI through a serial port (like on Arduino with its UART-USB converter), you can use for example PuTTY (Windows) or XTerm (Linux).
There is an example for Arduino (tested with Arduino Nano, but should work on anything with at least 1kB of RAM). Look inside examples directory for a full code.