/** * This header was automatically built using * embedded_cli.h and embedded_cli.c * @date 2022-11-03 * * MIT License * * Copyright (c) 2021 Sviatoslav Kokurin (funbiscuit) * * 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: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * 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. */ #ifndef EMBEDDED_CLI_H #define EMBEDDED_CLI_H #ifdef __cplusplus extern "C" { #else #include #endif // cstdint is available only since C++11, so use C header #include // used for proper alignment of cli buffer #if UINTPTR_MAX == 0xFFFF #define CLI_UINT uint16_t #elif UINTPTR_MAX == 0xFFFFFFFF #define CLI_UINT uint32_t #elif UINTPTR_MAX == 0xFFFFFFFFFFFFFFFFu #define CLI_UINT uint64_t #else #error unsupported pointer size #endif #define CLI_UINT_SIZE (sizeof(CLI_UINT)) // convert size in bytes to size in terms of CLI_UINTs (rounded up // if bytes is not divisible by size of single CLI_UINT) #define BYTES_TO_CLI_UINTS(bytes) \ (((bytes) + CLI_UINT_SIZE - 1)/CLI_UINT_SIZE) typedef struct CliCommand CliCommand; typedef struct CliCommandBinding CliCommandBinding; typedef struct EmbeddedCli EmbeddedCli; typedef struct EmbeddedCliConfig EmbeddedCliConfig; struct CliCommand { /** * Name of the command. * In command "set led 1 1" "set" is name */ const char *name; /** * String of arguments of the command. * In command "set led 1 1" "led 1 1" is string of arguments * Is ended with double 0x00 char * Use tokenize functions to easily get individual tokens */ char *args; }; /** * Struct to describe binding of command to function and */ struct CliCommandBinding { /** * Name of command to bind. Should not be NULL. */ const char *name; /** * Help string that will be displayed when "help " is executed. * Can have multiple lines separated with "\r\n" * Can be NULL if no help is provided. */ const char *help; /** * Flag to perform tokenization before calling binding function. */ bool tokenizeArgs; /** * Pointer to any specific app context that is required for this binding. * It will be provided in binding callback. */ void *context; /** * Binding function for when command is received. * If null, default callback (onCommand) will be called. * @param cli - pointer to cli that is calling this binding * @param args - string of args (if tokenizeArgs is false) or tokens otherwise * @param context */ void (*binding)(EmbeddedCli *cli, char *args, void *context); }; struct EmbeddedCli { /** * Should write char to connection * @param cli - pointer to cli that executed this function * @param c - actual character to write */ void (*writeChar)(EmbeddedCli *cli, char c); /** * Called when command is received and command not found in list of * command bindings (or binding function is null). * @param cli - pointer to cli that executed this function * @param command - pointer to received command */ void (*onCommand)(EmbeddedCli *cli, CliCommand *command); /** * Can be used by for any application context */ void *appContext; /** * Pointer to actual implementation, do not use. */ void *_impl; }; /** * Configuration to create CLI */ struct EmbeddedCliConfig { /** * Size of buffer that is used to store characters until they're processed */ uint16_t rxBufferSize; /** * Size of buffer that is used to store current input that is not yet * sended as command (return not pressed yet) */ uint16_t cmdBufferSize; /** * Size of buffer that is used to store previously entered commands * Only unique commands are stored in buffer. If buffer is smaller than * entered command (including arguments), command is discarded from history */ uint16_t historyBufferSize; /** * Maximum amount of bindings that can be added via addBinding function. * Cli increases takes extra bindings for internal commands: * - help */ uint16_t maxBindingCount; /** * Buffer to use for cli and all internal structures. If NULL, memory will * be allocated dynamically. Otherwise this buffer is used and no * allocations are made */ CLI_UINT *cliBuffer; /** * Size of buffer for cli and internal structures (in bytes). */ uint16_t cliBufferSize; /** * Whether autocompletion should be enabled. * If false, autocompletion is disabled but you still can use 'tab' to * complete current command manually. */ bool enableAutoComplete; }; /** * Returns pointer to default configuration for cli creation. It is safe to * modify it and then send to embeddedCliNew(). * Returned structure is always the same so do not free and try to use it * immediately. * Default values: *
    *
  • rxBufferSize = 64
  • *
  • cmdBufferSize = 64
  • *
  • historyBufferSize = 128
  • *
  • cliBuffer = NULL (use dynamic allocation)
  • *
  • cliBufferSize = 0
  • *
  • maxBindingCount = 8
  • *
  • enableAutoComplete = true
  • *
* @return configuration for cli creation */ EmbeddedCliConfig *embeddedCliDefaultConfig(void); /** * Returns how many space in config buffer is required for cli creation * If you provide buffer with less space, embeddedCliNew will return NULL * This amount will always be divisible by CLI_UINT_SIZE so allocated buffer * and internal structures can be properly aligned * @param config * @return */ uint16_t embeddedCliRequiredSize(EmbeddedCliConfig *config); /** * Create new CLI. * Memory is allocated dynamically if cliBuffer in config is NULL. * After CLI is created, override function pointers to start using it * @param config - config for cli creation * @return pointer to created CLI */ EmbeddedCli *embeddedCliNew(EmbeddedCliConfig *config); /** * Same as calling embeddedCliNew with default config. * @return */ EmbeddedCli *embeddedCliNewDefault(void); /** * Receive character and put it to internal buffer * Actual processing is done inside embeddedCliProcess * You can call this function from something like interrupt service routine, * just make sure that you call it only from single place. Otherwise input * might get corrupted * @param cli * @param c - received char */ void embeddedCliReceiveChar(EmbeddedCli *cli, char c); /** * Process rx/tx buffers. Command callbacks are called from here * @param cli */ void embeddedCliProcess(EmbeddedCli *cli); /** * Add specified binding to list of bindings. If list is already full, binding * is not added and false is returned * @param cli * @param binding * @return true if binding was added, false otherwise */ bool embeddedCliAddBinding(EmbeddedCli *cli, CliCommandBinding binding); /** * Print specified string and account for currently entered but not submitted * command. * Current command is deleted, provided string is printed (with new line) after * that current command is printed again, so user can continue typing it. * @param cli * @param string */ void embeddedCliPrint(EmbeddedCli *cli, const char *string); /** * Free allocated for cli memory * @param cli */ void embeddedCliFree(EmbeddedCli *cli); /** * Perform tokenization of arguments string. Original string is modified and * should not be used directly (only inside other token functions). * Individual tokens are separated by single 0x00 char, double 0x00 is put at * the end of token list. After calling this function, you can use other * token functions to get individual tokens and token count. * * Important: Call this function only once. Otherwise information will be lost if * more than one token existed * @param args - string to tokenize (must have extra writable char after 0x00) * @return */ void embeddedCliTokenizeArgs(char *args); /** * Return specific token from tokenized string * @param tokenizedStr * @param pos (counted from 1) * @return token */ const char *embeddedCliGetToken(const char *tokenizedStr, uint16_t pos); /** * Same as embeddedCliGetToken but works on non-const buffer * @param tokenizedStr * @param pos (counted from 1) * @return token */ char *embeddedCliGetTokenVariable(char *tokenizedStr, uint16_t pos); /** * Find token in provided tokens string and return its position (counted from 1) * If no such token is found - 0 is returned. * @param tokenizedStr * @param token - token to find * @return position (increased by 1) or zero if no such token found */ uint16_t embeddedCliFindToken(const char *tokenizedStr, const char *token); /** * Return number of tokens in tokenized string * @param tokenizedStr * @return number of tokens */ uint16_t embeddedCliGetTokenCount(const char *tokenizedStr); #ifdef __cplusplus } #endif #endif //EMBEDDED_CLI_H #ifdef EMBEDDED_CLI_IMPL #ifndef EMBEDDED_CLI_IMPL_GUARD #define EMBEDDED_CLI_IMPL_GUARD #ifdef __cplusplus extern "C" { #endif #include #include #define CLI_TOKEN_NPOS 0xffff #define UNUSED(x) (void)x #define PREPARE_IMPL(t) \ EmbeddedCliImpl* impl = (EmbeddedCliImpl*)t->_impl #define IS_FLAG_SET(flags, flag) (((flags) & (flag)) != 0) #define SET_FLAG(flags, flag) ((flags) |= (flag)) #define UNSET_U8FLAG(flags, flag) ((flags) &= (uint8_t) ~(flag)) /** * Marks binding as candidate for autocompletion * This flag is updated each time getAutocompletedCommand is called */ #define BINDING_FLAG_AUTOCOMPLETE 1u /** * Indicates that rx buffer overflow happened. In such case last command * that wasn't finished (no \r or \n were received) will be discarded */ #define CLI_FLAG_OVERFLOW 0x01u /** * Indicates that initialization is completed. Initialization is completed in * first call to process and needed, for example, to print invitation message. */ #define CLI_FLAG_INIT_COMPLETE 0x02u /** * Indicates that CLI structure and internal structures were allocated with * malloc and should bre freed */ #define CLI_FLAG_ALLOCATED 0x04u /** * Indicates that CLI structure and internal structures were allocated with * malloc and should bre freed */ #define CLI_FLAG_ESCAPE_MODE 0x08u /** * Indicates that CLI in mode when it will print directly to output without * clear of current command and printing it back */ #define CLI_FLAG_DIRECT_PRINT 0x10u /** * Indicates that live autocompletion is enabled */ #define CLI_FLAG_AUTOCOMPLETE_ENABLED 0x20u typedef struct EmbeddedCliImpl EmbeddedCliImpl; typedef struct AutocompletedCommand AutocompletedCommand; typedef struct FifoBuf FifoBuf; typedef struct CliHistory CliHistory; struct FifoBuf { char *buf; /** * Position of first element in buffer. From this position elements are taken */ uint16_t front; /** * Position after last element. At this position new elements are inserted */ uint16_t back; /** * Size of buffer */ uint16_t size; }; struct CliHistory { /** * Items in buffer are separated by null-chars */ char *buf; /** * Total size of buffer */ uint16_t bufferSize; /** * Index of currently selected element. This allows to navigate history * After command is sent, current element is reset to 0 (no element) */ uint16_t current; /** * Number of items in buffer * Items are counted from top to bottom (and are 1 based). * So the most recent item is 1 and the oldest is itemCount. */ uint16_t itemsCount; }; struct EmbeddedCliImpl { /** * Invitation string. Is printed at the beginning of each line with user * input */ const char *invitation; CliHistory history; /** * Buffer for storing received chars. * Chars are stored in FIFO mode. */ FifoBuf rxBuffer; /** * Buffer for current command */ char *cmdBuffer; /** * Size of current command */ uint16_t cmdSize; /** * Total size of command buffer */ uint16_t cmdMaxSize; CliCommandBinding *bindings; /** * Flags for each binding. Sizes are the same as for bindings array */ uint8_t *bindingsFlags; uint16_t bindingsCount; uint16_t maxBindingsCount; /** * Total length of input line. This doesn't include invitation but * includes current command and its live autocompletion */ uint16_t inputLineLength; /** * Stores last character that was processed. */ char lastChar; /** * Flags are defined as CLI_FLAG_* */ uint8_t flags; }; struct AutocompletedCommand { /** * Name of autocompleted command (or first candidate for autocompletion if * there are multiple candidates). * NULL if autocomplete not possible. */ const char *firstCandidate; /** * Number of characters that can be completed safely. For example, if there * are two possible commands "get-led" and "get-adc", then for prefix "g" * autocompletedLen will be 4. If there are only one candidate, this number * is always equal to length of the command. */ uint16_t autocompletedLen; /** * Total number of candidates for autocompletion */ uint16_t candidateCount; }; static EmbeddedCliConfig defaultConfig; /** * Number of commands that cli adds. Commands: * - help */ static const uint16_t cliInternalBindingCount = 1; static const char *lineBreak = "\r\n"; /** * Navigate through command history back and forth. If navigateUp is true, * navigate to older commands, otherwise navigate to newer. * When history end is reached, nothing happens. * @param cli * @param navigateUp */ static void navigateHistory(EmbeddedCli *cli, bool navigateUp); /** * Process escaped character. After receiving ESC+[ sequence, all chars up to * ending character are sent to this function * @param cli * @param c */ static void onEscapedInput(EmbeddedCli *cli, char c); /** * Process input character. Character is valid displayable char and should be * added to current command string and displayed to client. * @param cli * @param c */ static void onCharInput(EmbeddedCli *cli, char c); /** * Process control character (like \r or \n) possibly altering state of current * command or executing onCommand callback. * @param cli * @param c */ static void onControlInput(EmbeddedCli *cli, char c); /** * Parse command in buffer and execute callback * @param cli */ static void parseCommand(EmbeddedCli *cli); /** * Setup bindings for internal commands, like help * @param cli */ static void initInternalBindings(EmbeddedCli *cli); /** * Show help for given tokens (or default help if no tokens) * @param cli * @param tokens * @param context - not used */ static void onHelp(EmbeddedCli *cli, char *tokens, void *context); /** * Show error about unknown command * @param cli * @param name */ static void onUnknownCommand(EmbeddedCli *cli, const char *name); /** * Return autocompleted command for given prefix. * Prefix is compared to all known command bindings and autocompleted result * is returned * @param cli * @param prefix * @return */ static AutocompletedCommand getAutocompletedCommand(EmbeddedCli *cli, const char *prefix); /** * Prints autocompletion result while keeping current command unchanged * Prints only if autocompletion is present and only one candidate exists. * @param cli */ static void printLiveAutocompletion(EmbeddedCli *cli); /** * Handles autocomplete request. If autocomplete possible - fills current * command with autocompleted command. When multiple commands satisfy entered * prefix, they are printed to output. * @param cli */ static void onAutocompleteRequest(EmbeddedCli *cli); /** * Removes all input from current line (replaces it with whitespaces) * And places cursor at the beginning of the line * @param cli */ static void clearCurrentLine(EmbeddedCli *cli); /** * Write given string to cli output * @param cli * @param str */ static void writeToOutput(EmbeddedCli *cli, const char *str); /** * Returns true if provided char is a supported control char: * \r, \n, \b or 0x7F (treated as \b) * @param c * @return */ static bool isControlChar(char c); /** * Returns true if provided char is a valid displayable character: * a-z, A-Z, 0-9, whitespace, punctuation, etc. * Currently only ASCII is supported * @param c * @return */ static bool isDisplayableChar(char c); /** * How many elements are currently available in buffer * @param buffer * @return number of elements */ static uint16_t fifoBufAvailable(FifoBuf *buffer); /** * Return first character from buffer and remove it from buffer * Buffer must be non-empty, otherwise 0 is returned * @param buffer * @return */ static char fifoBufPop(FifoBuf *buffer); /** * Push character into fifo buffer. If there is no space left, character is * discarded and false is returned * @param buffer * @param a - character to add * @return true if char was added to buffer, false otherwise */ static bool fifoBufPush(FifoBuf *buffer, char a); /** * Copy provided string to the history buffer. * If it is already inside history, it will be removed from it and added again. * So after addition, it will always be on top * If available size is not enough (and total size is enough) old elements will * be removed from history so this item can be put to it * @param history * @param str * @return true if string was put in history */ static bool historyPut(CliHistory *history, const char *str); /** * Get item from history. Items are counted from 1 so if item is 0 or greater * than itemCount, NULL is returned * @param history * @param item * @return true if string was put in history */ static const char *historyGet(CliHistory *history, uint16_t item); /** * Remove specific item from history * @param history * @param str - string to remove * @return */ static void historyRemove(CliHistory *history, const char *str); /** * Return position (index of first char) of specified token * @param tokenizedStr - tokenized string (separated by \0 with * \0\0 at the end) * @param pos - token position (counted from 1) * @return index of first char of specified token */ static uint16_t getTokenPosition(const char *tokenizedStr, uint16_t pos); EmbeddedCliConfig *embeddedCliDefaultConfig(void) { defaultConfig.rxBufferSize = 64; defaultConfig.cmdBufferSize = 64; defaultConfig.historyBufferSize = 128; defaultConfig.cliBuffer = NULL; defaultConfig.cliBufferSize = 0; defaultConfig.maxBindingCount = 8; defaultConfig.enableAutoComplete = true; return &defaultConfig; } uint16_t embeddedCliRequiredSize(EmbeddedCliConfig *config) { uint16_t bindingCount = (uint16_t) (config->maxBindingCount + cliInternalBindingCount); return (uint16_t) (CLI_UINT_SIZE * ( BYTES_TO_CLI_UINTS(sizeof(EmbeddedCli)) + BYTES_TO_CLI_UINTS(sizeof(EmbeddedCliImpl)) + BYTES_TO_CLI_UINTS(config->rxBufferSize * sizeof(char)) + BYTES_TO_CLI_UINTS(config->cmdBufferSize * sizeof(char)) + BYTES_TO_CLI_UINTS(config->historyBufferSize * sizeof(char)) + BYTES_TO_CLI_UINTS(bindingCount * sizeof(CliCommandBinding)) + BYTES_TO_CLI_UINTS(bindingCount * sizeof(uint8_t)))); } EmbeddedCli *embeddedCliNew(EmbeddedCliConfig *config) { EmbeddedCli *cli = NULL; uint16_t bindingCount = (uint16_t) (config->maxBindingCount + cliInternalBindingCount); size_t totalSize = embeddedCliRequiredSize(config); bool allocated = false; if (config->cliBuffer == NULL) { config->cliBuffer = (CLI_UINT *) malloc(totalSize); // malloc guarantees alignment. if (config->cliBuffer == NULL) return NULL; allocated = true; } else if (config->cliBufferSize < totalSize) { return NULL; } CLI_UINT *buf = config->cliBuffer; memset(buf, 0, totalSize); cli = (EmbeddedCli *) buf; buf += BYTES_TO_CLI_UINTS(sizeof(EmbeddedCli)); cli->_impl = (EmbeddedCliImpl *) buf; buf += BYTES_TO_CLI_UINTS(sizeof(EmbeddedCliImpl)); PREPARE_IMPL(cli); impl->rxBuffer.buf = (char *) buf; buf += BYTES_TO_CLI_UINTS(config->rxBufferSize * sizeof(char)); impl->cmdBuffer = (char *) buf; buf += BYTES_TO_CLI_UINTS(config->cmdBufferSize * sizeof(char)); impl->bindings = (CliCommandBinding *) buf; buf += BYTES_TO_CLI_UINTS(bindingCount * sizeof(CliCommandBinding)); impl->bindingsFlags = (uint8_t *) buf; buf += BYTES_TO_CLI_UINTS(bindingCount); impl->history.buf = (char *) buf; impl->history.bufferSize = config->historyBufferSize; if (allocated) SET_FLAG(impl->flags, CLI_FLAG_ALLOCATED); if (config->enableAutoComplete) SET_FLAG(impl->flags, CLI_FLAG_AUTOCOMPLETE_ENABLED); impl->rxBuffer.size = config->rxBufferSize; impl->rxBuffer.front = 0; impl->rxBuffer.back = 0; impl->cmdMaxSize = config->cmdBufferSize; impl->bindingsCount = 0; impl->maxBindingsCount = (uint16_t) (config->maxBindingCount + cliInternalBindingCount); impl->lastChar = '\0'; impl->invitation = "> "; initInternalBindings(cli); return cli; } EmbeddedCli *embeddedCliNewDefault(void) { return embeddedCliNew(embeddedCliDefaultConfig()); } void embeddedCliReceiveChar(EmbeddedCli *cli, char c) { PREPARE_IMPL(cli); if (!fifoBufPush(&impl->rxBuffer, c)) { SET_FLAG(impl->flags, CLI_FLAG_OVERFLOW); } } void embeddedCliProcess(EmbeddedCli *cli) { if (cli->writeChar == NULL) return; PREPARE_IMPL(cli); if (!IS_FLAG_SET(impl->flags, CLI_FLAG_INIT_COMPLETE)) { SET_FLAG(impl->flags, CLI_FLAG_INIT_COMPLETE); writeToOutput(cli, impl->invitation); } while (fifoBufAvailable(&impl->rxBuffer)) { char c = fifoBufPop(&impl->rxBuffer); if (IS_FLAG_SET(impl->flags, CLI_FLAG_ESCAPE_MODE)) { onEscapedInput(cli, c); } else if (impl->lastChar == 0x1B && c == '[') { //enter escape mode SET_FLAG(impl->flags, CLI_FLAG_ESCAPE_MODE); } else if (isControlChar(c)) { onControlInput(cli, c); } else if (isDisplayableChar(c)) { onCharInput(cli, c); } printLiveAutocompletion(cli); impl->lastChar = c; } // discard unfinished command if overflow happened if (IS_FLAG_SET(impl->flags, CLI_FLAG_OVERFLOW)) { impl->cmdSize = 0; impl->cmdBuffer[impl->cmdSize] = '\0'; UNSET_U8FLAG(impl->flags, CLI_FLAG_OVERFLOW); } } bool embeddedCliAddBinding(EmbeddedCli *cli, CliCommandBinding binding) { PREPARE_IMPL(cli); if (impl->bindingsCount == impl->maxBindingsCount) return false; impl->bindings[impl->bindingsCount] = binding; ++impl->bindingsCount; return true; } void embeddedCliPrint(EmbeddedCli *cli, const char *string) { if (cli->writeChar == NULL) return; PREPARE_IMPL(cli); // remove chars for autocompletion and live command if (!IS_FLAG_SET(impl->flags, CLI_FLAG_DIRECT_PRINT)) clearCurrentLine(cli); // print provided string writeToOutput(cli, string); writeToOutput(cli, lineBreak); // print current command back to screen if (!IS_FLAG_SET(impl->flags, CLI_FLAG_DIRECT_PRINT)) { writeToOutput(cli, impl->invitation); writeToOutput(cli, impl->cmdBuffer); impl->inputLineLength = impl->cmdSize; printLiveAutocompletion(cli); } } void embeddedCliFree(EmbeddedCli *cli) { PREPARE_IMPL(cli); if (IS_FLAG_SET(impl->flags, CLI_FLAG_ALLOCATED)) { // allocation is done in single call to malloc, so need only single free free(cli); } } void embeddedCliTokenizeArgs(char *args) { if (args == NULL) return; // for now only space, but can add more later const char *separators = " "; // indicates that arg is quoted so separators are copied as is bool quotesEnabled = false; // indicates that previous char was a slash, so next char is copied as is bool escapeActivated = false; int insertPos = 0; int i = 0; char currentChar; while ((currentChar = args[i]) != '\0') { ++i; if (escapeActivated) { escapeActivated = false; } else if (currentChar == '\\') { escapeActivated = true; continue; } else if (currentChar == '"') { quotesEnabled = !quotesEnabled; currentChar = '\0'; } else if (!quotesEnabled && strchr(separators, currentChar) != NULL) { currentChar = '\0'; } // null chars are only copied once and not copied to the beginning if (currentChar != '\0' || (insertPos > 0 && args[insertPos - 1] != '\0')) { args[insertPos] = currentChar; ++insertPos; } } // make args double null-terminated source buffer must be big enough to contain extra spaces args[insertPos] = '\0'; args[insertPos + 1] = '\0'; } const char *embeddedCliGetToken(const char *tokenizedStr, uint16_t pos) { uint16_t i = getTokenPosition(tokenizedStr, pos); if (i != CLI_TOKEN_NPOS) return &tokenizedStr[i]; else return NULL; } char *embeddedCliGetTokenVariable(char *tokenizedStr, uint16_t pos) { uint16_t i = getTokenPosition(tokenizedStr, pos); if (i != CLI_TOKEN_NPOS) return &tokenizedStr[i]; else return NULL; } uint16_t embeddedCliFindToken(const char *tokenizedStr, const char *token) { if (tokenizedStr == NULL || token == NULL) return 0; uint16_t size = embeddedCliGetTokenCount(tokenizedStr); for (uint16_t i = 1; i <= size; ++i) { if (strcmp(embeddedCliGetToken(tokenizedStr, i), token) == 0) return i; } return 0; } uint16_t embeddedCliGetTokenCount(const char *tokenizedStr) { if (tokenizedStr == NULL || tokenizedStr[0] == '\0') return 0; int i = 0; uint16_t tokenCount = 1; while (true) { if (tokenizedStr[i] == '\0') { if (tokenizedStr[i + 1] == '\0') break; ++tokenCount; } ++i; } return tokenCount; } static void navigateHistory(EmbeddedCli *cli, bool navigateUp) { PREPARE_IMPL(cli); if (impl->history.itemsCount == 0 || (navigateUp && impl->history.current == impl->history.itemsCount) || (!navigateUp && impl->history.current == 0)) return; clearCurrentLine(cli); writeToOutput(cli, impl->invitation); if (navigateUp) ++impl->history.current; else --impl->history.current; const char *item = historyGet(&impl->history, impl->history.current); // simple way to handle empty command the same way as others if (item == NULL) item = ""; uint16_t len = (uint16_t) strlen(item); memcpy(impl->cmdBuffer, item, len); impl->cmdBuffer[len] = '\0'; impl->cmdSize = len; writeToOutput(cli, impl->cmdBuffer); impl->inputLineLength = impl->cmdSize; printLiveAutocompletion(cli); } static void onEscapedInput(EmbeddedCli *cli, char c) { PREPARE_IMPL(cli); if (c >= 64 && c <= 126) { // handle escape sequence UNSET_U8FLAG(impl->flags, CLI_FLAG_ESCAPE_MODE); if (c == 'A' || c == 'B') { // treat \e[..A as cursor up and \e[..B as cursor down // there might be extra chars between [ and A/B, just ignore them navigateHistory(cli, c == 'A'); } } } static void onCharInput(EmbeddedCli *cli, char c) { PREPARE_IMPL(cli); // have to reserve two extra chars for command ending (used in tokenization) if (impl->cmdSize + 2 >= impl->cmdMaxSize) return; impl->cmdBuffer[impl->cmdSize] = c; ++impl->cmdSize; impl->cmdBuffer[impl->cmdSize] = '\0'; cli->writeChar(cli, c); } static void onControlInput(EmbeddedCli *cli, char c) { PREPARE_IMPL(cli); // process \r\n and \n\r as single \r\n command if ((impl->lastChar == '\r' && c == '\n') || (impl->lastChar == '\n' && c == '\r')) return; if (c == '\r' || c == '\n') { // try to autocomplete command and then process it onAutocompleteRequest(cli); writeToOutput(cli, lineBreak); if (impl->cmdSize > 0) parseCommand(cli); impl->cmdSize = 0; impl->cmdBuffer[impl->cmdSize] = '\0'; impl->inputLineLength = 0; impl->history.current = 0; writeToOutput(cli, impl->invitation); } else if ((c == '\b' || c == 0x7F) && impl->cmdSize > 0) { // remove char from screen cli->writeChar(cli, '\b'); cli->writeChar(cli, ' '); cli->writeChar(cli, '\b'); // and from buffer --impl->cmdSize; impl->cmdBuffer[impl->cmdSize] = '\0'; } else if (c == '\t') { onAutocompleteRequest(cli); } } static void parseCommand(EmbeddedCli *cli) { PREPARE_IMPL(cli); bool isEmpty = true; for (int i = 0; i < impl->cmdSize; ++i) { if (impl->cmdBuffer[i] != ' ') { isEmpty = false; break; } } // do not process empty commands if (isEmpty) return; // push command to history before buffer is modified historyPut(&impl->history, impl->cmdBuffer); char *cmdName = NULL; char *cmdArgs = NULL; bool nameFinished = false; // find command name and command args inside command buffer for (int i = 0; i < impl->cmdSize; ++i) { char c = impl->cmdBuffer[i]; if (c == ' ') { // all spaces between name and args are filled with zeros // so name is a correct null-terminated string if (cmdArgs == NULL) impl->cmdBuffer[i] = '\0'; if (cmdName != NULL) nameFinished = true; } else if (cmdName == NULL) { cmdName = &impl->cmdBuffer[i]; } else if (cmdArgs == NULL && nameFinished) { cmdArgs = &impl->cmdBuffer[i]; } } // we keep two last bytes in cmd buffer reserved so cmdSize is always by 2 // less than cmdMaxSize impl->cmdBuffer[impl->cmdSize + 1] = '\0'; if (cmdName == NULL) return; // try to find command in bindings for (int i = 0; i < impl->bindingsCount; ++i) { if (strcmp(cmdName, impl->bindings[i].name) == 0) { if (impl->bindings[i].binding == NULL) break; if (impl->bindings[i].tokenizeArgs) embeddedCliTokenizeArgs(cmdArgs); // currently, output is blank line, so we can just print directly SET_FLAG(impl->flags, CLI_FLAG_DIRECT_PRINT); impl->bindings[i].binding(cli, cmdArgs, impl->bindings[i].context); UNSET_U8FLAG(impl->flags, CLI_FLAG_DIRECT_PRINT); return; } } // command not found in bindings or binding was null // try to call default callback if (cli->onCommand != NULL) { CliCommand command; command.name = cmdName; command.args = cmdArgs; // currently, output is blank line, so we can just print directly SET_FLAG(impl->flags, CLI_FLAG_DIRECT_PRINT); cli->onCommand(cli, &command); UNSET_U8FLAG(impl->flags, CLI_FLAG_DIRECT_PRINT); } else { onUnknownCommand(cli, cmdName); } } static void initInternalBindings(EmbeddedCli *cli) { CliCommandBinding b = { "help", "Print list of commands", true, NULL, onHelp }; embeddedCliAddBinding(cli, b); } static void onHelp(EmbeddedCli *cli, char *tokens, void *context) { UNUSED(context); PREPARE_IMPL(cli); if (impl->bindingsCount == 0) { writeToOutput(cli, "Help is not available"); writeToOutput(cli, lineBreak); return; } uint16_t tokenCount = embeddedCliGetTokenCount(tokens); if (tokenCount == 0) { for (int i = 0; i < impl->bindingsCount; ++i) { writeToOutput(cli, " * "); writeToOutput(cli, impl->bindings[i].name); writeToOutput(cli, lineBreak); if (impl->bindings[i].help != NULL) { cli->writeChar(cli, '\t'); writeToOutput(cli, impl->bindings[i].help); writeToOutput(cli, lineBreak); } } } else if (tokenCount == 1) { // try find command const char *helpStr = NULL; const char *cmdName = embeddedCliGetToken(tokens, 1); bool found = false; for (int i = 0; i < impl->bindingsCount; ++i) { if (strcmp(impl->bindings[i].name, cmdName) == 0) { helpStr = impl->bindings[i].help; found = true; break; } } if (found && helpStr != NULL) { writeToOutput(cli, " * "); writeToOutput(cli, cmdName); writeToOutput(cli, lineBreak); cli->writeChar(cli, '\t'); writeToOutput(cli, helpStr); writeToOutput(cli, lineBreak); } else if (found) { writeToOutput(cli, "Help is not available"); writeToOutput(cli, lineBreak); } else { onUnknownCommand(cli, cmdName); } } else { writeToOutput(cli, "Command \"help\" receives one or zero arguments"); writeToOutput(cli, lineBreak); } } static void onUnknownCommand(EmbeddedCli *cli, const char *name) { writeToOutput(cli, "Unknown command: \""); writeToOutput(cli, name); writeToOutput(cli, "\". Write \"help\" for a list of available commands"); writeToOutput(cli, lineBreak); } static AutocompletedCommand getAutocompletedCommand(EmbeddedCli *cli, const char *prefix) { AutocompletedCommand cmd = {NULL, 0, 0}; size_t prefixLen = strlen(prefix); PREPARE_IMPL(cli); if (impl->bindingsCount == 0 || prefixLen == 0) return cmd; for (int i = 0; i < impl->bindingsCount; ++i) { const char *name = impl->bindings[i].name; size_t len = strlen(name); // unset autocomplete flag UNSET_U8FLAG(impl->bindingsFlags[i], BINDING_FLAG_AUTOCOMPLETE); if (len < prefixLen) continue; // check if this command is candidate for autocomplete bool isCandidate = true; for (size_t j = 0; j < prefixLen; ++j) { if (prefix[j] != name[j]) { isCandidate = false; break; } } if (!isCandidate) continue; impl->bindingsFlags[i] |= BINDING_FLAG_AUTOCOMPLETE; if (cmd.candidateCount == 0 || len < cmd.autocompletedLen) cmd.autocompletedLen = (uint16_t) len; ++cmd.candidateCount; if (cmd.candidateCount == 1) { cmd.firstCandidate = name; continue; } for (size_t j = impl->cmdSize; j < cmd.autocompletedLen; ++j) { if (cmd.firstCandidate[j] != name[j]) { cmd.autocompletedLen = (uint16_t) j; break; } } } return cmd; } static void printLiveAutocompletion(EmbeddedCli *cli) { PREPARE_IMPL(cli); if (!IS_FLAG_SET(impl->flags, CLI_FLAG_AUTOCOMPLETE_ENABLED)) return; AutocompletedCommand cmd = getAutocompletedCommand(cli, impl->cmdBuffer); if (cmd.candidateCount == 0) { cmd.autocompletedLen = impl->cmdSize; } // print live autocompletion (or nothing, if it doesn't exist) for (size_t i = impl->cmdSize; i < cmd.autocompletedLen; ++i) { cli->writeChar(cli, cmd.firstCandidate[i]); } // replace with spaces previous autocompletion for (size_t i = cmd.autocompletedLen; i < impl->inputLineLength; ++i) { cli->writeChar(cli, ' '); } impl->inputLineLength = cmd.autocompletedLen; cli->writeChar(cli, '\r'); // print current command again so cursor is moved to initial place writeToOutput(cli, impl->invitation); writeToOutput(cli, impl->cmdBuffer); } static void onAutocompleteRequest(EmbeddedCli *cli) { PREPARE_IMPL(cli); AutocompletedCommand cmd = getAutocompletedCommand(cli, impl->cmdBuffer); if (cmd.candidateCount == 0) return; if (cmd.candidateCount == 1 || cmd.autocompletedLen > impl->cmdSize) { // can copy from index cmdSize, but prefix is the same, so copy everything memcpy(impl->cmdBuffer, cmd.firstCandidate, cmd.autocompletedLen); if (cmd.candidateCount == 1) { impl->cmdBuffer[cmd.autocompletedLen] = ' '; ++cmd.autocompletedLen; } impl->cmdBuffer[cmd.autocompletedLen] = '\0'; writeToOutput(cli, &impl->cmdBuffer[impl->cmdSize]); impl->cmdSize = cmd.autocompletedLen; impl->inputLineLength = impl->cmdSize; return; } // with multiple candidates when we already completed to common prefix // we show all candidates and print input again // we need to completely clear current line since it begins with invitation clearCurrentLine(cli); for (int i = 0; i < impl->bindingsCount; ++i) { // autocomplete flag is set for all candidates by last call to // getAutocompletedCommand if (!(impl->bindingsFlags[i] & BINDING_FLAG_AUTOCOMPLETE)) continue; const char *name = impl->bindings[i].name; writeToOutput(cli, name); writeToOutput(cli, lineBreak); } writeToOutput(cli, impl->invitation); writeToOutput(cli, impl->cmdBuffer); impl->inputLineLength = impl->cmdSize; } static void clearCurrentLine(EmbeddedCli *cli) { PREPARE_IMPL(cli); size_t len = impl->inputLineLength + strlen(impl->invitation); cli->writeChar(cli, '\r'); for (size_t i = 0; i < len; ++i) { cli->writeChar(cli, ' '); } cli->writeChar(cli, '\r'); impl->inputLineLength = 0; } static void writeToOutput(EmbeddedCli *cli, const char *str) { size_t len = strlen(str); for (size_t i = 0; i < len; ++i) { cli->writeChar(cli, str[i]); } } static bool isControlChar(char c) { return c == '\r' || c == '\n' || c == '\b' || c == '\t' || c == 0x7F; } static bool isDisplayableChar(char c) { return (c >= 32 && c <= 126); } static uint16_t fifoBufAvailable(FifoBuf *buffer) { if (buffer->back >= buffer->front) return (uint16_t) (buffer->back - buffer->front); else return (uint16_t) (buffer->size - buffer->front + buffer->back); } static char fifoBufPop(FifoBuf *buffer) { char a = '\0'; if (buffer->front != buffer->back) { a = buffer->buf[buffer->front]; buffer->front = (uint16_t) (buffer->front + 1) % buffer->size; } return a; } static bool fifoBufPush(FifoBuf *buffer, char a) { uint16_t newBack = (uint16_t) (buffer->back + 1) % buffer->size; if (newBack != buffer->front) { buffer->buf[buffer->back] = a; buffer->back = newBack; return true; } return false; } static bool historyPut(CliHistory *history, const char *str) { size_t len = strlen(str); // each item is ended with \0 so, need to have that much space at least if (history->bufferSize < len + 1) return false; // remove str from history (if it's present) so we don't get duplicates historyRemove(history, str); size_t usedSize; // remove old items if new one can't fit into buffer while (history->itemsCount > 0) { const char *item = historyGet(history, history->itemsCount); size_t itemLen = strlen(item); usedSize = ((size_t) (item - history->buf)) + itemLen + 1; size_t freeSpace = history->bufferSize - usedSize; if (freeSpace >= len + 1) break; // space not enough, remove last element --history->itemsCount; } if (history->itemsCount > 0) { // when history not empty, shift elements so new item is first memmove(&history->buf[len + 1], history->buf, usedSize); } memcpy(history->buf, str, len + 1); ++history->itemsCount; return true; } static const char *historyGet(CliHistory *history, uint16_t item) { if (item == 0 || item > history->itemsCount) return NULL; // items are stored in the same way (separated by \0 and counted from 1), // so can use this call return embeddedCliGetToken(history->buf, item); } static void historyRemove(CliHistory *history, const char *str) { if (str == NULL || history->itemsCount == 0) return; char *item = NULL; uint16_t itemPosition; for (itemPosition = 1; itemPosition <= history->itemsCount; ++itemPosition) { // items are stored in the same way (separated by \0 and counted from 1), // so can use this call item = embeddedCliGetTokenVariable(history->buf, itemPosition); if (strcmp(item, str) == 0) { break; } item = NULL; } if (item == NULL) return; --history->itemsCount; if (itemPosition == (history->itemsCount + 1)) { // if this is a last element, nothing is remaining to move return; } size_t len = strlen(item); size_t remaining = (size_t) (history->bufferSize - (item + len + 1 - history->buf)); // move everything to the right of found item memmove(item, &item[len + 1], remaining); } static uint16_t getTokenPosition(const char *tokenizedStr, uint16_t pos) { if (tokenizedStr == NULL || pos == 0) return CLI_TOKEN_NPOS; uint16_t i = 0; uint16_t tokenCount = 1; while (true) { if (tokenCount == pos) break; if (tokenizedStr[i] == '\0') { ++tokenCount; if (tokenizedStr[i + 1] == '\0') break; } ++i; } if (tokenizedStr[i] != '\0') return i; else return CLI_TOKEN_NPOS; } #ifdef __cplusplus } #endif #endif // EMBEDDED_CLI_IMPL_GUARD #endif // EMBEDDED_CLI_IMPL