├── CLICommand.h ├── CLIOutputStream.cpp ├── CLIOutputStream.h ├── CLISessionContext.cpp ├── CLISessionContext.h ├── CLIToken.cpp ├── CLIToken.h ├── CMakeLists.txt ├── LICENSE └── README.md /CLICommand.h: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************************************** 2 | * * 3 | * embedded-cli v0.1 * 4 | * * 5 | * Copyright (c) 2021 Andrew D. Zonenberg and contributors * 6 | * All rights reserved. * 7 | * * 8 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * 9 | * following conditions are met: * 10 | * * 11 | * * Redistributions of source code must retain the above copyright notice, this list of conditions, and the * 12 | * following disclaimer. * 13 | * * 14 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * 15 | * following disclaimer in the documentation and/or other materials provided with the distribution. * 16 | * * 17 | * * Neither the name of the author nor the names of any contributors may be used to endorse or promote products * 18 | * derived from this software without specific prior written permission. * 19 | * * 20 | * THIS SOFTWARE IS PROVIDED BY THE AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * 21 | * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * 22 | * THE AUTHORS BE HELD LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * 24 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * 26 | * POSSIBILITY OF SUCH DAMAGE. * 27 | * * 28 | ***********************************************************************************************************************/ 29 | 30 | /** 31 | @file 32 | @brief Declaration of CLICommand 33 | */ 34 | #ifndef CLICommand_h 35 | #define CLICommand_h 36 | 37 | #ifndef MAX_TOKENS_PER_COMMAND 38 | 39 | ///@brief Maximum number of tokens in a command 40 | #define MAX_TOKENS_PER_COMMAND 8 41 | 42 | #endif 43 | 44 | #include "CLIToken.h" 45 | 46 | class CLICommand 47 | { 48 | public: 49 | CLICommand() 50 | { 51 | 52 | } 53 | 54 | /** 55 | @brief Resets this command to the empty state 56 | */ 57 | void Clear() 58 | { 59 | for(int i=0; i 38 | 39 | /** 40 | @brief An output stream for text content 41 | 42 | May be backed by a UART, socket, SSH session, or something else. 43 | 44 | Implementations of this class are expected to perform translation from \n to \r\n if required by the 45 | underlying output device. 46 | 47 | Provides a minimal printf-compatible output formatting helper that does not actually call the libc printf. 48 | This is important since printf in most embedded libc's can trigger a dynamic allocation. 49 | */ 50 | class CLIOutputStream : public CharacterDevice 51 | { 52 | public: 53 | 54 | void Backspace() 55 | { PutString("\b \b"); } 56 | 57 | void CursorLeft() 58 | { PutString("\x1b[D"); } 59 | 60 | void CursorRight() 61 | { PutString("\x1b[C"); } 62 | 63 | ///@brief CharacterDevice compatibility 64 | virtual void PrintBinary(char ch) override 65 | { PutCharacter(ch); } 66 | 67 | ///@brief CharacterDevice compatibility 68 | virtual char BlockingRead() 69 | { return 0; } 70 | 71 | /** 72 | @brief Prints a single character 73 | */ 74 | virtual void PutCharacter(char ch) =0; 75 | 76 | /** 77 | @brief Prints a string with no formatting 78 | */ 79 | virtual void PutString(const char* str) =0; 80 | 81 | /** 82 | @brief Flushes pending content so that it's displayed to the user. 83 | */ 84 | virtual void Flush() =0; 85 | 86 | virtual void Disconnect(); 87 | }; 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /CLISessionContext.cpp: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************************************** 2 | * * 3 | * embedded-cli * 4 | * * 5 | * Copyright (c) 2021-2024 Andrew D. Zonenberg and contributors * 6 | * All rights reserved. * 7 | * * 8 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * 9 | * following conditions are met: * 10 | * * 11 | * * Redistributions of source code must retain the above copyright notice, this list of conditions, and the * 12 | * following disclaimer. * 13 | * * 14 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * 15 | * following disclaimer in the documentation and/or other materials provided with the distribution. * 16 | * * 17 | * * Neither the name of the author nor the names of any contributors may be used to endorse or promote products * 18 | * derived from this software without specific prior written permission. * 19 | * * 20 | * THIS SOFTWARE IS PROVIDED BY THE AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * 21 | * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * 22 | * THE AUTHORS BE HELD LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * 24 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * 26 | * POSSIBILITY OF SUCH DAMAGE. * 27 | * * 28 | ***********************************************************************************************************************/ 29 | 30 | /** 31 | @file 32 | @brief Implementation of CLISessionContext 33 | */ 34 | #include "stdio.h" 35 | #include "CLISessionContext.h" 36 | #include "CLIOutputStream.h" 37 | #include 38 | #include 39 | 40 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 41 | // Setup 42 | 43 | void CLISessionContext::Initialize(CLIOutputStream* ctx, const char* username) 44 | { 45 | strncpy(m_username, username, CLI_USERNAME_MAX); 46 | m_username[CLI_USERNAME_MAX-1] = 0; 47 | m_command.Clear(); 48 | 49 | m_output = ctx; 50 | m_escapeState = STATE_NORMAL; 51 | 52 | m_lastToken = 0; 53 | m_currentToken = 0; 54 | m_tokenOffset = 0; 55 | } 56 | 57 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 58 | // Input handling 59 | 60 | /** 61 | @brief Handles an incoming keystroke 62 | */ 63 | void CLISessionContext::OnKeystroke(char c, bool echo) 64 | { 65 | //Square bracket in escape sequence 66 | if(m_escapeState == STATE_EXPECT_BRACKET) 67 | { 68 | if(c == '[') 69 | m_escapeState = STATE_EXPECT_PAYLOAD; 70 | 71 | //ignore malformed escape sequences 72 | else 73 | m_escapeState = STATE_NORMAL; 74 | 75 | return; 76 | } 77 | 78 | //Escape sequence payload 79 | else if(m_escapeState == STATE_EXPECT_PAYLOAD) 80 | { 81 | switch(c) 82 | { 83 | //B = down, A = up 84 | 85 | case 'C': 86 | OnArrowRight(); 87 | break; 88 | 89 | case 'D': 90 | OnArrowLeft(); 91 | break; 92 | 93 | //ignore unknown escape sequences 94 | default: 95 | break; 96 | } 97 | 98 | //escape sequence is over 99 | m_escapeState = STATE_NORMAL; 100 | } 101 | 102 | //Newline? Execute the command 103 | else if( (c == '\r') || (c == '\n') ) 104 | { 105 | if(echo) 106 | m_output->PutCharacter('\n'); 107 | OnLineReady(); 108 | if(ParseCommand()) 109 | OnExecute(); 110 | OnExecuteComplete(); 111 | } 112 | 113 | //Backspace? Delete the current character. 114 | //But we might have to move left a token 115 | else if( (c == '\b') || (c == '\x7f') ) 116 | OnBackspace(); 117 | 118 | //Tab? Do tab completion 119 | else if(c == '\t') 120 | OnTabComplete(); 121 | 122 | //Question mark? Print help text 123 | else if(c == '?') 124 | OnHelp(); 125 | 126 | //Space starts a new token 127 | else if(c == ' ') 128 | OnSpace(); 129 | 130 | //Start an escape sequence 131 | else if(c == '\x1b') 132 | m_escapeState = STATE_EXPECT_BRACKET; 133 | 134 | else 135 | OnChar(c, echo); 136 | 137 | //Update the last-token index 138 | for(size_t i=0; i m_lastToken) 150 | m_lastToken = m_currentToken; 151 | 152 | //All done with whatever we're printing, flush stdout 153 | m_output->Flush(); 154 | } 155 | 156 | /** 157 | @brief Parse and execute the current command without printing anything besides what the command generates 158 | 159 | Usable for scripting flows etc 160 | */ 161 | void CLISessionContext::SilentExecute() 162 | { 163 | OnLineReady(); 164 | if(ParseCommand()) 165 | OnExecute(); 166 | 167 | m_command.Clear(); 168 | 169 | m_lastToken = 0; 170 | m_currentToken = 0; 171 | m_tokenOffset = 0; 172 | } 173 | 174 | ///@brief Handles a printable character 175 | void CLISessionContext::OnChar(char c, bool echo) 176 | { 177 | //If the token doesn't have room for another character, abort 178 | char* token = m_command[m_currentToken].m_text; 179 | int len = strlen(token); 180 | if(len >= (MAX_TOKEN_LEN - 1)) 181 | return; 182 | 183 | //If we're NOT at the end of the token, we need to move everything right to make room for the new character 184 | bool redrawLine = (m_currentToken != m_lastToken); 185 | if(m_tokenOffset != len) 186 | { 187 | redrawLine = true; 188 | for(int i=len; i > m_tokenOffset; i--) 189 | token[i] = token[i-1]; 190 | } 191 | 192 | //Append the character to the current token and echo it 193 | token[m_tokenOffset ++] = c; 194 | len++; 195 | if(echo) 196 | m_output->PutCharacter(c); 197 | 198 | //Update the remainder of the line. 199 | if(redrawLine && echo) 200 | RedrawLineRightOfCursor(); 201 | } 202 | 203 | ///@brief Handles a tab character 204 | void CLISessionContext::OnTabComplete() 205 | { 206 | m_output->Printf("\n*** Tab complete unimplemented ***\n"); 207 | } 208 | 209 | ///@brief Handles a '?' character 210 | void CLISessionContext::OnHelp() 211 | { 212 | if(m_rootCommands == NULL) 213 | return; 214 | 215 | //If we have NO command, show all legal top level commands and descriptions 216 | if(m_command[0].IsEmpty()) 217 | { 218 | PrintHelp(m_rootCommands, NULL); 219 | return; 220 | } 221 | 222 | //Go through each token and figure out if it matches anything we know about 223 | const clikeyword_t* node = m_rootCommands; 224 | for(int i = 0; i < MAX_TOKENS_PER_COMMAND; i ++) 225 | { 226 | //If this is the current token, always search it 227 | if(i == m_currentToken) 228 | { 229 | PrintHelp(node, m_command[i].m_text); 230 | return; 231 | } 232 | 233 | //See what's legal at this position 234 | if(m_command[i].IsEmpty()) 235 | { 236 | if(node != NULL) 237 | { 238 | PrintHelp(node, NULL); 239 | return; 240 | } 241 | } 242 | 243 | for(auto row = node; row->keyword != NULL; row++) 244 | { 245 | //Wildcards always match 246 | if(row->id == FREEFORM_TOKEN) 247 | { 248 | } 249 | 250 | else 251 | { 252 | //If the token doesn't match the prefix, we're definitely not a hit 253 | if(!m_command[i].PrefixMatch(row->keyword)) 254 | continue; 255 | 256 | //If it matches, but the subsequent token matches too, there's a few options 257 | if(m_command[i].PrefixMatch(row[1].keyword)) 258 | { 259 | PrintHelp(node, m_command[i].m_text); 260 | return; 261 | } 262 | } 263 | 264 | //Match! 265 | m_command[i].m_commandID = row->id; 266 | node = row->children; 267 | } 268 | } 269 | } 270 | 271 | ///@brief Prints help 272 | void CLISessionContext::PrintHelp(const clikeyword_t* node, const char* prefix) 273 | { 274 | m_output->Printf("?\n"); 275 | 276 | //If node is null, there's nothing we can do 277 | if(!node) 278 | m_output->Printf(" No help available\n"); 279 | 280 | //Print the help text for matching commands 281 | else 282 | { 283 | bool filter = true; 284 | if( (prefix == nullptr) || (strlen(prefix) == 0) ) 285 | filter = false; 286 | 287 | m_output->Printf("?\n"); 288 | for(size_t i=0; node[i].keyword != nullptr; i++) 289 | { 290 | //Skip stuff with the wrong prefix 291 | if(filter) 292 | { 293 | if(node[i].keyword != strstr(node[i].keyword, prefix)) 294 | continue; 295 | } 296 | 297 | m_output->Printf(" %-20s %s\n", node[i].keyword, node[i].help); 298 | } 299 | } 300 | 301 | PrintPrompt(); 302 | 303 | //Re-print the current command 304 | //TODO: handle ? at anywhere other than end of command 305 | for(int i=0; i 0) 311 | m_output->Printf(" "); 312 | m_output->Printf("%s", m_command[i].m_text); 313 | } 314 | 315 | //If the current token is blank, add a trailing space 316 | if((m_currentToken > 0) && m_command[m_currentToken].IsEmpty()) 317 | m_output->Printf(" "); 318 | } 319 | 320 | ///@brief Handles a backspace character 321 | void CLISessionContext::OnBackspace() 322 | { 323 | //We're in the middle/end of a token 324 | if(m_tokenOffset > 0) 325 | { 326 | //Delete the character 327 | m_output->Backspace(); 328 | 329 | //Move back one character and shift the token left 330 | m_tokenOffset --; 331 | char* text = m_command[m_currentToken].m_text; 332 | for(int i=m_tokenOffset; i 0) 338 | { 339 | //Move before the space (already blank, no need to space over it) 340 | m_output->CursorLeft(); 341 | 342 | //Move to end of previous token 343 | m_currentToken --; 344 | m_tokenOffset = strlen(m_command[m_currentToken].m_text); 345 | 346 | //Merge the token we were in with the current one 347 | strncpy( 348 | m_command[m_currentToken].m_text + m_tokenOffset, 349 | m_command[m_currentToken+1].m_text, 350 | MAX_TOKEN_LEN - m_tokenOffset); 351 | 352 | //If we have any tokens to the right of us, move them left 353 | for(int i = m_currentToken+1; i < m_lastToken; i++) 354 | strncpy(m_command[i].m_text, m_command[i+1].m_text, MAX_TOKEN_LEN); 355 | 356 | //Blow away the removed token at the far right 357 | memset(m_command[m_lastToken].m_text, 0, MAX_TOKEN_LEN); 358 | } 359 | 360 | //Backspace at the start of the prompt. Ignore it. 361 | else 362 | { 363 | } 364 | 365 | RedrawLineRightOfCursor(); 366 | } 367 | 368 | ///@brief Handles a space character 369 | void CLISessionContext::OnSpace() 370 | { 371 | //If we're out of token space, stop 372 | if(m_lastToken >= (MAX_TOKENS_PER_COMMAND - 1) ) 373 | return; 374 | 375 | //Ignore consecutive spaces - if we already made an empty token between the existing ones, 376 | //there's no need to do another one 377 | if(m_command[m_currentToken].IsEmpty()) 378 | return; 379 | 380 | //OK, we're definitely adding a space. The only question now is where. 381 | m_output->PutCharacter(' ' ); 382 | 383 | //If we're at the end of the token, just insert a new token 384 | if(m_tokenOffset == m_command[m_currentToken].Length()) 385 | { 386 | //Not empty. Start a new token. 387 | m_currentToken ++; 388 | 389 | //If this was the last token, nothing more to do. 390 | if(m_currentToken > m_lastToken) 391 | { 392 | m_tokenOffset = 0; 393 | return; 394 | } 395 | 396 | //If we have tokens to the right, move them right and wipe the current one. 397 | for(int i = m_lastToken+1; i > m_currentToken; i-- ) 398 | strncpy(m_command[i].m_text, m_command[i-1].m_text, MAX_TOKEN_LEN); 399 | memset(m_command[m_currentToken].m_text, 0, MAX_TOKEN_LEN); 400 | 401 | m_tokenOffset = 0; 402 | } 403 | 404 | //We're in the middle of a token. Need to split it. 405 | else 406 | { 407 | //If we have tokens to the right, move them right 408 | if(m_currentToken < m_lastToken) 409 | { 410 | for(int i = m_lastToken+1; i > (m_currentToken+1); i-- ) 411 | strncpy(m_command[i].m_text, m_command[i-1].m_text, MAX_TOKEN_LEN); 412 | } 413 | 414 | //Move the right half of the split token into a new one at right 415 | strncpy( 416 | m_command[m_currentToken+1].m_text, 417 | m_command[m_currentToken].m_text + m_tokenOffset, 418 | MAX_TOKEN_LEN); 419 | 420 | //Truncate the left half of the split token 421 | for(size_t i=m_tokenOffset; i 0) 440 | { 441 | m_tokenOffset --; 442 | m_output->CursorLeft(); 443 | } 444 | 445 | //Move left, but go to the previous token 446 | else if(m_currentToken > 0) 447 | { 448 | m_output->CursorLeft(); 449 | m_currentToken --; 450 | m_tokenOffset = strlen(m_command[m_currentToken].m_text); 451 | } 452 | 453 | //Start of prompt, can't go left any further 454 | else 455 | { 456 | } 457 | } 458 | 459 | ///@brief Handles a right arrow key press 460 | void CLISessionContext::OnArrowRight() 461 | { 462 | //Are we at the end of the current token? 463 | char* text = m_command[m_currentToken].m_text; 464 | if(m_tokenOffset == (int)strlen(text)) 465 | { 466 | //Is this the last token? Can't go any further 467 | if(m_currentToken == m_lastToken) 468 | { 469 | } 470 | 471 | //Move to the start of the next one 472 | else 473 | { 474 | m_tokenOffset = 0; 475 | m_currentToken ++; 476 | m_output->CursorRight(); 477 | } 478 | } 479 | 480 | //Nope, just move one to the right 481 | else 482 | { 483 | m_tokenOffset ++; 484 | m_output->CursorRight(); 485 | } 486 | } 487 | 488 | ///@brief Prepares a line to be executed 489 | void CLISessionContext::OnLineReady() 490 | { 491 | //If we have any empty tokens, move stuff left to form a canonical command for execution 492 | for(int i=0; iPutString(restOfToken); 523 | 524 | //Draw all subsequent tokens 525 | for(int i = m_currentToken + 1; i < MAX_TOKENS_PER_COMMAND; i++) 526 | { 527 | char* tok = m_command[i].m_text; 528 | charsDrawn += strlen(tok) + 1; 529 | m_output->PutCharacter(' '); 530 | m_output->PutString(tok); 531 | } 532 | 533 | //Draw a space at the end to clean up anything we may have deleted 534 | m_output->PutCharacter(' '); 535 | charsDrawn ++; 536 | 537 | //Move the cursor back to where it belongs 538 | for(int i=0; iCursorLeft(); 540 | } 541 | 542 | /** 543 | @brief Parses a command to numeric command IDs 544 | */ 545 | bool CLISessionContext::ParseCommand() 546 | { 547 | if(m_rootCommands == NULL) 548 | return false; 549 | 550 | //Go through each token and figure out if it matches anything we know about 551 | const clikeyword_t* node = m_rootCommands; 552 | bool earlyOut = false; 553 | for(size_t i = 0; i < MAX_TOKENS_PER_COMMAND; i ++) 554 | { 555 | //If the node at the end of the command is not NULL, we're missing arguments! 556 | if(m_command[i].IsEmpty()) 557 | { 558 | if(node != NULL) 559 | { 560 | //See if there is an optional token at the start of the list 561 | //(if so, we can skip the unnecessary arguments) 562 | if(node->id == OPTIONAL_TOKEN) 563 | { 564 | m_command[i].m_commandID = OPTIONAL_TOKEN; 565 | break; 566 | } 567 | 568 | if(i > 0) 569 | m_output->Printf("Incomplete command: \"%s\" expects arguments\n", m_command[i-1].m_text); 570 | return false; 571 | } 572 | 573 | break; 574 | } 575 | 576 | //If node is null, give an error (too many arguments to command) 577 | if(node == NULL) 578 | { 579 | m_output->Printf("Too many arguments for \"%s\"\n", m_command[0].m_text); 580 | return false; 581 | } 582 | 583 | m_command[i].m_commandID = INVALID_COMMAND; 584 | 585 | for(auto row = node; row->keyword != NULL; row++) 586 | { 587 | //Wildcards always match. 588 | //Freeform token only consumes one token. 589 | //Text token consumes all subsequent input. 590 | if(row->id == TEXT_TOKEN) 591 | { 592 | for(size_t j=i+1; jid; 602 | node = nullptr; 603 | earlyOut = true; 604 | break; 605 | } 606 | else if(row->id == FREEFORM_TOKEN) 607 | { 608 | } 609 | 610 | else 611 | { 612 | //If the token doesn't match the prefix, we're definitely not a hit 613 | if(!m_command[i].PrefixMatch(row->keyword)) 614 | continue; 615 | 616 | //Check for an exact match 617 | if(m_command[i].ExactMatch(row->keyword)) 618 | { 619 | m_command[i].m_commandID = row->id; 620 | node = row->children; 621 | break; 622 | } 623 | 624 | //If it matches, but the subsequent token matches too, the command is ambiguous! 625 | //Fail with an error unless it's an exact match to the first command. 626 | else if(m_command[i].PrefixMatch(row[1].keyword)) 627 | { 628 | m_output->Printf("Ambiguous command: \"%s\" could mean \"%s\" or \"%s\"\n", 629 | m_command[i].m_text, 630 | row->keyword, 631 | row[1].keyword); 632 | return false; 633 | } 634 | } 635 | 636 | //Match! 637 | m_command[i].m_commandID = row->id; 638 | node = row->children; 639 | } 640 | 641 | if(earlyOut) 642 | break; 643 | 644 | //Didn't match anything at all, give up 645 | if(m_command[i].m_commandID == INVALID_COMMAND) 646 | { 647 | m_output->Printf("Unrecognized command: \"%s\"\n", m_command[i].m_text); 648 | return false; 649 | } 650 | 651 | } 652 | 653 | //all good 654 | return true; 655 | } 656 | -------------------------------------------------------------------------------- /CLISessionContext.h: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************************************** 2 | * * 3 | * embedded-cli * 4 | * * 5 | * Copyright (c) 2021-2024 Andrew D. Zonenberg and contributors * 6 | * All rights reserved. * 7 | * * 8 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * 9 | * following conditions are met: * 10 | * * 11 | * * Redistributions of source code must retain the above copyright notice, this list of conditions, and the * 12 | * following disclaimer. * 13 | * * 14 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * 15 | * following disclaimer in the documentation and/or other materials provided with the distribution. * 16 | * * 17 | * * Neither the name of the author nor the names of any contributors may be used to endorse or promote products * 18 | * derived from this software without specific prior written permission. * 19 | * * 20 | * THIS SOFTWARE IS PROVIDED BY THE AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * 21 | * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * 22 | * THE AUTHORS BE HELD LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * 24 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * 26 | * POSSIBILITY OF SUCH DAMAGE. * 27 | * * 28 | ***********************************************************************************************************************/ 29 | 30 | /** 31 | @file 32 | @brief Declaration of CLISessionContext 33 | */ 34 | #ifndef CLISessionContext_h 35 | #define CLISessionContext_h 36 | 37 | #include 38 | #include "CLICommand.h" 39 | 40 | class CLIOutputStream; 41 | 42 | #ifndef CLI_USERNAME_MAX 43 | #define CLI_USERNAME_MAX 32 44 | #endif 45 | 46 | /** 47 | @brief A single keyword in the CLI command tree 48 | */ 49 | struct clikeyword_t 50 | { 51 | ///@brief ASCII representation of the unabbreviated keyword 52 | const char* keyword; 53 | 54 | ///@brief Integer identifier used by the command parser 55 | uint16_t id; 56 | 57 | ///@brief Child nodes for subsequent words 58 | const clikeyword_t* children; 59 | 60 | ///@brief Help message 61 | const char* help; 62 | }; 63 | 64 | /** 65 | @brief A session context for a CLI session 66 | */ 67 | class CLISessionContext 68 | { 69 | public: 70 | CLISessionContext(const clikeyword_t* root) 71 | : m_rootCommands(root) 72 | {} 73 | 74 | virtual void Initialize(CLIOutputStream* ctx, const char* username); 75 | 76 | void OnKeystroke(char c, bool echo = true); 77 | 78 | /** 79 | @brief Prints the command prompt 80 | */ 81 | virtual void PrintPrompt() =0; 82 | 83 | void SilentExecute(); 84 | 85 | protected: 86 | 87 | ///@brief Handles a line of input being fully entered 88 | virtual void OnExecute() =0; 89 | 90 | void RedrawLineRightOfCursor(); 91 | 92 | void OnExecuteComplete(); 93 | 94 | void OnBackspace(); 95 | void OnTabComplete(); 96 | void OnSpace(); 97 | void OnChar(char c, bool echo = true); 98 | void OnArrowLeft(); 99 | void OnArrowRight(); 100 | void OnLineReady(); 101 | void OnHelp(); 102 | void PrintHelp(const clikeyword_t* node, const char* prefix); 103 | 104 | bool ParseCommand(); 105 | 106 | ///@brief The output stream 107 | CLIOutputStream* m_output; 108 | 109 | ///@brief The command currently being 110 | CLICommand m_command; 111 | 112 | /** 113 | @brief Name of the currently logged in user 114 | 115 | Provided by upper layer in case of e.g. SSH. May be blank in case of UART or other transports that do not 116 | include authentication. 117 | */ 118 | char m_username[CLI_USERNAME_MAX]; 119 | 120 | ///@brief State machine for escape sequence parsing 121 | enum 122 | { 123 | STATE_NORMAL, 124 | STATE_EXPECT_BRACKET, 125 | STATE_EXPECT_PAYLOAD 126 | } m_escapeState; 127 | 128 | ///@brief Index of the last token in the command 129 | int m_lastToken; 130 | 131 | ///@brief Index of the token we're currently writing to 132 | int m_currentToken; 133 | 134 | ///@brief Position of the cursor within the current token 135 | int m_tokenOffset; 136 | 137 | ///@brief The root of the command tree 138 | const clikeyword_t* m_rootCommands; 139 | }; 140 | 141 | #endif 142 | -------------------------------------------------------------------------------- /CLIToken.cpp: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************************************** 2 | * * 3 | * embedded-cli v0.1 * 4 | * * 5 | * Copyright (c) 2021-2023 Andrew D. Zonenberg and contributors * 6 | * All rights reserved. * 7 | * * 8 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * 9 | * following conditions are met: * 10 | * * 11 | * * Redistributions of source code must retain the above copyright notice, this list of conditions, and the * 12 | * following disclaimer. * 13 | * * 14 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * 15 | * following disclaimer in the documentation and/or other materials provided with the distribution. * 16 | * * 17 | * * Neither the name of the author nor the names of any contributors may be used to endorse or promote products * 18 | * derived from this software without specific prior written permission. * 19 | * * 20 | * THIS SOFTWARE IS PROVIDED BY THE AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * 21 | * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * 22 | * THE AUTHORS BE HELD LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * 24 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * 26 | * POSSIBILITY OF SUCH DAMAGE. * 27 | * * 28 | ***********************************************************************************************************************/ 29 | 30 | /** 31 | @file 32 | @brief Implementation of CLIToken 33 | */ 34 | #include "stdio.h" 35 | #include "CLIToken.h" 36 | #include 37 | #include 38 | 39 | /** 40 | @brief Checks if a short-form command matches this token 41 | */ 42 | bool CLIToken::PrefixMatch(const char* fullcommand) 43 | { 44 | //Null is legal to pass if we match against the end of the token array. 45 | //It never matches, since it's not a valid command 46 | if(fullcommand == NULL) 47 | return false; 48 | 49 | for(size_t i = 0; i < MAX_TOKEN_LEN; i++) 50 | { 51 | //End of input with no mismatches? It's a match 52 | if(m_text[i] == '\0') 53 | return true; 54 | 55 | //Early-out on the first mismatch 56 | if(m_text[i] != fullcommand[i]) 57 | return false; 58 | } 59 | 60 | return true; 61 | } 62 | 63 | /** 64 | @brief Checks if a command exactly matches this token 65 | */ 66 | bool CLIToken::ExactMatch(const char* fullcommand) 67 | { 68 | //Null is legal to pass if we match against the end of the token array. 69 | //It never matches, since it's not a valid command 70 | if(fullcommand == NULL) 71 | return false; 72 | 73 | return (0 == strcmp(m_text, fullcommand) ); 74 | } 75 | -------------------------------------------------------------------------------- /CLIToken.h: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************************************** 2 | * * 3 | * embedded-cli v0.1 * 4 | * * 5 | * Copyright (c) 2021-2023 Andrew D. Zonenberg and contributors * 6 | * All rights reserved. * 7 | * * 8 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * 9 | * following conditions are met: * 10 | * * 11 | * * Redistributions of source code must retain the above copyright notice, this list of conditions, and the * 12 | * following disclaimer. * 13 | * * 14 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * 15 | * following disclaimer in the documentation and/or other materials provided with the distribution. * 16 | * * 17 | * * Neither the name of the author nor the names of any contributors may be used to endorse or promote products * 18 | * derived from this software without specific prior written permission. * 19 | * * 20 | * THIS SOFTWARE IS PROVIDED BY THE AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * 21 | * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * 22 | * THE AUTHORS BE HELD LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * 24 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * 26 | * POSSIBILITY OF SUCH DAMAGE. * 27 | * * 28 | ***********************************************************************************************************************/ 29 | 30 | /** 31 | @file 32 | @brief Declaration of CLIToken 33 | */ 34 | #ifndef CLIToken_h 35 | #define CLIToken_h 36 | 37 | #include 38 | #include 39 | 40 | #ifndef MAX_TOKEN_LEN 41 | 42 | ///@brief Maximum number of characters in a single token 43 | #define MAX_TOKEN_LEN 32 44 | 45 | #endif 46 | 47 | ///@brief Empty string or otherwise malformed 48 | #define INVALID_COMMAND 0xffff 49 | 50 | ///@brief This token can consist of arbitrary freeform text without spaces 51 | #define FREEFORM_TOKEN 0xfffe 52 | 53 | ///@brief This token denotes an early end-of-command (for optional arguments) 54 | #define OPTIONAL_TOKEN 0xfffd 55 | 56 | ///@brief This token consumes all input to the end of the command line (including spaces) 57 | #define TEXT_TOKEN 0xfffc 58 | 59 | /** 60 | @brief A single token within a command 61 | */ 62 | class CLIToken 63 | { 64 | public: 65 | 66 | CLIToken() 67 | { 68 | Clear(); 69 | } 70 | 71 | /** 72 | @brief Returns true if the token is empty 73 | */ 74 | bool IsEmpty() 75 | { return strlen(m_text) == 0; } 76 | 77 | /** 78 | @brief Returns the length of this token, in characters 79 | */ 80 | int Length() 81 | { return strlen(m_text); } 82 | 83 | /** 84 | @brief Resets this token to the empty state 85 | */ 86 | void Clear() 87 | { 88 | memset(m_text, 0, MAX_TOKEN_LEN); 89 | m_commandID = INVALID_COMMAND; 90 | } 91 | 92 | /** 93 | @brief Helper operator for string matching 94 | */ 95 | bool operator==(const char* rhs) 96 | { return strcmp(m_text, rhs) == 0; } 97 | 98 | bool PrefixMatch(const char* fullcommand); 99 | bool ExactMatch(const char* fullcommand); 100 | 101 | public: 102 | 103 | ///@brief Text representation of the token 104 | char m_text[MAX_TOKEN_LEN]; 105 | 106 | ///@brief Parsed command ID (if matched) or INVALID_TOKEN otherwise 107 | uint16_t m_commandID; 108 | }; 109 | 110 | #endif 111 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # CMake build script for embedded-cli. 2 | # Intended to be integrated into a larger project, not built standalone. 3 | 4 | add_library(embedded-cli STATIC 5 | 6 | # TODO: only for stm32 targets? 7 | ../stm32-cpp/src/cli/UARTOutputStream.cpp 8 | 9 | CLIOutputStream.cpp 10 | CLISessionContext.cpp 11 | CLIToken.cpp 12 | ) 13 | 14 | # TODO: only for stm32 targets? 15 | target_include_directories(embedded-cli 16 | PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} 17 | "$" 18 | ) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Andrew Zonenberg 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This is a simple C++ library providing a framework for a command-line interface on an embedded device. 4 | 5 | It supports arbitrary byte-oriented transports and is intended to work over raw UARTs, SSH connections, 6 | and more. 7 | 8 | This library performs no dynamic memory allocation, and does not call any C/C++ library functions which 9 | may trigger a dynamic allocation. 10 | 11 | The CLI supports shortest-unique-prefix completion, similar to that used by most networking equipment. 12 | --------------------------------------------------------------------------------