├── README.md ├── examples ├── CustomizeParameters │ └── CustomizeParameters.ino └── SerialCommands │ └── SerialCommands.ino ├── keywords.txt ├── library.properties └── src └── CommandParser.h /README.md: -------------------------------------------------------------------------------- 1 | CommandParser 2 | ============= 3 | 4 | An Arduino library for parsing commands of the form `COMMAND_NAME ARG1 ARG2 ARG3 ...`. 5 | 6 | Features: 7 | 8 | * **No dynamic memory allocation**. 9 | * Compile-time-**configurable resource limits**. 10 | * Strongly typed arguments with **strict input validation** (including integer overflow detection!). 11 | * 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). 12 | * Support for **escape sequences** in string arguments (e.g., `SOME_COMMAND "\"\x41\r\n\t\\"`). 13 | 14 | This library works with all Arduino-compatible boards. 15 | 16 | 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. 17 | 18 | Quickstart 19 | ---------- 20 | 21 | Search for "CommandParser" in the Arduino Library Manager, and install it. Now you can try a quick example sketch: 22 | 23 | ```cpp 24 | #include 25 | 26 | typedef CommandParser<> MyCommandParser; 27 | 28 | MyCommandParser parser; 29 | 30 | void cmd_test(MyCommandParser::Argument *args, char *response) { 31 | Serial.print("string: "); Serial.println(args[0].asString); 32 | Serial.print("double: "); Serial.println(args[1].asDouble); 33 | Serial.print("int64: "); Serial.println(args[2].asInt64); 34 | Serial.print("uint64: "); Serial.println(args[3].asUInt64); 35 | strlcpy(response, "success", MyCommandParser::MAX_RESPONSE_SIZE); 36 | } 37 | 38 | void setup() { 39 | Serial.begin(9600); 40 | while (!Serial); 41 | 42 | parser.registerCommand("TEST", "sdiu", &cmd_test); 43 | Serial.println("registered command: TEST "); 44 | Serial.println("example: TEST \"\\x41bc\\ndef\" -1.234e5 -123 123"); 45 | } 46 | 47 | void loop() { 48 | if (Serial.available()) { 49 | char line[128]; 50 | size_t lineLength = Serial.readBytesUntil('\n', line, 127); 51 | line[lineLength] = '\0'; 52 | 53 | char response[MyCommandParser::MAX_RESPONSE_SIZE]; 54 | parser.processCommand(line, response); 55 | Serial.println(response); 56 | } 57 | } 58 | ``` 59 | 60 | More examples: 61 | 62 | * [Parsing commands over Serial](examples/SerialCommands/SerialCommands.ino) 63 | * [Customizing resource limits](examples/CustomizeParameters/CustomizeParameters.ino) 64 | 65 | Grammar 66 | ------- 67 | 68 | Commands are null-terminated strings that largely follow this PEG grammar: 69 | 70 | ``` 71 | COMMAND <- COMMAND_NAME (' '+ (ARG_STRING / ARG_DOUBLE / ARG_INT64 / ARG_UINT64))* ' '* 72 | COMMAND_NAME <- (!' ')+ 73 | ARG_STRING <- ('"' STRING_CHAR_QUOTED* '"') / (STRING_CHAR_UNQUOTED+) 74 | ARG_DOUBLE <- SIGN? ((DEC+ '.' DEC*) / ('.' DEC+)) (('e' / 'E') SIGN? DEC+)? 75 | ARG_INT64 <- SIGN? ARG_UINT64 76 | ARG_UINT64 <- ('0x' HEX+) / ('0o' OCT+) / ('0b' BIN+) / DEC+ 77 | STRING_CHAR_QUOTED <- STRING_CHAR_ESCAPE / (!'"') 78 | STRING_CHAR_UNQUOTED <- STRING_CHAR_ESCAPE / (!' ') 79 | STRING_CHAR_ESCAPE <- ( '\\x' HEX HEX ) / '\\n' / '\\r' / '\\t' / '\\"' / '\\\\' 80 | HEX <- [0-9a-fA-F] 81 | DEC <- [0-9] 82 | OCT <- [0-7] 83 | BIN <- [0-1] 84 | SIGN <- '+' / '-' 85 | ``` 86 | 87 | 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. 88 | 89 | Here are some examples of strings that match the `COMMAND` rule in the above grammar: 90 | 91 | * `test_command "Hello, \"World\"! \\\x31\x32\x33"` where `test_command` is registered with arg types `s`: one string argument with value `Hello, "World"! \123`. 92 | * `! yes no` where `!` is registered with arg types `ss`: two string arguments, one with value `yes` and the other `no`. 93 | * `ABC 0.1 +.1 -123 0. 0 12.3e-1 1E10` where `ABC` is registered with arg types `dddddd`: six double arguments with values `0.1`, `0.1`, `-123`, `0`, `0`, `1.23`, `10000000000`. 94 | * `SomeCommand 0 +0x1F 0b101 0o77 -26` where `SomeCommand` is registered with arg types `iiiii`: five int64 arguments with values `0`, `31`, `5`, `63`, `-26`. 95 | * `SomeCommand 0 0x1F 0b101 0o77 26` where `SomeCommand` is registered with arg types `uuuuu`: five uint64 arguments with values `0`, `31`, `5`, `63`, `26`. 96 | 97 | Comparison 98 | ---------- 99 | 100 | Other libraries for parsing commands: 101 | 102 | * [CmdArduino](https://github.com/joshmarinacci/CmdArduino): 103 | * 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. 104 | * CommandParser has configurable size limits, supports multiple argument types, and supports string escape sequences. 105 | * [CmdParser](https://github.com/pvizeli/CmdParser): 106 | * 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. 107 | * 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. 108 | * Also, CommandParser checks all inputs for validity (including syntax errors when invalid input is given), and doesn't assume the use of Serial. 109 | * [SerialCommand](https://github.com/kroimon/Arduino-SerialCommand): 110 | * 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. 111 | * 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. 112 | 113 | CommandParser is likely the best choice when you need strict input validation, error checking, and configurable resource usage. 114 | 115 | 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. 116 | 117 | Reference 118 | --------- 119 | 120 | ### `CommandParser` 121 | 122 | This accepts several template arguments that limit how much RAM is required: 123 | 124 | * `size_t COMMANDS = 16` - up to 16 commands can be registered. Past this limit, `registerCommand` will return `false`. 125 | * `size_t COMMAND_ARGS = 4` - up to 4 arguments are supported for any given command. Past this limit, `registerCommand` will return `false`. 126 | * `size_t COMMAND_NAME_LENGTH = 10` - command names can be up to 10 bytes. Past this limit, `registerCommand` will return `false`. 127 | * `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 return `false`. 128 | * `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 as `strlcpy` and `snprintf` when writing responses. 129 | 130 | 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. 131 | 132 | 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`. 133 | 134 | ### `CommandParser<...>::Argument` 135 | 136 | 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. 137 | 138 | Command callbacks are passed an array of these, as well as a buffer to write their response into. 139 | 140 | ### `bool CommandParser<...>::registerCommand(const char *name, const char *argTypes, void (*callback)(union Argument *args, char *response))` 141 | 142 | 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. 143 | 144 | Each character in `argTypes` represents the type of its corresponding positional argument: 145 | 146 | * `d` represents a `double`. 147 | * `i` represents an `int64_t`. 148 | * `u` represents an `uint64_t`. 149 | * `s` represents a `char *` (as a null-terminated string). 150 | 151 | 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. 152 | 153 | Returns `true` if the command was successfully registered, `false` otherwise (usually because it exceeds the `CommandParser<...>` limits). 154 | 155 | ### `bool CommandParser<...>::processCommand(const char *command, char *response)` 156 | 157 | Processes a string `command` that contains a command previously registed using `CommandParser<...>::registerCommand`, parsing any arguments and looking up the command's callback. 158 | 159 | 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`. 160 | 161 | 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`. 162 | -------------------------------------------------------------------------------- /examples/CustomizeParameters/CustomizeParameters.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // all of the template arguments below are optional, but it is useful to adjust them to save memory (by lowering the limits) or allow larger inputs (by increasing the limits) 4 | // limit number of commands to at most 5 5 | // limit number of arguments per command to at most 3 6 | // limit length of command names to 10 characters 7 | // limit size of all arguments to 15 bytes (e.g., the argument "\x41\x42\x43" uses 14 characters to represent the string but is actually only 3 bytes, 0x41, 0x42, and 0x43) 8 | // limit size of response strings to 64 bytes 9 | typedef CommandParser<5, 3, 10, 15, 64> MyCommandParser; 10 | 11 | MyCommandParser parser; 12 | int positionX = 0, positionY = 0; 13 | 14 | void cmd_move(MyCommandParser::Argument *args, char *response) { 15 | positionX = args[0].asInt64; 16 | positionY = args[1].asInt64; 17 | Serial.print("MOVING "); Serial.print(args[0].asInt64); Serial.print(" "); Serial.println(args[1].asInt64); 18 | snprintf(response, MyCommandParser::MAX_RESPONSE_SIZE, "moved to %d, %d", positionX, positionY); 19 | } 20 | 21 | void cmd_jump(MyCommandParser::Argument *args, char *response) { 22 | Serial.println("JUMPING!"); 23 | snprintf(response, MyCommandParser::MAX_RESPONSE_SIZE, "jumped at %d, %d", positionX, positionY); 24 | } 25 | 26 | void cmd_say(MyCommandParser::Argument *args, char *response) { 27 | Serial.print("SAYING "); Serial.println(args[0].asString); 28 | snprintf(response, MyCommandParser::MAX_RESPONSE_SIZE, "said %s at %d, %d", args[0].asString, positionX, positionY); 29 | } 30 | 31 | void setup() { 32 | Serial.begin(9600); 33 | while (!Serial); 34 | 35 | parser.registerCommand("move", "ii", &cmd_move); // two int64_t arguments 36 | parser.registerCommand("jump", "", &cmd_jump); // no arguments 37 | parser.registerCommand("say", "s", &cmd_say); // one string argument 38 | 39 | char response[MyCommandParser::MAX_RESPONSE_SIZE]; 40 | 41 | Serial.println("let's try a simple example..."); 42 | parser.processCommand("move 45 -23", response); 43 | Serial.println(response); 44 | parser.processCommand("jump", response); 45 | Serial.println(response); 46 | parser.processCommand("say \"Hello, \\\"world!\\\"\"", response); 47 | Serial.println(response); 48 | 49 | Serial.println("now let's try some invalid inputs..."); 50 | parser.processCommand("invalid", response); // bad command 51 | Serial.println(response); 52 | parser.processCommand("move", response); // missing args 53 | Serial.println(response); 54 | parser.processCommand("jump 123", response); // extra args 55 | Serial.println(response); 56 | parser.processCommand("move 123abc 456", response); // invalid int64 argument 57 | Serial.println(response); 58 | parser.processCommand("say abc", response); // invalid string argument 59 | Serial.println(response); 60 | parser.processCommand("say \"\\x\"", response); // invalid string argument 61 | Serial.println(response); 62 | parser.processCommand("say \"\\x5z\"", response); // invalid string escape 63 | Serial.println(response); 64 | parser.processCommand("say \"abc", response); // missing ending quote 65 | Serial.println(response); 66 | } 67 | 68 | void loop() {} 69 | -------------------------------------------------------------------------------- /examples/SerialCommands/SerialCommands.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | typedef CommandParser<> MyCommandParser; 4 | 5 | MyCommandParser parser; 6 | 7 | void cmd_test(MyCommandParser::Argument *args, char *response) { 8 | Serial.print("string: "); Serial.println(args[0].asString); 9 | Serial.print("double: "); Serial.println(args[1].asDouble); 10 | Serial.print("int64: "); Serial.println((int32_t)args[2].asInt64); // NOTE: on older AVR-based boards, Serial doesn't support printing 64-bit values, so we'll cast it down to 32-bit 11 | Serial.print("uint64: "); Serial.println((uint32_t)args[3].asUInt64); // NOTE: on older AVR-based boards, Serial doesn't support printing 64-bit values, so we'll cast it down to 32-bit 12 | strlcpy(response, "success", MyCommandParser::MAX_RESPONSE_SIZE); 13 | } 14 | 15 | void setup() { 16 | Serial.begin(9600); 17 | while (!Serial); 18 | 19 | parser.registerCommand("TEST", "sdiu", &cmd_test); 20 | Serial.println("registered command: TEST "); 21 | Serial.println("example: TEST \"\\x41bc\\ndef\" -1.234e5 -123 123"); 22 | } 23 | 24 | void loop() { 25 | if (Serial.available()) { 26 | char line[128]; 27 | size_t lineLength = Serial.readBytesUntil('\n', line, 127); 28 | line[lineLength] = '\0'; 29 | 30 | char response[MyCommandParser::MAX_RESPONSE_SIZE]; 31 | parser.processCommand(line, response); 32 | Serial.println(response); 33 | } 34 | } -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | # Datatypes (KEYWORD1) 2 | CommandParser KEYWORD1 3 | Argument KEYWORD1 4 | 5 | # Methods and Functions (KEYWORD2) 6 | registerCommand KEYWORD2 7 | processCommand KEYWORD2 8 | 9 | # Constants (LITERAL1) 10 | MAX_COMMANDS LITERAL1 11 | MAX_COMMAND_ARGS LITERAL1 12 | MAX_COMMAND_NAME_LENGTH LITERAL1 13 | MAX_COMMAND_ARG_SIZE LITERAL1 14 | MAX_RESPONSE_SIZE LITERAL1 15 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=CommandParser 2 | version=1.1.0 3 | author=Anthony Zhang (Uberi) 4 | maintainer=Anthony Zhang (Uberi) 5 | sentence=An Arduino library for parsing commands of the form COMMAND_NAME ARG1 ARG2 ARG3. 6 | paragraph=No dynamic memory allocation. Compile-time-configurable resource limits. Strongly typed arguments with strict input validation. Friendly error messages for invalid inputs. Support for escape sequences in string arguments. 7 | category=Communication 8 | url=https://github.com/Uberi/Arduino-CommandParser 9 | architectures=* 10 | -------------------------------------------------------------------------------- /src/CommandParser.h: -------------------------------------------------------------------------------- 1 | /* 2 | CommandParser.h - Library for parsing commands of the form "COMMAND_NAME ARG1 ARG2 ARG3 ...". 3 | 4 | Copyright 2020 Anthony Zhang (Uberi) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | #ifndef __COMMAND_PARSER_H__ 14 | #define __COMMAND_PARSER_H__ 15 | 16 | #include 17 | 18 | /* 19 | #include 20 | size_t strlcpy(char *dst, const char *src, size_t size) { 21 | *dst = '\0'; 22 | strncat(dst, src, size - 1); 23 | return strlen(dst); 24 | } 25 | */ 26 | 27 | // avr-libc lacks strtoll and strtoull (see https://www.nongnu.org/avr-libc/user-manual/group__avr__stdlib.html), so we'll implement our own to be compatible with AVR boards such as the Arduino Uno 28 | // typically you would use this like: `int64_t result; size_t bytesRead = strToInt("-0x123", &result, std::numeric_limits::min(), std::numeric_limits::max())` 29 | // if an error occurs during parsing, `bytesRead` will be 0 and `result` will be an arbitrary value 30 | template size_t strToInt(const char* buf, T *value, T min_value, T max_value) { 31 | size_t position = 0; 32 | 33 | // parse sign if necessary 34 | bool isNegative = false; 35 | if (min_value < 0 && buf[position] == '+' || buf[position] == '-') { 36 | isNegative = buf[position] == '-'; 37 | position ++; 38 | } 39 | 40 | // parse base identifier if necessary 41 | int base = 10; 42 | if (buf[position] == '0' && buf[position + 1] == 'b') { 43 | base = 2; 44 | position += 2; 45 | } else if (buf[position] == '0' && buf[position + 1] == 'o') { 46 | base = 8; 47 | position += 2; 48 | } else if (buf[position] == '0' && buf[position + 1] == 'x') { 49 | base = 16; 50 | position += 2; 51 | } 52 | 53 | int digit = -1; 54 | *value = 0; 55 | while (true) { 56 | // obtain the next digit of the number 57 | if (base >= 2 && '0' <= buf[position] && buf[position] <= '1') { digit = buf[position] - '0'; } 58 | else if (base >= 8 && '2' <= buf[position] && buf[position] <= '7') { digit = (buf[position] - '2') + 2; } 59 | else if (base >= 10 && '8' <= buf[position] && buf[position] <= '9') { digit = (buf[position] - '8') + 8; } 60 | else if (base >= 16 && 'a' <= buf[position] && buf[position] <= 'f') { digit = (buf[position] - 'a') + 10; } 61 | else if (base >= 16 && 'A' <= buf[position] && buf[position] <= 'F') { digit = (buf[position] - 'A') + 10; } 62 | else { break; } 63 | 64 | if (*value < min_value / base || *value > max_value / base) { return 0; } // integer multiplication underflow/overflow, fail gracefully 65 | *value *= base; 66 | if (isNegative ? *value < min_value + digit : *value > max_value - digit) { return 0; } // integer subtraction-underflow/addition-overflow, fail gracefully 67 | *value += digit; 68 | 69 | position ++; 70 | } 71 | return digit == -1 ? 0 : position; // ensure that there is at least one digit 72 | } 73 | 74 | template 75 | class CommandParser { 76 | public: 77 | static const size_t MAX_COMMANDS = COMMANDS; 78 | static const size_t MAX_COMMAND_ARGS = COMMAND_ARGS; 79 | static const size_t MAX_COMMAND_NAME_LENGTH = COMMAND_NAME_LENGTH; 80 | static const size_t MAX_COMMAND_ARG_SIZE = COMMAND_ARG_SIZE; 81 | static const size_t MAX_RESPONSE_SIZE = RESPONSE_SIZE; 82 | 83 | union Argument { 84 | double asDouble; 85 | uint64_t asUInt64; 86 | int64_t asInt64; 87 | char asString[MAX_COMMAND_ARG_SIZE + 1]; 88 | }; 89 | private: 90 | struct Command { 91 | char name[MAX_COMMAND_NAME_LENGTH + 1]; 92 | char argTypes[MAX_COMMAND_ARGS + 1]; 93 | void (*callback)(union Argument *args, char *response); 94 | }; 95 | 96 | union Argument commandArgs[MAX_COMMAND_ARGS]; 97 | struct Command commandDefinitions[MAX_COMMANDS]; 98 | size_t numCommands = 0; 99 | 100 | size_t parseString(const char *buf, char *output) { 101 | size_t readCount = 0; 102 | bool isQuoted = buf[0] == '"'; // whether the string is quoted or just a plain word 103 | if (isQuoted) { 104 | readCount ++; // move past the opening quote 105 | } 106 | 107 | size_t i = 0; 108 | for (; i < MAX_COMMAND_ARG_SIZE && buf[readCount] != '\0'; i ++) { // loop through each character of the string literal 109 | if (isQuoted ? buf[readCount] == '"' : buf[readCount] == ' ') { 110 | break; 111 | } 112 | if (buf[readCount] == '\\') { // start of the escape sequence 113 | readCount ++; // move past the backslash 114 | switch (buf[readCount]) { // check what kind of escape sequence it is, turn it into the correct character 115 | case 'n': output[i] = '\n'; readCount ++; break; 116 | case 'r': output[i] = '\r'; readCount ++; break; 117 | case 't': output[i] = '\t'; readCount ++; break; 118 | case '"': output[i] = '"'; readCount ++; break; 119 | case '\\': output[i] = '\\'; readCount ++; break; 120 | case 'x': { // hex escape, of the form \xNN where NN is a byte in hex 121 | readCount ++; // move past the "x" character 122 | output[i] = 0; 123 | for (size_t j = 0; j < 2; j ++, readCount ++) { 124 | if ('0' <= buf[readCount] && buf[readCount] <= '9') { output[i] = output[i] * 16 + (buf[readCount] - '0'); } 125 | else if ('a' <= buf[readCount] && buf[readCount] <= 'f') { output[i] = output[i] * 16 + (buf[readCount] - 'a') + 10; } 126 | else if ('A' <= buf[readCount] && buf[readCount] <= 'F') { output[i] = output[i] * 16 + (buf[readCount] - 'A') + 10; } 127 | else { return 0; } 128 | } 129 | break; 130 | } 131 | default: // unknown escape sequence 132 | return 0; 133 | } 134 | } else { // non-escaped character 135 | output[i] = buf[readCount]; 136 | readCount ++; 137 | } 138 | } 139 | if (isQuoted) { 140 | if (buf[readCount] != '"') { return 0; } 141 | readCount ++; // move past the closing quote 142 | } 143 | 144 | output[i] = '\0'; 145 | return readCount; 146 | } 147 | public: 148 | bool registerCommand(const char *name, const char *argTypes, void (*callback)(union Argument *args, char *response)) { 149 | if (numCommands == MAX_COMMANDS) { return false; } 150 | if (strlen(name) > MAX_COMMAND_NAME_LENGTH) { return false; } 151 | if (strlen(argTypes) > MAX_COMMAND_ARGS) { return false; } 152 | if (callback == nullptr) { return false; } 153 | for (size_t i = 0; argTypes[i] != '\0'; i ++) { 154 | switch (argTypes[i]) { 155 | case 'd': 156 | case 'u': 157 | case 'i': 158 | case 's': 159 | break; 160 | default: 161 | return false; 162 | } 163 | } 164 | 165 | strlcpy(commandDefinitions[numCommands].name, name, MAX_COMMAND_NAME_LENGTH + 1); 166 | strlcpy(commandDefinitions[numCommands].argTypes, argTypes, MAX_COMMAND_ARGS + 1); 167 | commandDefinitions[numCommands].callback = callback; 168 | numCommands ++; 169 | return true; 170 | } 171 | 172 | bool processCommand(const char *command, char *response) { 173 | // retrieve the command name 174 | char name[MAX_COMMAND_NAME_LENGTH + 1]; 175 | size_t i = 0; 176 | for (; i < MAX_COMMAND_NAME_LENGTH && *command != ' ' && *command != '\0'; i ++, command ++) { name[i] = *command; } 177 | name[i] = '\0'; 178 | 179 | // look up the command argument types and callback 180 | char *argTypes = nullptr; 181 | void (*callback)(union Argument *, char *) = nullptr; 182 | for (size_t i = 0; i < numCommands; i ++) { 183 | if (strcmp(commandDefinitions[i].name, name) == 0) { 184 | argTypes = commandDefinitions[i].argTypes; 185 | callback = commandDefinitions[i].callback; 186 | break; 187 | } 188 | } 189 | if (argTypes == nullptr) { 190 | snprintf(response, MAX_RESPONSE_SIZE, "parse error: unknown command name %s", name); 191 | return false; 192 | } 193 | 194 | // parse each command 195 | for (size_t i = 0; argTypes[i] != '\0'; i ++) { 196 | // require and skip 1 or more whitespace characters 197 | if (*command != ' ') { 198 | snprintf(response, MAX_RESPONSE_SIZE, "parse error: missing whitespace before arg %d", i + 1); 199 | return false; 200 | } 201 | do { command ++; } while (*command == ' '); 202 | 203 | switch (argTypes[i]) { 204 | case 'd': { // double argument 205 | char *after; 206 | commandArgs[i].asDouble = strtod(command, &after); 207 | if (after == command || (*after != ' ' && *after != '\0')) { 208 | snprintf(response, MAX_RESPONSE_SIZE, "parse error: invalid double for arg %d", i + 1); 209 | return false; 210 | } 211 | command = after; 212 | break; 213 | } 214 | case 'u': { // uint64_t argument 215 | size_t bytesRead = strToInt(command, &commandArgs[i].asUInt64, 0, ULONG_LONG_MAX); 216 | if (bytesRead == 0 || (command[bytesRead] != ' ' && command[bytesRead] != '\0')) { 217 | snprintf(response, MAX_RESPONSE_SIZE, "parse error: invalid uint64_t for arg %d", i + 1); 218 | return false; 219 | } 220 | command += bytesRead; 221 | break; 222 | } 223 | case 'i': { // int64_t argument 224 | size_t bytesRead = strToInt(command, &commandArgs[i].asInt64, LONG_LONG_MIN, LONG_LONG_MAX); 225 | if (bytesRead == 0 || (command[bytesRead] != ' ' && command[bytesRead] != '\0')) { 226 | snprintf(response, MAX_RESPONSE_SIZE, "parse error: invalid int64_t for arg %d", i + 1); 227 | return false; 228 | } 229 | command += bytesRead; 230 | break; 231 | } 232 | case 's': { 233 | size_t readCount = parseString(command, commandArgs[i].asString); 234 | if (readCount == 0) { 235 | snprintf(response, MAX_RESPONSE_SIZE, "parse error: invalid string for arg %d", i + 1); 236 | return false; 237 | } 238 | command += readCount; 239 | break; 240 | } 241 | default: 242 | snprintf(response, MAX_RESPONSE_SIZE, "parse error: invalid argtype %c for arg %d", argTypes[i], i + 1); 243 | return false; 244 | } 245 | } 246 | 247 | // skip whitespace 248 | while (*command == ' ') { command ++; } 249 | 250 | // ensure that we're at the end of the command 251 | if (*command != '\0') { 252 | snprintf(response, MAX_RESPONSE_SIZE, "parse error: too many args (expected %d)", strlen(argTypes)); 253 | return false; 254 | } 255 | 256 | // set response to empty string 257 | response[0] = '\0'; 258 | 259 | // invoke the command 260 | (*callback)(commandArgs, response); 261 | return true; 262 | } 263 | }; 264 | 265 | #endif 266 | --------------------------------------------------------------------------------