summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am10
-rw-r--r--shell.c1150
-rw-r--r--shell.h87
-rw-r--r--test/test_shell.c126
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
diff --git a/shell.c b/shell.c
new file mode 100644
index 0000000..d8fa387
--- /dev/null
+++ b/shell.c
@@ -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);
+ }
+}
diff --git a/shell.h b/shell.h
new file mode 100644
index 0000000..c559fc6
--- /dev/null
+++ b/shell.h
@@ -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);
+}