diff options
author | Richard Braun <rbraun@sceen.net> | 2015-03-12 23:26:15 +0100 |
---|---|---|
committer | Richard Braun <rbraun@sceen.net> | 2015-03-12 23:26:15 +0100 |
commit | 9a96eab8383e94da192dc3a2aa77b9bed1fb39aa (patch) | |
tree | 51186ca05bb7f7ada88ce0b2c4a3a271164cd918 | |
parent | 569c0aa5807c6627761b6cf8d3f726a4b6e9535d (diff) |
shell: new module
-rw-r--r-- | Makefile.am | 10 | ||||
-rw-r--r-- | shell.c | 1150 | ||||
-rw-r--r-- | shell.h | 87 | ||||
-rw-r--r-- | test/test_shell.c | 126 |
4 files changed, 1371 insertions, 2 deletions
diff --git a/Makefile.am b/Makefile.am index b5d255d..39bc14b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -33,14 +33,17 @@ librbraun_la_SOURCES = \ rbtree_i.h \ rdxtree.c \ rdxtree.h \ - rdxtree_i.h + rdxtree_i.h \ + shell.c + shell.h librbraun_la_LIBADD = -lrt -lpthread bin_PROGRAMS = \ test_avltree \ test_rbtree \ - test_rdxtree + test_rdxtree \ + test_shell test_avltree_SOURCES = test/test_avltree.c test_avltree_LDADD = librbraun.la @@ -50,3 +53,6 @@ test_rbtree_LDADD = librbraun.la test_rdxtree_SOURCES = test/test_rdxtree.c test_rdxtree_LDADD = -lrt -lpthread + +test_shell_SOURCES = test/test_shell.c +test_shell_LDADD = librbraun.la @@ -0,0 +1,1150 @@ +/* + * Copyright (c) 2015 Richard Braun. + * 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. + * + * + * Upstream site with license notes : + * http://git.sceen.net/rbraun/librbraun.git/ + */ + +#include <pthread.h> +#include <stdio.h> +#include <string.h> + +#include "macros.h" +#include "error.h" +#include "hash.h" +#include "shell.h" + +/* + * Binary exponent and size of the hash table used to store commands. + */ +#define SHELL_HTABLE_BITS 6 +#define SHELL_HTABLE_SIZE (1 << SHELL_HTABLE_BITS) + +struct shell_bucket { + struct shell_cmd *cmd; +}; + +/* + * Hash table for quick command lookup. + */ +static struct shell_bucket shell_htable[SHELL_HTABLE_SIZE]; + +#define SHELL_COMPLETION_MATCH_FMT "-16s" +#define SHELL_COMPLETION_NR_MATCHES_PER_LINE 4 + +/* + * Sorted command list. + */ +static struct shell_cmd *shell_list; + +/* + * Lock protecting access to the hash table and list of commands. + * + * Note that this lock only protects access to the commands containers, + * not the commands themselves. In particular, it is not necessary to + * hold this lock when a command is used, i.e. when accessing a command + * name, function pointer, or description. + */ +static pthread_mutex_t shell_lock; + +/* + * Escape sequence states. + * + * Here is an incomplete description of escape sequences : + * http://en.wikipedia.org/wiki/ANSI_escape_code + * + * These values must be different from 0. + */ +#define SHELL_ESC_STATE_START 1 +#define SHELL_ESC_STATE_CSI 2 + +/* + * This value changes depending on the standard used and was chosen arbitrarily. + */ +#define SHELL_ESC_SEQ_MAX_SIZE 8 + +typedef void (*shell_esc_seq_fn)(void); + +struct shell_esc_seq { + const char *str; + shell_esc_seq_fn fn; +}; + +#define SHELL_LINE_MAX_SIZE 64 + +/* + * Line containing a shell entry. + * + * The string must be nul-terminated. The size doesn't include this + * additional nul character, the same way strlen() doesn't account for it. + */ +struct shell_line { + char str[SHELL_LINE_MAX_SIZE]; + unsigned long size; +}; + +/* + * Number of entries in the history. + * + * One of these entryes is used as the current line. + */ +#define SHELL_HISTORY_SIZE 21 + +#if SHELL_HISTORY_SIZE == 0 +#error shell history size must be non-zero +#endif /* SHELL_HISTORY_SIZE == 0 */ + +/* + * Shell history. + * + * The history is never empty. There is always at least one entry, the + * current line, referenced by the newest (most recent) index. The array + * is used like a circular buffer, i.e. old entries are implicitely + * erased by new ones. The index references the entry used as a template + * for the current line. + */ +static struct shell_line shell_history[SHELL_HISTORY_SIZE]; +static unsigned long shell_history_newest; +static unsigned long shell_history_oldest; +static unsigned long shell_history_index; + +/* + * Cursor index within the current line. + */ +static unsigned long shell_cursor_index; + +#define SHELL_SEPARATOR ' ' + +#if 0 +#define SHELL_ERASE '\b' +#define SHELL_ERASE '\x7f' +#endif + +#define SHELL_ERASE '\x7f' + +/* + * Buffer used to store the current line during argument processing. + * + * The pointers in the argv array point inside this buffer. The + * separators immediately following the arguments are replaced with + * nul characters. + */ +static char shell_tmp_line[SHELL_LINE_MAX_SIZE]; + +#define SHELL_MAX_ARGS 16 + +static int shell_argc; +static char *shell_argv[SHELL_MAX_ARGS]; + +void +shell_cmd_init(struct shell_cmd *cmd, const char *name, + shell_fn_t fn, const char *usage, + const char *short_desc, const char *long_desc) +{ + cmd->ht_next = NULL; + cmd->ls_next = NULL; + cmd->name = name; + cmd->fn = fn; + cmd->usage = usage; + cmd->short_desc = short_desc; + cmd->long_desc = long_desc; +} + +static const char * +shell_cmd_name(const struct shell_cmd *cmd) +{ + return cmd->name; +} + +static inline struct shell_bucket * +shell_bucket_get(const char *name) +{ + return &shell_htable[hash_str(name, SHELL_HTABLE_BITS)]; +} + +static void +shell_cmd_acquire(void) +{ + pthread_mutex_lock(&shell_lock); +} + +static void +shell_cmd_release(void) +{ + pthread_mutex_unlock(&shell_lock); +} + +static const struct shell_cmd * +shell_cmd_lookup(const char *name) +{ + const struct shell_bucket *bucket; + const struct shell_cmd *cmd; + + shell_cmd_acquire(); + + bucket = shell_bucket_get(name); + + for (cmd = bucket->cmd; cmd != NULL; cmd = cmd->ht_next) { + if (strcmp(cmd->name, name) == 0) { + break; + } + } + + shell_cmd_release(); + + return cmd; +} + +/* + * The global lock must be acquired before calling this function. + */ +static const struct shell_cmd * +shell_cmd_match(const struct shell_cmd *cmd, const char *str, + unsigned long size) +{ + while (cmd != NULL) { + if (strncmp(cmd->name, str, size) == 0) { + return cmd; + } + + cmd = cmd->ls_next; + } + + return NULL; +} + +/* + * The global lock must be acquired before calling this function. + */ +static int +shell_cmd_complete(const char *str, unsigned long *sizep, + const struct shell_cmd **cmdp) +{ + const struct shell_cmd *cmd, *next; + unsigned long size; + + size = *sizep; + + /* + * Start with looking up a command that matches the given argument. + * If there is no match, return an error. + */ + cmd = shell_cmd_match(shell_list, str, size); + + if (cmd == NULL) { + return ERR_INVAL; + } + + *cmdp = cmd; + + /* + * If at least one command matches, try to complete it. + * There can be two cases : + * 1/ There is one and only one match, which is directly returned. + * 2/ There are several matches, in which case the common length is + * computed. + */ + next = cmd->ls_next; + + if ((next == NULL) + || (strncmp(cmd->name, next->name, size) != 0)) { + *sizep = strlen(cmd->name); + return 0; + } + + /* + * When computing the common length, all the commands that can match + * must be evaluated. Considering the current command is the first + * that can match, the only other variable missing is the last + * command that can match. + */ + while (next->ls_next != NULL) { + if (strncmp(cmd->name, next->ls_next->name, size) != 0) { + break; + } + + next = next->ls_next; + } + + if (size != 0) { + while ((cmd->name[size - 1] != '\0') + && (cmd->name[size - 1] == next->name[size - 1])) { + size++; + } + + size--; + } + + *sizep = size; + return ERR_AGAIN; +} + +/* + * The global lock must be acquired before calling this function. + */ +static void +shell_cmd_print_matches(const struct shell_cmd *cmd, unsigned long size) +{ + const struct shell_cmd *tmp; + unsigned int i; + + printf("\n"); + + for (tmp = cmd, i = 1; tmp != NULL; tmp = tmp->ls_next, i++) { + if (strncmp(cmd->name, tmp->name, size) != 0) { + break; + } + + printf("%" SHELL_COMPLETION_MATCH_FMT, tmp->name); + + if ((i % SHELL_COMPLETION_NR_MATCHES_PER_LINE) == 0) { + printf("\n"); + } + } + + if ((i % SHELL_COMPLETION_NR_MATCHES_PER_LINE) != 1) { + printf("\n"); + } +} + +static int +shell_cmd_check_char(char c) +{ + if (((c >= 'a') && (c <= 'z')) + || ((c >= 'A') && (c <= 'Z')) + || ((c >= '0') && (c <= '9')) + || (c == '-') + || (c == '_')) { + return 0; + } + + return ERR_INVAL; +} + +static int +shell_cmd_check(const struct shell_cmd *cmd) +{ + unsigned long i; + int error; + + for (i = 0; cmd->name[i] != '\0'; i++) { + error = shell_cmd_check_char(cmd->name[i]); + + if (error) { + return error; + } + } + + if (i == 0) { + return ERR_INVAL; + } + + return 0; +} + +/* + * The global lock must be acquired before calling this function. + */ +static void +shell_cmd_add_list(struct shell_cmd *cmd) +{ + struct shell_cmd *prev, *next; + + prev = shell_list; + + if ((prev == NULL) + || (strcmp(cmd->name, prev->name) < 0)) { + shell_list = cmd; + cmd->ls_next = prev; + return; + } + + for (;;) { + next = prev->ls_next; + + if ((next == NULL) + || (strcmp(cmd->name, next->name) < 0)) { + break; + } + + prev = next; + } + + prev->ls_next = cmd; + cmd->ls_next = next; +} + +/* + * The global lock must be acquired before calling this function. + */ +static int +shell_cmd_add(struct shell_cmd *cmd) +{ + struct shell_bucket *bucket; + struct shell_cmd *tmp; + + bucket = shell_bucket_get(cmd->name); + tmp = bucket->cmd; + + if (tmp == NULL) { + bucket->cmd = cmd; + goto out; + } + + for (;;) { + if (strcmp(cmd->name, tmp->name) == 0) { + fprintf(stderr, "shell: %s: shell command name collision", + cmd->name); + return ERR_EXIST; + } + + if (tmp->ht_next == NULL) { + break; + } + + tmp = tmp->ht_next; + } + + tmp->ht_next = cmd; + +out: + shell_cmd_add_list(cmd); + return 0; +} + +int +shell_cmd_register(struct shell_cmd *cmd) +{ + int error; + + error = shell_cmd_check(cmd); + + if (error) { + return error; + } + + shell_cmd_acquire(); + error = shell_cmd_add(cmd); + shell_cmd_release(); + + return error; +} + +static inline const char * +shell_line_str(const struct shell_line *line) +{ + return line->str; +} + +static inline unsigned long +shell_line_size(const struct shell_line *line) +{ + return line->size; +} + +static inline void +shell_line_reset(struct shell_line *line) +{ + line->str[0] = '\0'; + line->size = 0; +} + +static inline void +shell_line_copy(struct shell_line *dest, const struct shell_line *src) +{ + strcpy(dest->str, src->str); + dest->size = src->size; +} + +static inline int +shell_line_cmp(const struct shell_line *a, const struct shell_line *b) +{ + return strcmp(a->str, b->str); +} + +static int +shell_line_insert(struct shell_line *line, unsigned long index, char c) +{ + unsigned long remaining_chars; + + if (index > line->size) { + return ERR_INVAL; + } + + if ((line->size + 1) == sizeof(line->str)) { + return ERR_NOMEM; + } + + remaining_chars = line->size - index; + + if (remaining_chars != 0) { + memmove(&line->str[index + 1], &line->str[index], remaining_chars); + } + + line->str[index] = c; + line->size++; + line->str[line->size] = '\0'; + return 0; +} + +static int +shell_line_erase(struct shell_line *line, unsigned long index) +{ + unsigned long remaining_chars; + + if (index >= line->size) { + return ERR_INVAL; + } + + remaining_chars = line->size - index - 1; + + if (remaining_chars != 0) { + memmove(&line->str[index], &line->str[index + 1], remaining_chars); + } + + line->size--; + line->str[line->size] = '\0'; + return 0; +} + +static struct shell_line * +shell_history_get(unsigned long index) +{ + return &shell_history[index % ARRAY_SIZE(shell_history)]; +} + +static struct shell_line * +shell_history_get_newest(void) +{ + return shell_history_get(shell_history_newest); +} + +static struct shell_line * +shell_history_get_index(void) +{ + return shell_history_get(shell_history_index); +} + +static void +shell_history_reset_index(void) +{ + shell_history_index = shell_history_newest; +} + +static inline int +shell_history_same_newest(void) +{ + return (shell_history_newest != shell_history_oldest) + && shell_line_cmp(shell_history_get_newest(), + shell_history_get(shell_history_newest - 1)) == 0; +} + +static void +shell_history_push(void) +{ + if ((shell_line_size(shell_history_get_newest()) == 0) + || shell_history_same_newest()) { + shell_history_reset_index(); + return; + } + + shell_history_newest++; + shell_history_reset_index(); + + /* Mind integer overflows */ + if ((shell_history_newest - shell_history_oldest) + >= ARRAY_SIZE(shell_history)) { + shell_history_oldest = shell_history_newest + - ARRAY_SIZE(shell_history) + 1; + } +} + +static void +shell_history_back(void) +{ + if (shell_history_index == shell_history_oldest) { + return; + } + + shell_history_index--; + shell_line_copy(shell_history_get_newest(), shell_history_get_index()); +} + +static void +shell_history_forward(void) +{ + if (shell_history_index == shell_history_newest) { + return; + } + + shell_history_index++; + + if (shell_history_index == shell_history_newest) { + shell_line_reset(shell_history_get_newest()); + } else { + shell_line_copy(shell_history_get_newest(), shell_history_get_index()); + } +} + +static void +shell_cmd_help(int argc, char *argv[]) +{ + const struct shell_cmd *cmd; + + if (argc > 2) { + argc = 2; + argv[1] = "help"; + } + + if (argc == 2) { + cmd = shell_cmd_lookup(argv[1]); + + if (cmd == NULL) { + printf("shell: help: %s: command not found\n", argv[1]); + return; + } + + printf("usage: %s\n%s\n", cmd->usage, cmd->short_desc); + + if (cmd->long_desc != NULL) { + printf("\n%s\n", cmd->long_desc); + } + + return; + } + + shell_cmd_acquire(); + + for (cmd = shell_list; cmd != NULL; cmd = cmd->ls_next) { + printf("%13s %s\n", cmd->name, cmd->short_desc); + } + + shell_cmd_release(); +} + +static void +shell_cmd_history(int argc, char *argv[]) +{ + unsigned long i; + + (void)argc; + (void)argv; + + /* Mind integer overflows */ + for (i = shell_history_oldest; i != shell_history_newest; i++) { + printf("%6lu %s\n", i - shell_history_oldest, + shell_line_str(shell_history_get(i))); + } +} + +static struct shell_cmd shell_default_cmds[] = { + SHELL_CMD_INITIALIZER("help", shell_cmd_help, + "help [command]", + "obtain help about shell commands"), + SHELL_CMD_INITIALIZER("history", shell_cmd_history, + "history", + "display history list"), +}; + +static void +shell_prompt(void) +{ + printf("shell> "); +} + +static void +shell_reset(void) +{ + shell_line_reset(shell_history_get_newest()); + shell_cursor_index = 0; + shell_prompt(); +} + +static void +shell_erase(void) +{ + struct shell_line *current_line; + unsigned long remaining_chars; + + current_line = shell_history_get_newest(); + remaining_chars = shell_line_size(current_line); + + while (shell_cursor_index != remaining_chars) { + putchar(' '); + shell_cursor_index++; + } + + while (remaining_chars != 0) { + printf("\b \b"); + remaining_chars--; + } + + shell_cursor_index = 0; +} + +static void +shell_restore(void) +{ + struct shell_line *current_line; + + current_line = shell_history_get_newest(); + printf("%s", shell_line_str(current_line)); + shell_cursor_index = shell_line_size(current_line); +} + +static int +shell_is_ctrl_char(char c) +{ + return ((c < ' ') || (c >= 0x7f)); +} + +static void +shell_process_left(void) +{ + if (shell_cursor_index == 0) { + return; + } + + shell_cursor_index--; + printf("\e[1D"); +} + +static void +shell_process_right(void) +{ + if (shell_cursor_index >= shell_line_size(shell_history_get_newest())) { + return; + } + + shell_cursor_index++; + printf("\e[1C"); +} + +static void +shell_process_up(void) +{ + shell_erase(); + shell_history_back(); + shell_restore(); +} + +static void +shell_process_down(void) +{ + shell_erase(); + shell_history_forward(); + shell_restore(); +} + +static void +shell_process_backspace(void) +{ + struct shell_line *current_line; + unsigned long remaining_chars; + int error; + + current_line = shell_history_get_newest(); + error = shell_line_erase(current_line, shell_cursor_index - 1); + + if (error) { + return; + } + + shell_cursor_index--; + printf("\b%s ", shell_line_str(current_line) + shell_cursor_index); + remaining_chars = shell_line_size(current_line) - shell_cursor_index + 1; + + while (remaining_chars != 0) { + putchar('\b'); + remaining_chars--; + } +} + +static int +shell_process_raw_char(char c) +{ + struct shell_line *current_line; + unsigned long remaining_chars; + int error; + + current_line = shell_history_get_newest(); + error = shell_line_insert(current_line, shell_cursor_index, c); + + if (error) { + printf("\nshell: line too long\n"); + return error; + } + + shell_cursor_index++; + + if (shell_cursor_index == shell_line_size(current_line)) { + putchar(c); + goto out; + } + + printf("%s", shell_line_str(current_line) + shell_cursor_index - 1); + remaining_chars = shell_line_size(current_line) - shell_cursor_index; + + while (remaining_chars != 0) { + putchar('\b'); + remaining_chars--; + } + +out: + return 0; +} + +static int +shell_process_tabulation(void) +{ + const struct shell_cmd *cmd; + struct shell_line *current_line; + const char *name; + unsigned long i, size; + int error; + + shell_cmd_acquire(); + + current_line = shell_history_get_newest(); + size = shell_cursor_index; + error = shell_cmd_complete(shell_line_str(current_line), &size, &cmd); + + if (error && (error != ERR_AGAIN)) { + error = 0; + goto out; + } + + if (error == ERR_AGAIN) { + unsigned long cursor_index; + + cursor_index = shell_cursor_index; + shell_cmd_print_matches(cmd, size); + shell_prompt(); + shell_restore(); + + while (shell_cursor_index != cursor_index) { + shell_process_left(); + } + } + + name = shell_cmd_name(cmd); + + while (shell_cursor_index != 0) { + shell_process_backspace(); + } + + for (i = 0; i < size; i++) { + error = shell_process_raw_char(name[i]); + + if (error) + goto out; + } + + error = 0; + +out: + shell_cmd_release(); + return error; +} + +static void +shell_esc_seq_up(void) +{ + shell_process_up(); +} + +static void +shell_esc_seq_down(void) +{ + shell_process_down(); +} + +static void +shell_esc_seq_next(void) +{ + shell_process_right(); +} + +static void +shell_esc_seq_prev(void) +{ + shell_process_left(); +} + +static void +shell_esc_seq_home(void) +{ + while (shell_cursor_index != 0) { + shell_process_left(); + } +} + +static void +shell_esc_seq_del(void) +{ + shell_process_right(); + shell_process_backspace(); +} + +static void +shell_esc_seq_end(void) +{ + unsigned long size; + + size = shell_line_size(shell_history_get_newest()); + + while (shell_cursor_index < size) { + shell_process_right(); + } +} + +static const struct shell_esc_seq shell_esc_seqs[] = { + { "A", shell_esc_seq_up }, + { "B", shell_esc_seq_down }, + { "C", shell_esc_seq_next }, + { "D", shell_esc_seq_prev }, + { "H", shell_esc_seq_home }, + { "1~", shell_esc_seq_home }, + { "3~", shell_esc_seq_del }, + { "F", shell_esc_seq_end }, + { "4~", shell_esc_seq_end }, +}; + +static const struct shell_esc_seq * +shell_esc_seq_lookup(const char *str) +{ + unsigned long i; + + for (i = 0; i < ARRAY_SIZE(shell_esc_seqs); i++) { + if (strcmp(shell_esc_seqs[i].str, str) == 0) { + return &shell_esc_seqs[i]; + } + } + + return NULL; +} + +/* + * Process a single escape sequence character. + * + * Return the next escape state or 0 if the sequence is complete. + */ +static int +shell_process_esc_sequence(char c) +{ + static char str[SHELL_ESC_SEQ_MAX_SIZE], *ptr = str; + + const struct shell_esc_seq *seq; + uintptr_t index; + + index = ptr - str; + + if (index >= (ARRAY_SIZE(str) - 1)) { + printf("shell: escape sequence too long\n"); + goto reset; + } + + *ptr = c; + ptr++; + *ptr = '\0'; + + if ((c >= '@') && (c <= '~')) { + seq = shell_esc_seq_lookup(str); + + if (seq != NULL) { + seq->fn(); + } + + goto reset; + } + + return SHELL_ESC_STATE_CSI; + +reset: + ptr = str; + return 0; +} + +static int +shell_process_args(void) +{ + unsigned long i; + char c, prev; + int j; + + snprintf(shell_tmp_line, sizeof(shell_tmp_line), "%s", + shell_line_str(shell_history_get_newest())); + + for (i = 0, j = 0, prev = SHELL_SEPARATOR; + (c = shell_tmp_line[i]) != '\0'; + i++, prev = c) { + if (c == SHELL_SEPARATOR) { + if (prev != SHELL_SEPARATOR) { + shell_tmp_line[i] = '\0'; + } + } else { + if (prev == SHELL_SEPARATOR) { + shell_argv[j] = &shell_tmp_line[i]; + j++; + + if (j == ARRAY_SIZE(shell_argv)) { + printf("shell: too many arguments\n"); + return ERR_INVAL; + } + + shell_argv[j] = NULL; + } + } + } + + shell_argc = j; + return 0; +} + +static void +shell_process_line(void) +{ + const struct shell_cmd *cmd; + int error; + + cmd = NULL; + error = shell_process_args(); + + if (error) { + goto out; + } + + if (shell_argc == 0) { + goto out; + } + + cmd = shell_cmd_lookup(shell_argv[0]); + + if (cmd == NULL) { + printf("shell: %s: command not found\n", shell_argv[0]); + goto out; + } + +out: + shell_history_push(); + + if (cmd != NULL) { + cmd->fn(shell_argc, shell_argv); + } +} + +/* + * Process a single control character. + * + * Return an error if the caller should reset the current line state. + */ +static int +shell_process_ctrl_char(char c) +{ + switch (c) { + case SHELL_ERASE: + shell_process_backspace(); + break; + case '\t': + return shell_process_tabulation(); + case '\n': + putchar('\n'); + shell_process_line(); + return ERR_AGAIN; + default: + return 0; + } + + return 0; +} + +void +shell_run(void) +{ + int c, error, escape; + + for (;;) { + shell_reset(); + escape = 0; + + for (;;) { + c = getchar(); + + if (escape) { + switch (escape) { + case SHELL_ESC_STATE_START: + /* XXX CSI and SS3 sequence processing is the same */ + if ((c == '[') || (c == 'O')) { + escape = SHELL_ESC_STATE_CSI; + } else { + escape = 0; + } + + break; + case SHELL_ESC_STATE_CSI: + escape = shell_process_esc_sequence(c); + break; + default: + escape = 0; + } + + error = 0; + } else if (shell_is_ctrl_char(c)) { + if (c == '\e') { + escape = 1; + error = 0; + } else { + error = shell_process_ctrl_char(c); + + if (error) { + break; + } + } + } else { + error = shell_process_raw_char(c); + } + + if (error) { + break; + } + } + } +} + +void +shell_setup(void) +{ + unsigned long i; + int error; + + pthread_mutex_init(&shell_lock, NULL); + + for (i = 0; i < ARRAY_SIZE(shell_default_cmds); i++) { + error = shell_cmd_register(&shell_default_cmds[i]); + + if (error) + error_die(error); + } +} @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2015 Richard Braun. + * 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. + * + * + * Minimalist shell for embedded systems. + * + * Upstream site with license notes : + * http://git.sceen.net/rbraun/librbraun.git/ + */ + +#ifndef _SHELL_H +#define _SHELL_H + +typedef void (*shell_fn_t)(int argc, char *argv[]); + +struct shell_cmd { + struct shell_cmd *ht_next; + struct shell_cmd *ls_next; + const char *name; + shell_fn_t fn; + const char *usage; + const char *short_desc; + const char *long_desc; +}; + +/* + * Static shell command initializers. + */ +#define SHELL_CMD_INITIALIZER(name, fn, usage, short_desc) \ + { NULL, NULL, name, fn, usage, short_desc, NULL } +#define SHELL_CMD_INITIALIZER2(name, fn, usage, short_desc, long_desc) \ + { NULL, NULL, name, fn, usage, short_desc, long_desc } + +/* + * Initialize a shell command structure. + */ +void shell_cmd_init(struct shell_cmd *cmd, const char *name, + shell_fn_t fn, const char *usage, + const char *short_desc, const char *long_desc); + +/* + * Initialize the shell module. + * + * On return, shell commands can be registered. + */ +void shell_setup(void); + +/* + * Register a shell command. + * + * The command name must be unique. It must not include characters outside + * the [a-zA-Z0-9-_] class. + * + * The structure passed when calling this function is directly reused by + * the shell module and must persist in memory. + */ +int shell_cmd_register(struct shell_cmd *cmd); + +/* + * Run the shell. + * + * This function doesn't return. + */ +void shell_run(void); + +#endif /* _SHELL_H */ diff --git a/test/test_shell.c b/test/test_shell.c new file mode 100644 index 0000000..866ae5f --- /dev/null +++ b/test/test_shell.c @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2015 Richard Braun. + * 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. + */ + +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <termios.h> +#include <unistd.h> + +#include "../error.h" +#include "../macros.h" +#include "../shell.h" + +static void +test_exit(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + + /* Calling exit directly doesn't reset the terminal */ + raise(SIGINT); +} + +static void +test_top(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + + system("top"); +} + +static void +test_add(int argc, char *argv[]) +{ + int i, ret, tmp, total; + + if (argc < 3) { + printf("shell: add: invalid arguments\n"); + return; + } + + for (total = 0, i = 1; i < argc; i++) { + ret = sscanf(argv[i], "%d", &tmp); + + if (ret != 1) { + printf("shell: add: '%s': invalid argument\n", argv[i]); + return; + } + + total += tmp; + } + + printf("%d\n", total); +} + +static struct shell_cmd test_shell_cmds[] = { + SHELL_CMD_INITIALIZER2("add", test_add, + "add <i1 i2 [i3 ...]>", "add integers", + "The user must pass at least two integers"), + SHELL_CMD_INITIALIZER("top", test_top, + "top", "display system processes"), + SHELL_CMD_INITIALIZER("exit", test_exit, + "exit", "leave the shell"), +}; + +int +main(int argc, char *argv[]) +{ + struct termios termios; + unsigned int i; + int ret; + + (void)argc; + (void)argv; + + for (i = 0; i < ARRAY_SIZE(test_shell_cmds); i++) { + ret = shell_cmd_register(&test_shell_cmds[i]); + + if (ret) + error_die(ret); + } + + setbuf(stdin, NULL); + ret = tcgetattr(fileno(stdin), &termios); + + if (ret) { + perror("tcgetattr"); + return EXIT_FAILURE; + } + + termios.c_lflag &= ~(ICANON | ECHO); + termios.c_cc[VMIN] = 1; + ret = tcsetattr(fileno(stdin), TCSANOW, &termios); + + if (ret) { + perror("tcsetattr"); + return EXIT_FAILURE; + } + + shell_setup(); + shell_run(); + test_exit(0, NULL); +} |