A tiny extensible command interpreter with scripting support for embedded systems. Add an interactive serial CLI, single-key menus, or scripted command sequences to any microcontroller -- from an 8-bit ATtiny85 to a 64-bit ARM Cortex-A. Pure C, no malloc, no OS, under 3-5 KB fully featured depending on archicture.
Xelp is instance based so it's possible to run distinct copies on different ports and it can be run inside interrupts (pending your own functions are safe).
Many embedded projects end up with an ad-hoc if (char == 'x') debug
console. xelp replaces that with a proper CLI that:
- Compiles on 8-bit to 64-bit targets with any C89 or later compiler
- Fits in ~1 KB (key dispatch only) to ~5 KB (full CLI with line editing)
- Uses zero dynamic memory -- no malloc, no heap, safe for ISRs
- Supports multiple independent instances -- one per UART, no globals
- Multiple commands can be saved as strings (scripts) and run from C or from the CLI.
- Scripts are ROM-able const strings -- the parser never modifies its input (e.g. no strtok style processing)
- Function dispatch tables make any C/C++ function callable from the CLI
- Live help listing (optional, compile-time removable), commands can have run time assistance
- CLI commands can be called from C or C functions can be called from the CLI.
- A cpp wrapper is provided with identical functionality and some syntactic sugar for ease of use (still no memory allocation)
Xelp was first built for some embedded projects in the late 90s (though under different names) and then made more uniform in the late 2000s.
xelp is modular -- enable only what you need. Three profiles cover most use cases.
Single-keypress dispatch: each key triggers a function immediately, no ENTER needed. Ideal for debug menus and hardware test jigs.
#define XELP_ENABLE_KEY 1Append-only command line with tokenizer, scripting, and command dispatch. No line editing, no cursor movement, no help listing. Ideal for the most size-constrained targets that still need a CLI.
#define XELP_ENABLE_CLI 1Line-buffered command prompt with cursor movement, line editing, multi-byte ANSI key recognition, tokenizer, scripting, and help. This is the typical interactive configuration.
#define XELP_ENABLE_CLI 1
#define XELP_ENABLE_LINE_EDIT 1
#define XELP_ENABLE_KEY 1
#define XELP_ENABLE_HELP 1Optional: add XELP_ENABLE_HISTORY for UP/DOWN arrow command recall
(~420 bytes, requires XELP_ENABLE_LINE_EDIT).
Adds THR pass-through mode (~50-125 bytes more) for forwarding all keystrokes to another peripheral such as a modem or radio module. This enables the cli to flip in to a mode where one can send raw commands (such AT commands) to another device.
#define XELP_ENABLE_FULL 1Every flag is independent -- mix and match. For the full reference see Build Profiles & Configuration Guide.
See examples for simpler C++ version.
Add these three files to your project: xelp.c, xelp.h, xelpcfg.h.
#include "xelpcfg.h"
#include "xelp.h"
/* Your output function -- write one char to UART, LCD, etc. */
void uart_putc(char c) { UART_TX = c; }
/* Your destructive backspace function - customize for your OS */
void uart_bksp(void) { uart_putc('\b'); uart_putc(' '); uart_putc('\b'); }
/* Commands -- any C function with this signature */
XELPRESULT cmd_hello(XELP *ths, const char *args, int len) {
XelpOut(ths, "Hello!\n", 0);
return XELP_S_OK;
}
XELPRESULT cmd_led(XELP *ths, const char *args, int len) {
XelpArgs a;
int val;
XelpArgsInit(&a, args, len);
XelpNextTok(&a, 0); /* skip command name */
XelpNextInt(&a, &val);
LED_PORT = val;
return XELP_S_OK;
}
/* Command table */
XELPCLIFuncMapEntry commands[] = {
{ &cmd_hello, "hello", "say hello" },
{ &cmd_led, "led", "led <0|1>" },
XELP_FUNC_ENTRY_LAST
};
XELP cli;
void main(void) {
XelpInit(&cli, "My Device v1.0");
XELP_SET_FN_OUT(cli, &uart_putc);
XELP_SET_FN_BKSP(cli, &uart_bksp);
XELP_SET_FN_CLI(cli, commands);
for (;;) {
if (uart_rx_ready())
XelpParseKey(&cli, uart_getc());
}
}At the prompt:
xelp> hello
Hello!
xelp> led 1
xelp>
The prompt string is settable per instance:
XELP_SET_VAL_CLI_PROMPT(cli, "mydev>");The prompt is stored by pointer, not copied. It must be a null-terminated string that remains valid for the life of the instance (string literal, static, or global -- not a stack buffer that goes out of scope).
Prompts can be global (all instances share a prompt), or per-instance (each peripheral gets its own prompt)
Anything typed at the CLI can be run as a script. Scripts are const strings parsed without modification -- they can live in ROM:
const char *startup_script = "hello; led 1"; /* cli functions that are available plus mode switch imperatives are allowed */
XelpParse(&cli, startup_script, XelpStrLen(startup_script));Scripts support semicolons (;), newlines, # comments, quoted strings
("..."), and escape characters (backtick at CLI, backslash in quotes).
CTRL-P ESC CTRL-T
CLI mode -------> KEY mode -----> THR mode
^ |
+--------------------------------+
mode switch keys
- CLI: Line-buffered input with prompt. Type commands, press ENTER.
- KEY: Each keypress triggers a command immediately. For menus.
- THR: All keys pass through to another peripheral. For debugging modems, serial devices.
Mode switch keys are configurable in xelpcfg.h. If any mode is not enabled then the mode switch skips that mode
make validate # build + run tests + build all examples (the everyday check)
make tests # unit tests + coverage only
make examples # build all examples (no interactive launch)
make example # build and run the posix ncurses demo (interactive)
make coverage # tests + coverage summary
make fuzz # fuzz testing with libFuzzer (requires clang)
make funcsizes # per-function compiled sizes (x86-32, ARM32)
make sizes # print compiled sizes for all feature profiles
make clean # remove test build artifacts
make clean-all # clean tests + all examples47 test units, 598 test cases, 100% line coverage of xelp.c.
Feature profile sizes: dev/size_profiles.sh (uses Docker for ARM Cortex-M0, falls back to host GCC).
make prerelease # tests + examples + Docker cross-compile + update README size tablesRuns make validate, then tools/crossbuild.sh (Docker), then
tools/update_sizes.sh to patch the compiled-size tables in README.md
and pages/index.html. Does not tag, push, or publish.
bash tools/make_release.sh # full guided release (includes Docker cross-build)
bash tools/make_release.sh --validate # local validation only (no git, no push)The release script handles everything end-to-end: validation (tests +
examples), manifest sync, badge update, Docker cross-compilation, size
table update, push, PR, CI, merge, tag, and publish. The Docker
cross-build step is skipped if Docker is not installed. Day-to-day
development uses make validate which takes seconds.
Compiled .text section sizes with -Os. Three configurations: KEY
(single-key dispatch only), CLI (typical interactive use), FULL (all
features). Even the largest full build is under 7 KB.
| CPU | Width | Compiler | KEY (bytes) | CLI (bytes) | FULL (bytes) |
|---|---|---|---|---|---|
| AVR (ATtiny85) | 8 | avr-gcc | 1046 | 4270 | 4328 |
| AVR (ATmega328P) | 8 | avr-gcc | 1054 | 4370 | 4428 |
| Z80 | 8 | SDCC | 2121 | 7287 | 7395 |
| 6800 (HC08) | 8 | SDCC | 2471 | 8616 | 8718 |
| MSP430 | 16 | msp430-gcc | 770 | 3486 | 3532 |
| 68HC11 | 16 | m68hc11-gcc | 2369 | 6895 | 6966 |
| Xtensa LX7 (ESP32-S3) | 32 | xtensa-esp-elf-gcc | 576 | 2600 | 2632 |
| ARM Thumb | 32 | arm-none-eabi-gcc | 580 | 2598 | 2642 |
| RISC-V (rv32) | 32 | riscv64-unknown-elf-gcc | 722 | 3100 | 3138 |
| Xtensa LX106 (ESP8266) | 32 | xtensa-lx106-elf-gcc | 723 | 2947 | 2979 |
| m68k | 32 | m68k-linux-gnu-gcc | 728 | 3336 | 3384 |
| ARM32 | 32 | arm-none-eabi-gcc | 980 | 3934 | 3994 |
| x86-32 | 32 | GCC | 1081 | 4919 | 4969 |
| MIPS32 | 32 | mipsel-linux-gnu-gcc | 1296 | 5224 | 5272 |
| PowerPC | 32 | powerpc-linux-gnu-gcc | 1504 | 6066 | 6130 |
| RISC-V (rv64) | 64 | riscv64-linux-gnu-gcc | 756 | 3554 | 3588 |
| x86-64 | 64 | Clang | 1043 | 5269 | 5311 |
| x86-64 | 64 | GCC | 1063 | 5138 | 5187 |
| AArch64 (ARM64) | 64 | aarch64-linux-gnu-gcc | 1324 | 5574 | 5630 |
| MIPS64 | 64 | mips64el-linux-gnuabi64-gcc | 1360 | 5864 | 5928 |
x86-64 GCC row is measured directly; others from cross-compilation via
tools/Dockerfile.crossbuild. This is for size tracking, older versions / cousins of xelp were compiled and run on several (but not all of the above platforms)
All compile-time options (buffer size, key mappings, prompt, escape
characters, register count) are controlled via #define flags in
src/xelpcfg.h. See the
Build Profiles & Configuration Guide.
xelp compiles on anything with a C89 compiler. To port:
- Add
xelp.c,xelp.h,xelpcfg.hto your build - Write a
void putc(char c)function for your output hardware - Call
XELP_SET_FN_OUT()andXelpParseKey()-- that's it
No assembly. No platform #ifdefs (except optional SDCC __reentrant).
See Porting Guide.
The following toolchains compile xelp with zero warnings. Verified via
Docker cross-compilation (tools/Dockerfile.crossbuild):
| Architecture | Compiler | Word Size |
|---|---|---|
| x86-64 | GCC, Clang | 64-bit |
| x86-32 | GCC, Clang | 32-bit |
| ARM64 | aarch64-linux-gnu-gcc | 64-bit |
| ARM32 / Thumb | arm-none-eabi-gcc | 32-bit |
| RISC-V (rv64) | riscv64-linux-gnu-gcc | 64-bit |
| RISC-V (rv32) | riscv64-unknown-elf-gcc | 32-bit |
| Xtensa LX106 (ESP8266) | xtensa-lx106-elf-gcc | 32-bit |
| Xtensa LX7 (ESP32-S3) | xtensa-esp-elf-gcc | 32-bit |
| MSP430 | msp430-gcc | 16-bit |
| m68k (68000) | m68k-linux-gnu-gcc | 32-bit |
| AVR (ATmega, ATtiny) | avr-gcc | 8-bit |
| 8051 | SDCC | 8-bit |
| 68HC11/12 | m68hc11-gcc | 16-bit |
| PowerPC | powerpc-linux-gnu-gcc | 32-bit |
xelp/
src/ xelp.c, xelp.h, xelpcfg.h (the library -- add these to your project)
tests/ unit tests (jumpbug framework), fuzz harnesses, 100% coverage
examples/ POSIX ncurses, Arduino, C++ wrapper, bare-metal, scripting, ESP32 Wi-Fi
tools/ cross-build scripts, release script, banner generator
docs/ API reference, configuration guide, porting guide
pages/ GitHub Pages site
dev/ design notes, planning docs, size profiling tools
.github/ CI workflows (build, test, fuzz, PlatformIO)
PRs welcome. master is protected -- all changes go through pull requests.
CI must pass (zero warnings, all tests, coverage) before merge. Please note that
PRs which affect multi-instance support or require dynamic memory may not be accepted.
git checkout -b dev-my-feature master
# make changes...
make clean && make tests # must pass, zero warningsSee CONTRIBUTING.md for full guidelines: coding standards, what we welcome, branch model, and the release process.
- Tutorial -- step-by-step introduction to xelp
- Examples -- annotated code for various platforms
- API Reference -- all public functions, macros, types
- Build Profiles & Configuration Guide -- feature system and compile-time options
- Configuration Quick Reference -- all
#defineflags at a glance - Porting Guide -- bringing up xelp on a new platform
- Release Management -- versioning, CI, release workflow
- Tools -- build utilities and code generators
- Contributing -- how to contribute
If you use AI coding agents (Claude Code, Cursor, Copilot, etc.), xelp provides machine-readable context files for accurate code generation:
- AGENTS.md -- concise coding reference: function signatures, setup patterns, common mistakes
- llms.txt -- project overview and documentation index (llmstxt.org format)
BSD 2-Clause. See LICENSE.txt.
Copyright (c) 2011-2026, M. A. Chatterjee

