An Arduino library for parsing commands of the form COMMAND_NAME ARG1 ARG2 ARG3 ...
.
Features:
- No dynamic memory allocation.
- Compile-time-configurable resource limits.
- Strongly typed arguments with strict input validation (including integer overflow detection!).
- Friendly error messages for invalid inputs (e.g.,
parse error: invalid double for arg 2
if the command's second argument cannot be parsed as a double). - Support for escape sequences in string arguments (e.g.,
SOME_COMMAND "\"\x41\r\n\t\\"
).
This library works with all Arduino-compatible boards.
This library has a higher RAM footprint compared to similar libraries, because it fully parses each argument instead of leaving them as strings. An instance of CommandParser<>
in RAM is 456 bytes on a 32-bit Arduino Nano 33 BLE.
Search for "CommandParser" in the Arduino Library Manager, and install it. Now you can try a quick example sketch:
#include <CommandParser.h>
typedef CommandParser<> MyCommandParser;
MyCommandParser parser;
void cmd_test(MyCommandParser::Argument *args, char *response) {
Serial.print("string: "); Serial.println(args[0].asString);
Serial.print("double: "); Serial.println(args[1].asDouble);
Serial.print("int64: "); Serial.println(args[2].asInt64);
Serial.print("uint64: "); Serial.println(args[3].asUInt64);
strlcpy(response, "success", MyCommandParser::MAX_RESPONSE_SIZE);
}
void setup() {
Serial.begin(9600);
while (!Serial);
parser.registerCommand("TEST", "sdiu", &cmd_test);
Serial.println("registered command: TEST <string> <double> <int64> <uint64>");
Serial.println("example: TEST \"\\x41bc\\ndef\" -1.234e5 -123 123");
}
void loop() {
if (Serial.available()) {
char line[128];
size_t lineLength = Serial.readBytesUntil('\n', line, 127);
line[lineLength] = '\0';
char response[MyCommandParser::MAX_RESPONSE_SIZE];
parser.processCommand(line, response);
Serial.println(response);
}
}
More examples:
Commands are null-terminated strings that largely follow this PEG grammar:
COMMAND <- COMMAND_NAME (' '+ (ARG_STRING / ARG_DOUBLE / ARG_INT64 / ARG_UINT64))* ' '*
COMMAND_NAME <- (!' ')+
ARG_STRING <- ('"' STRING_CHAR_QUOTED* '"') / (STRING_CHAR_UNQUOTED+)
ARG_DOUBLE <- SIGN? ((DEC+ '.' DEC*) / ('.' DEC+)) (('e' / 'E') SIGN? DEC+)?
ARG_INT64 <- SIGN? ARG_UINT64
ARG_UINT64 <- ('0x' HEX+) / ('0o' OCT+) / ('0b' BIN+) / DEC+
STRING_CHAR_QUOTED <- STRING_CHAR_ESCAPE / (!'"')
STRING_CHAR_UNQUOTED <- STRING_CHAR_ESCAPE / (!' ')
STRING_CHAR_ESCAPE <- ( '\\x' HEX HEX ) / '\\n' / '\\r' / '\\t' / '\\"' / '\\\\'
HEX <- [0-9a-fA-F]
DEC <- [0-9]
OCT <- [0-7]
BIN <- [0-1]
SIGN <- '+' / '-'
This grammar is a superset of what the parser will actually accept, since the ARG_STRING / ARG_DOUBLE / ARG_INT64 / ARG_UINT64
choice is predetermined when COMMAND_NAME
is registered (i.e., a given command always accepts the same number and types of arguments). Additionally, there are other limits (string arguments that are longer than COMMAND_ARG_SIZE
, int64 arguments that would underflow/overflow a 64-bit signed integer, and so on) that further restrict what inputs are accepted.
Here are some examples of strings that match the COMMAND
rule in the above grammar:
test_command "Hello, \"World\"! \\\x31\x32\x33"
wheretest_command
is registered with arg typess
: one string argument with valueHello, "World"! \123
.! yes no
where!
is registered with arg typesss
: two string arguments, one with valueyes
and the otherno
.ABC 0.1 +.1 -123 0. 0 12.3e-1 1E10
whereABC
is registered with arg typesdddddd
: six double arguments with values0.1
,0.1
,-123
,0
,0
,1.23
,10000000000
.SomeCommand 0 +0x1F 0b101 0o77 -26
whereSomeCommand
is registered with arg typesiiiii
: five int64 arguments with values0
,31
,5
,63
,-26
.SomeCommand 0 0x1F 0b101 0o77 26
whereSomeCommand
is registered with arg typesuuuuu
: five uint64 arguments with values0
,31
,5
,63
,26
.
Other libraries for parsing commands:
- CmdArduino:
- CmdArduino has hardcoded size limits for arguments/commands, only supports string arguments, and doesn't support string escape sequences. It works well as the most minimal option.
- CommandParser has configurable size limits, supports multiple argument types, and supports string escape sequences.
- CmdParser:
- CmdParser's command syntax is more configurable, with options such as setting the separator character, named parameters, and quoting behaviour. It works well as the most configurable option.
- CommandParser's command syntax is not configurable, but additionally supports escape sequences in string arguments, non-string arguments such as doubles and integers that are automatically parsed/validated, and empty string arguments.
- Also, CommandParser checks all inputs for validity (including syntax errors when invalid input is given), and doesn't assume the use of Serial.
- SerialCommand:
- SerialCommand only accepts input from Serial, only supports string arguments, doesn't validate that the number of arguments, and doesn't support strings that contain the argument delimiter or any escape sequences. It works well for simpler Serial-specific use cases.
- CommandParser accepts input from any char array, supports multiple argument types, validates that all arguments parse correctly and that the correct syntax is used, and supports escape sequences in strings.
CommandParser is likely the best choice when you need strict input validation, error checking, and configurable resource usage.
CommandParser is likely not the best choice when you need to customize the command syntax, use optional/named arguments, or are very tight on RAM.
This accepts several template arguments that limit how much RAM is required:
size_t COMMANDS = 16
- up to 16 commands can be registered. Past this limit,registerCommand
will returnfalse
.size_t COMMAND_ARGS = 4
- up to 4 arguments are supported for any given command. Past this limit,registerCommand
will returnfalse
.size_t COMMAND_NAME_LENGTH = 10
- command names can be up to 10 bytes. Past this limit,registerCommand
will returnfalse
.size_t COMMAND_ARG_SIZE = 32
- arguments passed to commands can be up to 32 bytes in size. Note that there may be more than 32 characters used to represent the argument; for example, a string argument"\x41\x42\x43"
is 14 characters but the argument would only be 3 bytes, 0x41, 0x42, and 0x43. Past this limit,processCommand
will returnfalse
.size_t RESPONSE_SIZE = 64
- responses from command callback can be up to 64 characters in length. Past this limit, there is a risk of buffer overflow - always use bounded string handling functions such asstrlcpy
andsnprintf
when writing responses.
To avoid writing these arguments in multiple places in your code, typically you'll want to do something like typedef CommandParser<16, 4, 10, 32, 64> MyCommandParser;
, and then use MyCommandParser
everywhere else in your code.
These properties are then also available as static class variables: CommandParser::MAX_COMMANDS
. CommandParser::MAX_COMMAND_ARGS
, CommandParser<...>::MAX_COMMAND_NAME_LENGTH
, CommandParser<...>::MAX_COMMAND_ARG_SIZE
, CommandParser<...>::MAX_RESPONSE_SIZE
.
This union struct represents a single argument value. Access the correct field to get the right value out - if arg
is a CommandParser<...>::Argument
representing an int64_t
argument, then arg.asInt64
is the int64_t
value that it contains.
Command callbacks are passed an array of these, as well as a buffer to write their response into.
bool CommandParser<...>::registerCommand(const char *name, const char *argTypes, void (*callback)(union Argument *args, char *response))
Registers a new command with name name
and argument types argTypes
. When CommandParser<...>::processCommand
processes input containing this command, it calls callback
with the parsed arguments.
Each character in argTypes
represents the type of its corresponding positional argument:
d
represents adouble
.i
represents anint64_t
.u
represents anuint64_t
.s
represents achar *
(as a null-terminated string).
So if argTypes
is "sdiu"
, that represents four arguments, where the first is a string, the second is a double, the third is a 64-bit signed integer, and the fourth is a 64-bit unsigned integer.
Returns true
if the command was successfully registered, false
otherwise (usually because it exceeds the CommandParser<...>
limits).
Processes a string command
that contains a command previously registed using CommandParser<...>::registerCommand
, parsing any arguments and looking up the command's callback.
If command
is fully parsed, this calls the command's callback with an array of CommandParser<...>::Argument
instances, as well as a response buffer response
, which the callback may choose to write in (response
is initialized to an empty string before the callback is called), then returns true
.
Otherwise, command
could not be fully parsed, so processCommand
will write a descriptive error message to response
, no callbacks will be called, and this returns false
.