/* * Copyright (c) 2019 Stefan Strobel * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * @file shellmatta.c * @brief Main implementation of the Shellmatta terminal implementation * @author Stefan Strobel */ /** * @addtogroup shellmatta_private * @{ */ #include "shellmatta.h" #include "shellmatta_autocomplete.h" #include "shellmatta_history.h" #include "shellmatta_utils.h" #include "shellmatta_escape.h" #include "shellmatta_opt.h" #include #include #include #include /** * @} * @addtogroup shellmatta_api * @{ */ /** * @brief initialize the shellmatta terminal and provide all callbacks * @param[in,out] inst pointer to a shellmatta instance * @param[out] handle pointer to shellmatta handle - * has to be used for all furterh api calls * @param[in] buffer pointer to the input buffer to use * @param[in] bufferSize size of the provided input buffer * @param[in] historyBuffer pointer to the history buffer to use * NULL in case of no history buffer * @param[in] historyBufferSize size of the history buffer * 0 in case of no history buffer * @param[in] prompt pointer to prompt string - is printed * after each command * @param[in] cmdList constant command list if no dynamic * adding of commands is desired * @param[in] writeFct function pointer to output function */ shellmatta_retCode_t shellmatta_doInit( shellmatta_instance_t *inst, shellmatta_handle_t *handle, char *buffer, uint32_t bufferSize, char *historyBuffer, uint32_t historyBufferSize, const char *prompt, const shellmatta_cmd_t *cmdList, shellmatta_write_t writeFct) { /** -# check parameters for plausibility */ if( (NULL != inst) && (NULL != handle) && (NULL != buffer) && (0u != bufferSize) && (NULL != prompt) && (NULL != writeFct) && ((NULL != historyBuffer) || (0u == historyBufferSize))) { /** -# copy all provided buffers into the shellmatta instance */ inst->buffer = buffer; inst->bufferSize = bufferSize; inst->inputCount = 0u; inst->lastNewlineIdx = 0u; inst->cursor = 0u; inst->stdinIdx = 0u; inst->stdinLength = 0u; inst->historyBuffer = historyBuffer; inst->historyBufferSize = historyBufferSize; inst->historyStart = 0u; inst->historyEnd = 0u; inst->historyRead = 0u; inst->historyReadUp = true; inst->write = writeFct; inst->prompt = prompt; inst->echoEnabled = true; inst->dirty = false; inst->tabCounter = 0u; inst->escapeCounter = 0u; inst->hereStartIdx = 0u; inst->hereDelimiterIdx = 0u; inst->hereLength = 0u; inst->mode = SHELLMATTA_MODE_INSERT; inst->cmdList = &(inst->helpCmd); inst->continuousCmd = NULL; inst->cmdListIsConst = false; /** -# copy the help command structure to this instance */ memcpy(&(inst->helpCmd), &helpCmd, sizeof(shellmatta_cmd_t)); if(NULL != cmdList) { inst->helpCmd.next = (shellmatta_cmd_t *) cmdList; inst->cmdListIsConst = true; } inst->magic = SHELLMATTA_MAGIC; *handle = (shellmatta_handle_t)inst; /** -# print the first prompt */ utils_terminateInput(inst); } return SHELLMATTA_OK; } /** * @brief resets the whole shellmatta instance * @param[in] handle shellmatta instance handle * @param[in] printPrompt print a new command prompt * * This function can be used e.g. when working with connection based interfaces (e.g. sockets) to clear * the shell from old content when a new connection is opened. * It resets all internal states - the buffers are left as they are - they will be overwritten. * The history buffer is deleted as well. */ shellmatta_retCode_t shellmatta_resetShell( shellmatta_handle_t handle, bool printPrompt) { shellmatta_instance_t *inst = (shellmatta_instance_t *)handle; shellmatta_retCode_t ret = SHELLMATTA_OK; /** -# check if the instance is plausible */ if( (NULL != handle) && (SHELLMATTA_MAGIC == inst->magic)) { inst->inputCount = 0u; inst->continuousCmd = NULL; inst->lastNewlineIdx = 0u; inst->cursor = 0u; inst->stdinIdx = 0u; inst->stdinLength = 0u; inst->historyStart = 0u; inst->historyEnd = 0u; inst->historyRead = 0u; inst->historyReadUp = true; inst->dirty = false; inst->tabCounter = 0u; inst->escapeCounter = 0u; inst->hereStartIdx = 0u; inst->hereDelimiterIdx = 0u; inst->hereLength = 0u; if(true == printPrompt) { /** -# print a prompt if requested */ utils_terminateInput(inst); } } else { ret = SHELLMATTA_USE_FAULT; } return ret; } /** * @brief adds a command to the command list alphabetically ordered * @param[in] handle shellmatta instance handle * @param[in] cmd pointer to the command to add type #shellmatta_cmd_t * @return errorcode #SHELLMATTA_OK * #SHELLMATTA_USE_FAULT (param err) * SHELLMATTA_DUPLICATE * * The cmd name is mandatory, the rest of the command parameters (alias, helpText and usageText) are optional * and can be set to NULL if not used. */ shellmatta_retCode_t shellmatta_addCmd(shellmatta_handle_t handle, shellmatta_cmd_t *cmd) { shellmatta_instance_t *inst = (shellmatta_instance_t*)handle; shellmatta_cmd_t *tempCmd; shellmatta_cmd_t **prevCmd; bool cmdPlaced = false; shellmatta_retCode_t ret = SHELLMATTA_OK; int cmdDiff = 0; int aliasDiff = 0; /** -# check parameters for plausibility */ if( (NULL != inst) && (SHELLMATTA_MAGIC == inst->magic) && (false == inst->cmdListIsConst) && (NULL != cmd) && (NULL != cmd->cmd)) { tempCmd = inst->cmdList; prevCmd = &inst->cmdList; /** -# register first command as list entry */ if (NULL == tempCmd) { inst->cmdList = cmd; cmd->next = NULL; } /** -# append the new command sorted */ else { while ((false == cmdPlaced) && (SHELLMATTA_OK == ret)) { cmdDiff = strcmp(tempCmd->cmd, cmd->cmd); if( (NULL != cmd->cmdAlias) && (NULL != tempCmd->cmdAlias)) { aliasDiff = strcmp(tempCmd->cmdAlias, cmd->cmdAlias); } else { aliasDiff = 1; } /** -# check for a duplicate command */ if((0u == cmdDiff) || (0u == aliasDiff)) { ret = SHELLMATTA_DUPLICATE; } else if(0 < cmdDiff) { cmd->next = tempCmd; *prevCmd = cmd; cmdPlaced = true; } else if(NULL == tempCmd->next) { tempCmd->next = cmd; cmd->next = NULL; cmdPlaced = true; } else { /* nothing to do */ } prevCmd = &(tempCmd->next); tempCmd = tempCmd->next; } } } else { ret = SHELLMATTA_USE_FAULT; } return ret; } /** * @brief removes a command from the command list * @param[in] handle shellmatta instance handle * @param[in] cmd pointer to the command to remove type #shellmatta_cmd_t * @return errorcode #SHELLMATTA_OK * #SHELLMATTA_USE_FAULT (param err) */ shellmatta_retCode_t shellmatta_removeCmd(shellmatta_handle_t handle, shellmatta_cmd_t *cmd) { shellmatta_instance_t *inst = (shellmatta_instance_t*)handle; shellmatta_cmd_t *prevCmd; shellmatta_cmd_t *tempCmd; shellmatta_retCode_t ret = SHELLMATTA_OK; /** -# check parameters for plausibility */ if( (NULL != inst) && (SHELLMATTA_MAGIC == inst->magic) && (false == inst->cmdListIsConst) && (NULL != cmd) && (NULL != cmd->cmd)) { tempCmd = inst->cmdList; prevCmd = NULL; /** -# loop through command list */ while(NULL != tempCmd) { /** -# compare command strings to find the command to delete */ if (0 == strcmp( tempCmd->cmd, cmd->cmd) && (strlen(tempCmd->cmd) == strlen(cmd->cmd))) { /** -# first command removed */ if(NULL == prevCmd) { inst->cmdList = tempCmd->next; } /** -# last command removed */ else if(NULL == tempCmd->next) { prevCmd->next = NULL; } /** -# command removed from the middle of the list */ else { prevCmd->next = tempCmd->next; } break; } prevCmd = tempCmd; tempCmd = tempCmd->next; } } else { ret = SHELLMATTA_USE_FAULT; } return ret; } /** * @brief changes configuration of a shellmatta instance * @param[in] handle shellmatta instance handle * @param[in] mode insert mode of the shellmatta type #shellmatta_mode_t * @param[in] echoEnabled true: echo received chars to the output * @return errorcode #SHELLMATTA_OK * #SHELLMATTA_USE_FAULT (param err) */ shellmatta_retCode_t shellmatta_configure(shellmatta_handle_t handle, shellmatta_mode_t mode, bool echoEnabled) { shellmatta_instance_t *inst = (shellmatta_instance_t*)handle; shellmatta_retCode_t ret = SHELLMATTA_OK; /** -# check parameters for plausibility */ if( (NULL != inst) && (SHELLMATTA_MAGIC == inst->magic) && ((mode == SHELLMATTA_MODE_INSERT) || (mode == SHELLMATTA_MODE_OVERWRITE))) { inst->mode = mode; inst->echoEnabled = echoEnabled; } else { ret = SHELLMATTA_USE_FAULT; } return ret; } /** * @brief processes the passed amount of data * @param[in] handle shellmatta instance handle * @param[in] data pointer to input data to process * @param[in] size length of input data to process * @return errorcode #SHELLMATTA_OK * #SHELLMATTA_USE_FAULT */ shellmatta_retCode_t shellmatta_processData(shellmatta_handle_t handle, char *data, uint32_t size) { shellmatta_cmd_t *cmd; uint8_t cmdExecuted = 0u; uint32_t cmdLen; char *tempString; uint32_t byteCounter; shellmatta_retCode_t ret = SHELLMATTA_OK; shellmatta_retCode_t cmdRet; shellmatta_instance_t *inst = (shellmatta_instance_t*)handle; /** -# check parameters for plausibility */ if( (NULL != inst) && (SHELLMATTA_MAGIC == inst->magic)) { /** -# process byte wise */ for (byteCounter = 0u; byteCounter < size; byteCounter++) { /** -# in continuous mode - pass data directly to the command */ if(NULL != inst->continuousCmd) { /** -# copy data and call command function */ inst->buffer[inst->stdinIdx] = data[byteCounter]; inst->stdinLength = 1u; cmdRet = inst->continuousCmd->cmdFct(inst, inst->buffer, inst->inputCount); /** -# check if continuous mode is canceled */ if(('\x03' == data[byteCounter]) || (SHELLMATTA_CONTINUE != cmdRet)) { utils_terminateInput(inst); } } /** -# handle escape sequences */ else if(inst->escapeCounter != 0u) { escape_handleSequence(inst, *data); } /** -# ignore newline as first character (to be compatible to * terminals sending newline after return */ else if((0u == inst->inputCount) && ('\n' == *data)) { /* do nothing */ } /** -# handle return as start of processing the command */ else if ('\r' == *data) { if(0u == inst->hereLength) { /** * \dot * digraph heredocParser { * start -> wait [ label="<< in first line - store delimiter" ]; * wait -> wait [ label="delimiter not detected" ]; * wait -> end [ label="delimiter found - remove all heredoc stuff and send the input to the command" ]; * } * \enddot */ /** -# check for heredoc - add string delimiter to stop strstr from searching too far */ inst->buffer[inst->inputCount] = '\0'; tempString = strstr(inst->buffer, "<<"); if(NULL != tempString) { /*' -# check if length of heredoc delimiter is valid */ if(inst->inputCount > ((uint32_t)(tempString - inst->buffer) + 2u)) { inst->hereStartIdx = (uint32_t)(tempString - inst->buffer); inst->hereDelimiterIdx = inst->hereStartIdx + 2u; while((inst->hereDelimiterIdx < inst->inputCount) && ( ('\0' == inst->buffer[inst->hereDelimiterIdx]) || (' ' == inst->buffer[inst->hereDelimiterIdx]))) { inst->hereDelimiterIdx ++; } inst->hereLength = inst->inputCount - inst->hereDelimiterIdx; inst->dirty = true; utils_insertChars(inst, data, 1); inst->lastNewlineIdx = inst->inputCount; } else { inst->hereLength = 0u; /** -# store the current command and reset the history buffer */ inst->dirty = true; history_storeCmd(inst); history_reset(inst); } } else { /** -# store the current command and reset the history buffer */ inst->dirty = true; history_storeCmd(inst); history_reset(inst); } } else { tempString = &inst->buffer[inst->lastNewlineIdx]; cmdLen = inst->inputCount - inst->lastNewlineIdx; /** -# skip newline characters before comparison */ while(('\n' == *tempString) || ('\r' == *tempString)) { tempString ++; cmdLen --; } if( (inst->hereLength == cmdLen) && (0 == strncmp( &inst->buffer[inst->hereDelimiterIdx], tempString, inst->hereLength))) { /** -# store the current command and reset the history buffer */ inst->dirty = true; history_storeCmd(inst); history_reset(inst); /** -# process heredoc as stdin like input */ /** -# find start of heredoc data */ inst->stdinIdx = inst->hereDelimiterIdx + inst->hereLength; while( ('\n' == inst->buffer[inst->stdinIdx]) || ('\r' == inst->buffer[inst->stdinIdx])) { inst->stdinIdx ++; } /** -# calculate length and terminate stdin string */ inst->stdinLength = inst->lastNewlineIdx - inst->stdinIdx; inst->buffer[inst->lastNewlineIdx] = '\0'; /** -# calculate length and terminate argument string */ inst->inputCount = inst->hereStartIdx; inst->buffer[inst->hereStartIdx] = '\0'; /** -# terminate heredoc session */ inst->hereLength = 0u; } else { /** -# the party goes on - print the \r and add a \n to satisfy most terminals */ inst->lastNewlineIdx = inst->inputCount; utils_insertChars(inst, data, 1u); } } if(0u == inst->hereLength) { cmd = inst->cmdList; /** -# determine the cmd len (chars until first space or \0 is found */ cmdLen = 0u; while( (cmdLen < inst->inputCount) && (' ' != inst->buffer[cmdLen]) && ('\r' != inst->buffer[cmdLen]) && ('\n' != inst->buffer[cmdLen]) && ('\0' != inst->buffer[cmdLen])) { cmdLen ++; } /** -# search for a matching command */ while (NULL != cmd) { /** -# compare command and alias string and length */ if ( ((0 == strncmp( inst->buffer, cmd->cmd, cmdLen)) && (cmdLen == strlen(cmd->cmd))) || ((NULL != cmd->cmdAlias) && ((0 == strncmp( inst->buffer, cmd->cmdAlias, cmdLen)) && (cmdLen == strlen(cmd->cmdAlias))))) { utils_writeEcho(inst, "\r\n", 2u); shellmatta_opt_init(inst, cmdLen + 1u); cmdExecuted = 1u; cmdRet = cmd->cmdFct(inst, inst->buffer, inst->inputCount); if(SHELLMATTA_CONTINUE == cmdRet) { inst->continuousCmd = cmd; /** -# initialize stdin buffer */ inst->stdinIdx = inst->inputCount + 1u; inst->stdinLength = 0u; } cmd = NULL; } else { cmd = cmd->next; } } if ((cmdExecuted == 0u) && (inst->inputCount > 0)) { inst->write("\r\nCommand: ", 11u); inst->write(inst->buffer, inst->inputCount); inst->write(" not found", 10u); } /** -# terminate this session if no continuous mode is requested */ if(NULL == inst->continuousCmd) { utils_terminateInput(inst); } } } /** -# check for tabulator key - auto complete */ else if('\t' == *data) { inst->dirty = true; autocomplete_run(inst); } /** -# check for cancel - * terminate current input and print prompt again */ else if('\x03' == *data) { inst->dirty = false; history_reset(inst); utils_terminateInput(inst); } /** -# check for backspace */ else if( ('\b' == *data) || ('\x7f' == *data)) { inst->dirty = true; utils_removeChars(inst, 1u, true); } /** -# check for start of escape sequence */ else if('\x1b' == *data) { inst->escapeCounter = 1u; } else { inst->dirty = true; utils_insertChars(inst, data, 1); } /** -# reset tab counter on not a tab */ if ('\t' != *data) { inst->tabCounter = 0u; } data ++; } } else { ret = SHELLMATTA_USE_FAULT; } return ret; } /** * @brief simple write function to write a datastream to the output of an instance * @param[in] handle shellmatta instance handle * @param[in] data data to be written * @param[in] length amount of data to be written * @return */ shellmatta_retCode_t shellmatta_write( shellmatta_handle_t handle, char *data, uint32_t length) { shellmatta_retCode_t ret = SHELLMATTA_USE_FAULT; shellmatta_instance_t *inst = (shellmatta_instance_t*)handle; /** -# check parameters for plausibility */ if( (NULL != inst) && (SHELLMATTA_MAGIC == inst->magic)) { /** -# pass the data to the write function of the instance */ ret = inst->write(data, length); } return ret; } /** * @brief reads the stdin like buffer * @param[in] handle shellmatta instance handle * @param[out] data stdin data or NULL * @param[out] length size of the stdin data * @return */ shellmatta_retCode_t shellmatta_read( shellmatta_handle_t handle, char **data, uint32_t *length) { shellmatta_retCode_t ret = SHELLMATTA_USE_FAULT; shellmatta_instance_t *inst = (shellmatta_instance_t*)handle; /** -# check parameters for plausibility */ if( (NULL != inst) && (SHELLMATTA_MAGIC == inst->magic) && (NULL != data) && (NULL != length)) { /** -# return a pointer to the data or NULL */ if(0u == inst->stdinLength) { *data = NULL; } else { *data = &(inst->buffer[inst->stdinIdx]); } *length = inst->stdinLength; } return ret; } #ifndef SHELLMATTA_STRIP_PRINTF /** * @brief printf like function to print output to the instances output * @param[in] handle shellmatta instance handle * @param[in] fmt format string and parameters * @return errorcode #SHELLMATTA_OK * #SHELLMATTA_USE_FAULT (no valid instance) * #SHELLMATTA_ERROR (buffer overflow or format err) */ shellmatta_retCode_t shellmatta_printf( shellmatta_handle_t handle, const char *fmt, ...) { char outputBuffer[SHELLMATTA_OUTPUT_BUFFER_SIZE]; va_list arg; int length; shellmatta_retCode_t ret = SHELLMATTA_OK; shellmatta_instance_t *inst = (shellmatta_instance_t*)handle; /** -# check parameters for plausibility */ if( (NULL != inst) && (SHELLMATTA_MAGIC == inst->magic)) { /** -# assemble output string and write it */ va_start(arg, fmt); length = vsnprintf(outputBuffer, SHELLMATTA_OUTPUT_BUFFER_SIZE, fmt, arg); va_end(arg); if(length < 0) { ret = SHELLMATTA_ERROR; } else { inst->write(outputBuffer, length); } } else { ret = SHELLMATTA_USE_FAULT; } return ret; } #endif /** @} */