/*
BSD 3-Clause License
Copyright (c) 2024, k4m1 <me@k4m1.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef __KLSH_INPUT__
#define __KLSH_INPUT__
#include <iostream>
#include <string>
/* Helper functions and definitions to keep track of state for
* a given input.
*
*/
class user_input_state {
public:
/* Identifier for all possible input states
*
* @member DEFAULT -- No modifiers are applied, all text is to be processed as is.
* @member SQUOTED -- We're within a single-quote block, this changes how variables and
* co. are handled
* @member DQUOTED -- We're within a double-quote block, this changes end of line etc.
* But doesn't change variable handling
* @member BACKQUOTE -- We're within a backtick (`) quote block, as above.
* @member COMMENTED -- Rest of the line is just a comment
* @member DONE -- We've encountered unescaped linefeed outside quote block
*/
enum state {
DEFAULT,
SQUOTED,
DQUOTED,
BACKQUOTED,
COMMENTED,
DONE
};
bool escaped = false;
state current_state = DEFAULT;
/* Reset back to defaults */
void reset() {
this->current_state = DEFAULT;
this->escaped = false;
}
/* is_default() checks if no modifiers have been applied.
*
* @return bool true if we're in default state.
*/
inline bool is_default() {
return ((this->current_state == DEFAULT) && (this->escaped == false));
}
/* is_squoted() checks if we're in not-yet-closed single
* quote block.
*
* @return bool true if we're in single-quote block
*/
inline bool is_squoted() {
return (this->current_state == SQUOTED);
}
/* is_dquoted() checks if we're in not-yet-closed double
* quote block.
*
* @return bool true if we're in double-quote block
*/
inline bool is_dquoted() {
return (this->current_state == DQUOTED);
}
/* is_back_quoted() checks if we're in not-yet-closed back
* quote block.
*
* @return bool true if we're in back-quote block
*/
inline bool is_back_quoted() {
return (this->current_state == BACKQUOTED);
}
/* is_commented() checks if we've encountered unescaped '#' character
* outside quote blocks. If we have, we're in commented-out state.
*
* @return bool true if current character is to be treated as commented out
*/
inline bool is_commented() {
return (this->current_state == COMMENTED);
}
/* is_done() checks if we've encountered unescaped line-feed character
* outside of quote blocks.
*
* @return bool true if we're done
*/
inline bool is_done() {
return (this->current_state == DONE);
}
/* is_escaped() checks if previous given key was escape character / backslash.
*
* @return bool true if we're escaped.
*/
inline bool is_escaped() {
return this->escaped;
}
/* Check if given key requires us to update our state, and then
* act accordingly.
*
* If escaped was set to true, only set escape to false and continue.
*
* @param char key -- Key to check
* @return bool true if state was changed
*/
bool update_from_keypress(char key);
/* Helper for toggling given input state
*
* @param enum state toggle -- State to toggle (if possible)
*/
inline void toggle_state(enum state toggle) {
// No need to check for escaped here. This function will only
// ever be called if this->is_escaped is false.
//
if (this->is_default()) {
this->current_state = toggle;
} else if (this->current_state == toggle) {
this->current_state = DEFAULT;
}
}
};
class user_input {
public:
/* These keys apply modifiers to how user input is to be parsed
*
* @member BACKSLASH -- Following character is to be escaped/treated as is.
* No changes to state are to be made, besides disabling
* 'escaped' modifier.
* @member SQUOTE -- Toggle single-quoted modifier if applicable.
* @member DQUOTE -- Toggle double-...
* @member BACKQUOTE -- Toggle back...
* @member HASH -- Rest of the line is to be treated as a comment
*/
enum input_modifier_keys {
BACKSLASH = '\\',
SQUOTE = '\'',
DQUOTE = '"',
BACKQUOTE = '`',
HASH = '#'
};
/* Command or control key definitions
*
* @member NUL -- Ignore for now
* @member BEL -- Play bell sound
* @member BS -- Backspace one column
* @member HT -- Go to next tab stop or end of line
* @member LF -- Line feed, new-line.
* @member VT -- Line feed
* @member FF -- Line feed
* @member CR -- Carriage return, move to 1st column of line
* @member SO -- Activate G1 character set
* @member SI -- Activate G0 character set
* @member CAN -- Abort an escape sequence
* @member SUB -- Abort an escape sequence
* @member ESC -- Start an escape sequence
* @member DEL -- Backspace or ignored
* @member CSI -- ESC [
*
*/
enum input_command_keys {
NUL = 0x00,
BEL = 0x07,
BS = 0x08,
HT = 0x09,
LF = 0x0A,
VT = 0x0B,
FF = 0x0C,
CR = 0x0D,
SO = 0x0E,
SI = 0x0F,
CAN = 0x18,
SUB = 0x1A,
ESC = 0x1B,
DEL = 0x7F
};
/* Get given user input in format that works together
* with exec()
*
* @param std::string raw -- Raw user input to parse
* @return Pointer to argv on success or NULL on error.
*/
char **get_argv();
/* Get length of argv we've previously parsed
*
* @return int argc
*/
inline int get_argc() {
return this->argc;
}
/* Read a new command from user.
*
* @return std::string user input
*/
std::string read();
private:
// Argc and argv
int argc = 0;
char **argv = nullptr;
// State we're in
user_input_state state;
// Input string to build
std::string input_string;
/* Helper to convert raw input string into a
* parsed, read-to-use argv array
*
*/
void parse_to_argv();
/* Helper to push given input character to an appropriate location
* in our input_string
*
* @param char c -- character to push
*/
void push_character(char c);
const int offset_end = -1;
// Offset in input string we're currently adding characters to
int offset = offset_end;
// Helper to handle backspace character encounters
void handle_backspace();
/* Helper to check if given character is beginning of a command
* sequence, such as arrow-key or alike
*
* @param char c -- Character to check
* @return bool true if this is command sequence
*/
static inline bool key_is_command(char c) {
return ((c <= ESC) || (c == DEL));
}
/* Handle clearing out previous character from screen by
* first moving cursor one step to the left, then
* printing empty space, and moving left again.
*
*/
static inline void clear_character_from_screen() {
std::cout << "\b \b" << std::flush;
}
/* Helper to deal with control characters. Check which character
* we were given, execute matching command, and push the control
* character to input string, if appropriate.
*
* @param char c -- Key user pressed
*/
void handle_control_character(char c);
/* Deal with user-given special/esc/cmd key.
*
*/
void handle_esc_cmd();
};
#endif // __KLSH_INPUT__