Air quality sensor
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1824 lines
54 KiB

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <stdbool.h>
#include <string.h>
#include <assert.h>
#include "console/console.h"
#include "console/prefix_match.h"
#include "console/utils.h"
// library includes
#include "argtable3.h"
#include "console_linenoise.h"
#include "queue.h"
#include "console_split_argv.h"
#if CONSOLE_USE_TERMIOS && CONSOLE_USE_FILE_IO_STREAMS
static int enableRawMode(console_ctx_t *ctx, int fd);
static void disableRawMode(console_ctx_t *ctx, int fd);
#endif
static int cmd_exit(console_ctx_t *ctx, cmd_signature_t *reg);
/**
* Console errors - return codes
*/
static const char *err_names[_CONSOLE_ERR_MAX] = {
[CONSOLE_OK] = "OK",
[CONSOLE_ERROR] = "Unknown Error",
[CONSOLE_ERR_NO_MEM] = "Out of memory",
[CONSOLE_ERR_BAD_CALL] = "Illegal function call",
[CONSOLE_ERR_INVALID_ARG] = "Invalid argument(s)",
[CONSOLE_ERR_UNKNOWN_CMD] = "Unknown command",
[CONSOLE_ERR_TIMEOUT] = "Timed out",
[CONSOLE_ERR_IO] = "IO error",
[CONSOLE_ERR_NOT_POSSIBLE] = "Not Possible",
};
void console_err_print_ctx(struct console_ctx *ctx, int e) {
console_printf_ctx(ctx, COLOR_RESET, "Err(%d)", e);
if (e >= CONSOLE_OK && e < _CONSOLE_ERR_MAX) {
console_printf_ctx(ctx, COLOR_RESET, " - %s", err_names[e]);
}
}
/**
* Empty argtable used internally by commands with no arguments
*/
static struct {
struct arg_end *end;
} s_empty_argtable;
/**
* Entry in the internal commands table.
*/
typedef struct cmd_item_ {
struct cmd_signature sig; //!< Command signature
const char* group; //!< Command group (the first word, if multi-part)
const char* alias_of; //!< Aliased command name
console_command_t func; //!< Command function pointer
STAILQ_ENTRY(cmd_item_) next;
} cmd_item_t;
/**
* Commands table
*/
static STAILQ_HEAD(cmd_list_, cmd_item_) s_cmd_list = {};
typedef struct cmd_groups_item_ {
const char *group;
const char *description;
unsigned int num_commands;
STAILQ_ENTRY(cmd_groups_item_) next;
} cmd_groups_item_t;
static STAILQ_HEAD(cmd_groups_, cmd_groups_item_) s_cmd_groups = {};
enum fuzzymatch_cmd_result {
FUZZY_CMD_NO_MATCH = 0,
FUZZY_CMD_AMBIGUOUS,
FUZZY_CMD_PREFIX_MATCH,
FUZZY_CMD_EXACT_MATCH,
};
/**
* Recognize an entered command, allowing abbreviations and detecting ambiguity
*
* @param[in] name - entered command name
* @param[out] status - status of the recognition; NULL = don't care
* @param[out] pLongestNWords - word count of the longest matched command
* (may be used in case of ambiguity to show all possible alternatives of the same length)
* @return the detected command, if any
*/
static const cmd_item_t *fuzzy_recognize_command(
char *name,
enum fuzzymatch_cmd_result *status,
size_t *pLongestNWords
);
#define CMDLIST_SHOW_GROUPS 1 //!< Show groups in the command list, also hide commands that are in a group.
#define CMDLIST_SHOW_CMDS 2 //!< Show individual commands in the command list
#define CMDLIST_SHOW_ALIASES 4 //!< Include aliases in the command list
#define CMDLIST_DETAILED 8 //!< Use detailed listing format with descriptions
#define CMDLIST_GROUPCONTENT 16 //!< Showing what's inside a group
/**
* The "body" of the "ls" command
*/
static console_err_t do_list_commands(console_ctx_t *ctx, const char *filter_group, uint16_t flags);
/** Line buffer used for command parsing */
static char s_line_buf[CONSOLE_LINE_BUF_LEN];
/** Argv array, holds pointers to s_line_buf. */
static char *s_argv[CONSOLE_MAX_NUM_ARGS];
/** Command eval lock, guarding shared state */
#if CONSOLE_USE_FREERTOS
static console_mutex_t s_console_eval_mutex;
#endif
/** Flag that console was already inited */
static volatile bool console_inited = false;
/** Console config (local copy) */
static console_config_t s_config = CONSOLE_CONFIG_DEFAULTS();
/** Global pointer to console context, valid within a command handler only */
struct console_ctx * console_active_ctx = NULL;
/**
* Register default console commands (help, ls, exit)
*/
static void register_default_commands(void);
static const cmd_item_t *find_command_by_name(const char *name);
static const cmd_item_t *find_command_by_handler(console_command_t handler);
/** Append to a buffer with a maximal capacity */
static char *strbufcat(char *dest, size_t bufcap, const char*src) {
size_t available = bufcap - strlen(dest) - 1;
if (available == 0) return dest;
return strncat(dest, src, available);
}
/** Append at most num chars to a buffer with a maximal capacity */
static char *strnbufcat(char *dest, size_t bufcap, const char*src, size_t num) {
size_t available = bufcap - strlen(dest) - 1;
if (num < available) {
available = num;
}
return strncat(dest, src, available);
}
/** Append to a buffer with a maximal capacity */
static inline char *strbufcat1(char *dest, size_t bufcap, char src) {
return strnbufcat(dest, bufcap, &src, 1);
}
/**
* @brief Callback which provides command completion for linenoise library
*
* When using linenoise for line editing, command completion support
* can be enabled like this:
*
* linenoiseSetCompletionCallback(&console_get_completion);
*
* @param buf the string typed by the user
* @param lc linenoiseCompletions to be filled in
*/
static void console_get_completion(const char *buf, linenoiseCompletions *lc);
/**
* @brief Callback which provides command hints for linenoise library
*
* When using linenoise for line editing, hints support can be enabled as
* follows:
*
* linenoiseSetHintsCallback((linenoiseHintsCallback*) &console_get_hint);
*
* The extra cast is needed because linenoiseHintsCallback is defined as
* returning a char* instead of const char*.
*
* @param[in] buf line typed by the user
* @param[out] color ANSI color code to be used when displaying the hint
* @param[out] bold set to 1 if hint has to be displayed in bold
* @return string containing the hint text. This string is persistent and should
* not be freed (i.e. linenoiseSetFreeHintsCallback should not be used).
*/
static const char *console_get_hint(const char *buf, int *color, int *bold);
void __attribute__((weak)) console_internal_error_print(const char *msg) {
printf("\x1b[31m%s\x1b[m\n", msg);
}
/**
* Fill defaults, preserve `__internal_heap_allocated` and set MAGIC
*/
static void console_ctx_defaults(struct console_ctx *ctx) {
// clear, preserving the HA flag
bool ha = ctx->__internal_heap_allocated;
bzero(ctx, sizeof(console_ctx_t));
ctx->__internal_heap_allocated = ha;
ctx->__internal_magic = CONSOLE_CTX_MAGIC;
ctx->exit_allowed = true;
ctx->interactive = true;
ctx->use_colors = true;
strcpy(ctx->prompt, "> ");
}
console_err_t console_init(const console_config_t *config)
{
if (console_inited) {
return CONSOLE_OK; // no-op
}
if (config) {
memcpy(&s_config, config, sizeof(struct console_config));
}
s_empty_argtable.end = arg_end(1);
STAILQ_INIT(&s_cmd_list);
STAILQ_INIT(&s_cmd_groups);
#if CONSOLE_USE_FREERTOS
s_console_eval_mutex = xSemaphoreCreateMutex();
if (!s_console_eval_mutex) {
return CONSOLE_ERR_NO_MEM;
}
#elif CONSOLE_USE_PTHREADS
if (pthread_mutex_init(&s_console_eval_mutex, NULL) != 0) {
printf("\n mutex init has failed\n");
return 1;
}
#endif
// 'console_inited' must be set before calling 'register_default_commands()'
// because there is a check in the register function.
console_inited = true;
register_default_commands();
#if CONSOLE_TESTING_ALLOC_FUNCS
// test malloc
char *buf = console_malloc(11);
assert(buf);
strcpy(buf, "0123456789");
assert(0 == strcmp(buf, "0123456789"));
// no-op does not change the pointer
char *reallocated = console_realloc(buf, 11, 11);
assert(reallocated == buf);
buf = reallocated; reallocated = NULL;
// growing does
reallocated = console_realloc(buf, 11, 20);
assert(reallocated != buf);
assert(0 == strcmp(reallocated, "0123456789"));
buf = reallocated; reallocated = NULL;
// test that we can write into
strcat(buf, "banana");
assert(0 == strcmp(buf, "0123456789banana"));
char *copy = console_strdup(buf);
assert(copy != buf);
assert(0 == strcmp(buf, "0123456789banana"));
assert(0 == strcmp(copy, "0123456789banana"));
assert(0 == copy[16]);
char *copy2 = console_strndup(buf, 5);
assert(0 == strcmp(copy2, "01234"));
assert(0 == copy2[5]);
assert(copy2 != buf);
uint8_t *calloced = console_calloc(100,1);
for(int i=0;i<100;i++) {
assert(calloced[i] == 0);
}
console_free(calloced);
console_free(copy);
console_free(copy2);
console_free(buf);
console_free(NULL);
#endif
return CONSOLE_OK;
}
/**
* Extract "command group" from a command name.
*
* Single-word commands have NULL group.
*
* @param[in] name - command signature
* @return the first word of the command, or NULL
*/
static const char *command_name_to_group(const char *name) {
if (pm_count_words(name, " ") <= 1) {
return NULL;
}
const char *end = pm_skip_words(name, " ", 1);
size_t len = end - name;
// Look if we already have the group defined - avoids a needless strdup
struct cmd_groups_item_ *it = NULL;
STAILQ_FOREACH(it, &s_cmd_groups, next) {
if (strncmp(name, it->group, len) == 0 && it->group[len] == 0) {
return it->group;
}
}
return console_strndup(name, len);
}
static console_err_t do_add_group(const char *name, const char *descr, bool increment_cmds) {
if (!name) return CONSOLE_ERR_INVALID_ARG;
// iterate groups and look if we already know this one
cmd_groups_item_t *it = NULL;
bool known = false;
STAILQ_FOREACH(it, &s_cmd_groups, next) {
/* Check if command starts with buf */
if (strcmp(name, it->group) == 0) {
known = true;
if (descr) {
// maybe it was already spawned by a command.
it->description = descr;
}
if (increment_cmds) {
it->num_commands++;
}
break;
}
}
if (!known) {
// add new group to the group list
cmd_groups_item_t *grp = console_calloc(1, sizeof(cmd_groups_item_t));
if (!grp) return CONSOLE_ERR_NO_MEM;
grp->group = name; // borrow it forever
grp->description = descr; // borrow it forever
if (increment_cmds) {
grp->num_commands++;
}
STAILQ_INSERT_TAIL(&s_cmd_groups, grp, next);
}
return 0;
}
console_err_t console_group_add(const char *name, const char *descr) {
return do_add_group(name, descr, false);
}
static console_err_t add_command_to_list(cmd_item_t *cmd) {
if (cmd->group/* && !cmd->alias_of*/) {
console_err_t rv = do_add_group(cmd->group, NULL, true);
if (rv != CONSOLE_OK) {
return rv;
}
}
STAILQ_INSERT_TAIL(&s_cmd_list, cmd, next);
return 0;
}
static console_err_t add_alias(const cmd_item_t *original, const char *alias)
{
// check for duplicate
const cmd_item_t *existing = find_command_by_name(alias);
if (existing) {
fprintf(stderr, "Console: Command already exists: %s\n", alias);
return CONSOLE_ERR_UNKNOWN_CMD;
}
cmd_item_t *item2 = (cmd_item_t *) console_calloc(1, sizeof(cmd_item_t));
if (!item2) return CONSOLE_ERR_NO_MEM;
memcpy(item2, original, sizeof(cmd_item_t));
item2->alias_of = original->sig.command;
item2->sig.command = alias;
item2->group = command_name_to_group(alias);
item2->next.stqe_next = NULL;
add_command_to_list(item2);
return CONSOLE_OK;
}
console_err_t console_cmd_add_alias(const char *original, const char *alias)
{
// find command to copy
const cmd_item_t *item = find_command_by_name(original);
if (!item) {
fprintf(stderr, "Console: Unknown command to alias: %s\n", original);
return CONSOLE_ERR_UNKNOWN_CMD;
}
return add_alias(item, alias);
}
console_err_t console_cmd_add_alias_fn(console_command_t handler, const char *alias)
{
// find command to copy
const cmd_item_t *item = find_command_by_handler(handler);
if (!item) {
fprintf(stderr, "Console: Unknown command to alias: %p\n", handler);
return CONSOLE_ERR_UNKNOWN_CMD;
}
return add_alias(item, alias);
}
static void print_arg_hint_to_buffer(char *argbuf, size_t argbuf_cap, const struct arg_hdr *arg) {
const bool have_value = (arg->flag & ARG_HASVALUE);
const bool have_short = arg->shortopts;
const bool have_long = arg->longopts;
const bool optional = arg->mincount==0;
strbufcat1(argbuf, argbuf_cap, ' ');
if (optional) {
strbufcat1(argbuf, argbuf_cap, '[');
}
if (have_short) {
strbufcat1(argbuf, argbuf_cap, '-');
strnbufcat(argbuf, argbuf_cap, arg->shortopts, 1);
}
if (have_long) {
strbufcat(argbuf, argbuf_cap, have_short?"|--":"--");
strbufcat(argbuf, argbuf_cap, arg->longopts);
}
if (have_value) {
if (have_short || have_long) {
strbufcat1(argbuf, argbuf_cap, have_short? ' ' : '=');
}
strbufcat(argbuf, argbuf_cap, arg->datatype);
}
if (optional) {
strbufcat1(argbuf, argbuf_cap, ']');
}
}
console_err_t console_cmd_register(console_command_t handler, const char *name)
{
if (!console_inited) {
console_internal_error_print("console_cmd_register: not inited!");
return CONSOLE_ERR_BAD_CALL;
}
if (!handler) {
console_internal_error_print("console_cmd_register: NULL handler!");
return CONSOLE_ERR_INVALID_ARG;
}
if (!name || name[0]==0) {
console_internal_error_print("console_cmd_register: empty name!");
return CONSOLE_ERR_INVALID_ARG;
}
const cmd_item_t *existing = NULL;
// // If the command is already registered, create an alias.
// const cmd_item_t *existing = find_command_by_handler(handler);
// if (existing) {
// return add_alias(existing, name);
// }
// check for duplicate
existing = find_command_by_name(name);
if (existing) {
fprintf(stderr, "Console: Command already exists: %s\n", name);
return CONSOLE_ERR_UNKNOWN_CMD;
}
cmd_item_t *item = (cmd_item_t *) console_calloc(1, sizeof(cmd_item_t));
if (item == NULL) {
return CONSOLE_ERR_NO_MEM;
}
item->func = handler;
item->sig.command = name;
handler(NULL, &item->sig);
item->sig.command = name; // discard possible changes made inside the function
item->group = command_name_to_group(name);
if (item->sig.argtable) {
if (NULL == item->sig.hint) {
/* Generate hint based on cmd->argtable */
#if CONSOLE_USE_MEMSTREAM
char *buf = NULL;
size_t buf_size = 0;
FILE *f = open_memstream(&buf, &buf_size);
if (f != NULL) {
arg_print_syntax(f, item->sig.argtable, NULL);
fclose(f);
}
item->sig.hint = buf; // hint stays on heap
#else
// this is a fallback replacement for the argtable version
struct arg_hdr **table = item->sig.argtable;
int tabindex = 0;
const size_t argbuf_cap = CONSOLE_LINE_BUF_LEN;
char *argbuf = s_line_buf; // we can use 's_line_buf' as scratch here, certainly no command is running yet
*argbuf = 0;
for(tabindex = 0;
table[tabindex]
&& !(table[tabindex]->flag & ARG_TERMINATOR)
&& (tabindex < CONSOLE_MAX_NUM_ARGS);
tabindex++) {
print_arg_hint_to_buffer(argbuf, argbuf_cap, table[tabindex]);
}
size_t real_len = strlen(argbuf);
if (real_len > 0) {
char *copy = console_malloc(real_len + 1);
if (copy) {
memcpy(copy, argbuf, real_len + 1);
item->sig.hint = copy; // stays on the heap
}
}
#endif
}
}
else {
item->sig.argtable = &s_empty_argtable;
// hint is NULL
}
// Add the new entry to the list
add_command_to_list(item);
return CONSOLE_OK;
}
size_t console_count_commands(void)
{
assert(console_inited);
size_t count = 0;
cmd_item_t *it = NULL;
STAILQ_FOREACH(it, &s_cmd_list, next) {
count++;
}
return count;
}
static void console_get_completion(const char *buf, linenoiseCompletions *lc)
{
assert(console_inited);
assert(NULL != buf);
assert(NULL != lc);
size_t len = strlen(buf);
if (len == 0) {
return;
}
cmd_item_t *it = NULL;
STAILQ_FOREACH(it, &s_cmd_list, next) {
/* Check if command starts with buf */
if (strncmp(buf, it->sig.command, len) == 0) {
consLnAddCompletion((linenoiseCompletions *) lc, it->sig.command);
}
}
}
static const char *console_get_hint(const char *buf, int *color, int *bold)
{
assert(console_inited);
assert(NULL != buf);
assert(NULL != color);
assert(NULL != bold);
size_t len = strlen(buf);
cmd_item_t *it = NULL;
STAILQ_FOREACH(it, &s_cmd_list, next) {
if (strlen(it->sig.command) == len &&
strncmp(buf, it->sig.command, len) == 0) {
*color = 35; // purple
*bold = false;
return it->sig.hint ? it->sig.hint : "";
}
}
return NULL;
}
static const cmd_item_t *find_command_by_name(const char *name)
{
assert(console_inited);
assert(NULL != name);
const cmd_item_t *cmd = NULL;
cmd_item_t *it = NULL;
STAILQ_FOREACH(it, &s_cmd_list, next) {
if (prefix_multipart_test(name, it->sig.command, " ", PREFIXMATCH_NOABBREV) == PM_TEST_MATCH) {
cmd = it;
break;
}
}
return cmd;
}
static const cmd_item_t *find_command_by_handler(console_command_t handler)
{
assert(console_inited);
assert(NULL != handler);
const cmd_item_t *cmd = NULL;
cmd_item_t *it = NULL;
STAILQ_FOREACH(it, &s_cmd_list, next) {
if (it->func == handler) {
cmd = it;
break;
}
}
return cmd;
}
/**
* Print help for one command. Must be called in a command context.
*
* @param it - the command
* @param with_args - to show args
* @return success
*/
static console_err_t print_help_onecmd(const cmd_item_t *cmd, bool with_args);
static void print_command_suggestions(char *cmdline) {
cmd_item_t *it = NULL;
STAILQ_FOREACH(it, &s_cmd_list, next) {
// if a command is multi-part
size_t full_nwords = pm_count_words(it->sig.command, " ");
for (size_t nwords = full_nwords; nwords >= 1; nwords--) {
char *end = (char *) pm_skip_words(cmdline, " ", nwords);
if (!end) break;
char old_end = *end; // backup the old byte at the end position
*end = 0; // terminate the string there
if (0 != prefix_multipart_test(cmdline, it->sig.command, " ", PREFIXMATCH_MULTI_PARTIAL)) {
console_color_printf(COLOR_YELLOW, "-> %s\n", it->sig.command);
break; // use the longest match
}
*end = old_end;
}
}
}
static void print_ambiguous_command_suggestions(char *cmdline, size_t longest_nwords) {
cmd_item_t *it = NULL;
STAILQ_FOREACH(it, &s_cmd_list, next) {
// if a command is multi-part
size_t nwords = pm_count_words(it->sig.command, " ");
char *end = (char *) pm_skip_words(cmdline, " ", nwords);
if (!end) continue;
char old_end = *end; // backup the old byte at the end position
*end = 0; // terminate the string there
if (1 == prefix_multipart_test(cmdline, it->sig.command, " ", 0)) {
// use the longest matching command
if (nwords == longest_nwords) {
console_color_printf(COLOR_YELLOW, "-> %s\n", it->sig.command);
}
}
*end = old_end;
}
}
/**
* Show command group info, called from the handle function.
*
* s_line_buf must contain the raw unprocessed command.
*
* @param ctx
* @param g
* @return
*/
static console_err_t print_group_info(console_ctx_t *ctx, const cmd_groups_item_t *g)
{
console_err_t rv;
const char *descr = g->description ? g->description : "Command group:";
if (ctx->use_colors) {
console_color_printf(COLOR_GREEN, "%s\n", descr);
} else {
console_printf("%s\n", descr);
}
// catch attempts to show details
bool detailed = (strstr(s_line_buf, "-h") != NULL) ||
(strstr(s_line_buf, "-d") != NULL) ||
(strstr(s_line_buf, "-v") != NULL);
// remove the -h etc
char *first_hyphen = strchr(s_line_buf, '-');
if (first_hyphen) {
*first_hyphen = 0;
}
rv = do_list_commands(ctx, s_line_buf, (detailed ? CMDLIST_DETAILED : 0) | CMDLIST_SHOW_CMDS | CMDLIST_GROUPCONTENT);
if (!detailed) {
console_printf("Use -h to show command descriptions.\n");
}
return rv;
}
console_err_t console_handle_cmd(console_ctx_t *ctx, const char *cmdline, int *pRetval, const struct cmd_signature **pCommandSig)
{
if (!console_inited) {
console_internal_error_print("console_handle_cmd: not inited!");
return CONSOLE_ERR_BAD_CALL;
}
if (!ctx) {
console_internal_error_print("console_handle_cmd: NULL ctx!");
return CONSOLE_ERR_INVALID_ARG;
}
if (!cmdline) {
console_internal_error_print("console_handle_cmd: NULL cmdline!");
return CONSOLE_ERR_INVALID_ARG;
}
// cmd_ret may be null
int argc;
console_err_t rv;
// ensure retval is inited
if (pRetval) {
*pRetval = 0;
}
if (pCommandSig) {
*pCommandSig = NULL;
}
#if CONSOLE_USE_FREERTOS
if (pdPASS != xSemaphoreTake(s_console_eval_mutex, pdMS_TO_TICKS(s_config.execution_lock_timeout_ms))) {
return CONSOLE_ERR_TIMEOUT;
}
#elif CONSOLE_USE_PTHREADS
struct timespec timeoutTime;
clock_gettime(CLOCK_REALTIME, &timeoutTime);
timeoutTime.tv_nsec += s_config.execution_lock_timeout_ms*1000LL;
if(0 != pthread_mutex_timedlock(&s_console_eval_mutex, &timeoutTime)) {
return CONSOLE_ERR_TIMEOUT;
}
#endif
console_active_ctx = ctx;
strncpy(s_line_buf, cmdline, CONSOLE_LINE_BUF_LEN);
s_line_buf[CONSOLE_LINE_BUF_LEN - 1] = 0; // ensure it is terminated
enum fuzzymatch_cmd_result status = FUZZY_CMD_NO_MATCH;
size_t longest_nwords = 0;
const cmd_item_t *cmd = fuzzy_recognize_command(s_line_buf, &status, &longest_nwords);
if (status == FUZZY_CMD_AMBIGUOUS) {
// check if we have an exact match among groups
cmd_groups_item_t *g = NULL;
STAILQ_FOREACH(g, &s_cmd_groups, next) {
/* Check if command starts with buf */
const size_t grouplen = strlen(g->group);
if (strncmp(s_line_buf, g->group, grouplen) == 0 &&
((s_line_buf[grouplen] == 0) || (s_line_buf[grouplen] == ' ')))
{
rv = print_group_info(ctx, g);
goto exit;
}
}
}
if (cmd == NULL) {
// there is no match
if (status == FUZZY_CMD_NO_MATCH) {
// Check if this is a group name - if so, show group members.
cmd_groups_item_t *g = NULL;
STAILQ_FOREACH(g, &s_cmd_groups, next) {
/* Check if command starts with buf */
const size_t grouplen = strlen(g->group);
if (strncmp(s_line_buf, g->group, grouplen) == 0 &&
(s_line_buf[grouplen] == 0 || s_line_buf[grouplen] == ' '))
{
rv = print_group_info(ctx, g);
goto exit;
}
}
// no match in groups...
if (longest_nwords > 0) {
console_color_printf(COLOR_RED, "Unknown command. Partial matches:\n");
print_command_suggestions(s_line_buf);
} else {
console_color_printf(COLOR_RED, "Unknown command.\n");
}
} else if (status == FUZZY_CMD_AMBIGUOUS) {
console_color_printf(COLOR_RED, "Ambiguous command. Possible matches:\n");
// This dump is shown in all cases, even non-interactive, to help debugging.
print_ambiguous_command_suggestions(s_line_buf, longest_nwords);
}
rv = CONSOLE_ERR_UNKNOWN_CMD;
goto exit;
}
if (ctx->interactive && status != FUZZY_CMD_EXACT_MATCH) {
// Show the recognized command
console_color_printf(COLOR_YELLOW, "%s\n", cmd->sig.command);
}
argc = console_split_argv(s_line_buf, s_argv, CONSOLE_MAX_NUM_ARGS);
if (argc == 0) {
rv = CONSOLE_ERR_BAD_CALL;
goto exit;
}
// help for all commands
for (int i = 1; i < argc; i++) {
if (0 == strcmp(s_argv[i], "-h") || 0 == strcmp(s_argv[i], "-?") || 0 == strcmp(s_argv[i], "--help")) {
print_help_onecmd(cmd, true);
if (pRetval != NULL) {
*pRetval = 0;
}
rv = CONSOLE_OK;
goto exit;
}
}
// now we know it's the command we want.
// we have to manipulate argv to match the command signature (join multipart command words)
if (longest_nwords > 1) {
// const is discarded here - it's OK. nobody should try to free the strings, as they are normally
// pieces of the global static char array
s_argv[0] = (char *) cmd->sig.command; // This is safe, since user code see it through a const*
// shift the tail
for (int i = longest_nwords, j = 1; i <= argc; i++, j++) {
s_argv[j] = s_argv[i];
}
argc -= (int)longest_nwords - 1;
}
/* Run the command */
if (pCommandSig) {
*pCommandSig = &cmd->sig;
}
if (!cmd->sig.custom_args) {
// Parse argv using argtable3
int nerrors = arg_parse(argc, s_argv, (void **) cmd->sig.argtable);
if (nerrors != 0) {
#if CONSOLE_USE_MEMSTREAM
// find end
struct arg_hdr **arg = cmd->sig.argtable;
int depth = 0;
while (0 == ((*arg)->flag & ARG_TERMINATOR) && (depth < CONSOLE_MAX_NUM_ARGS)) {
arg++;
depth++;
}
// temporary stream FILE for argtable
char *buf = NULL;
size_t buf_size = 0;
FILE *f = open_memstream(&buf, &buf_size);
if (!f) {
rv = CONSOLE_ERR_NO_MEM;
goto exit;
}
arg_print_errors(f, (struct arg_end *) *arg, cmd->sig.command);
// clean up
fclose(f);
console_print(buf);
free(buf); // allocated by memstream
#else
console_printf_ctx(ctx, COLOR_RED, "bad args\n");
#endif
rv = CONSOLE_ERR_INVALID_ARG;
goto exit;
}
}
// Expose some context information to the command handler
ctx->argv = (const char **) &s_argv[0];
ctx->argc = argc;
ctx->cmd = &cmd->sig;
int cmd_rv = (*cmd->func)(ctx, NULL);
if (pRetval) {
*pRetval = cmd_rv;
}
// Clear the context pointers
ctx->argv = NULL;
ctx->argc = 0;
ctx->cmd = NULL;
rv = CONSOLE_OK;
// fall through
exit:
console_active_ctx = NULL;
#if CONSOLE_USE_FREERTOS
xSemaphoreGive(s_console_eval_mutex);
#elif CONSOLE_USE_PTHREADS
pthread_mutex_unlock(&s_console_eval_mutex);
#endif
return rv;
}
static console_err_t print_help_onecmd(const cmd_item_t *cmd, bool with_args)
{
assert(NULL != cmd);
/* First line: command name and hint
* Pad all the hints to the same column
*/
const char *hint = (cmd->sig.hint) ? cmd->sig.hint : "";
if (console_active_ctx->use_colors) {
console_printf("\x1b[1m%s\x1b[22;35m%s\x1b[m\n", cmd->sig.command, hint);
} else {
console_printf("%-s%s\n", cmd->sig.command, hint);
}
bool any_aliases = false;
cmd_item_t *iter;
/* Print all aliases */
STAILQ_FOREACH(iter, &s_cmd_list, next) {
if (cmd != iter && cmd->func == iter->func) {
if (!any_aliases) {
console_print(" (aliases: ");
} else {
console_print(", ");
}
any_aliases = true;
if (console_active_ctx->use_colors) {
console_printf("\x1b[1m%s\x1b[m", iter->sig.command);
} else {
console_print(iter->sig.command);
}
}
}
if (any_aliases) {
console_print(")\n");
}
#if CONSOLE_USE_MEMSTREAM
// argtable3 needs a FILE*
char *buf = NULL;
size_t buf_size = 0;
FILE *capf = open_memstream(&buf, &buf_size);
if (!capf) return CONSOLE_ERR_NO_MEM;
if (cmd->sig.help && cmd->sig.help[0] != 0) {
/* Second line: print help.
* Argtable has a nice helper function for this which does line
* wrapping.
*/
console_printf(" "); // arg_print_formatted does not indent the first line
arg_print_formatted(capf, 2, 78, cmd->sig.help);
}
if (with_args) {
/* Finally, print the list of arguments */
if (cmd->sig.argtable) {
arg_print_glossary(capf, (void **) cmd->sig.argtable, " %12s %s\n");
}
fputs("\n", capf);
}
// clean up the memstream FILE
fclose(capf);
console_print(buf);
free(buf); // allocated by memstream
#else
// this is a fallback replacement for the argtable version
if (cmd->sig.help && cmd->sig.help[0] != 0) {
console_printf(" %s\n", cmd->sig.help);
}
const size_t argbuf_cap = 32;
char argbuf[32];
struct arg_hdr **table = cmd->sig.argtable;
int tabindex = 0;
for(tabindex = 0;
table[tabindex]
&& !(table[tabindex]->flag & ARG_TERMINATOR)
&& (tabindex < CONSOLE_MAX_NUM_ARGS);
tabindex++) {
*argbuf = 0;
print_arg_hint_to_buffer(argbuf, argbuf_cap, table[tabindex]);
console_print(" ");
console_print(argbuf);
console_print(" ");
console_println(table[tabindex]->glossary);
}
#endif
return CONSOLE_OK;
}
static const cmd_item_t *fuzzy_recognize_command(char *name, enum fuzzymatch_cmd_result *pStatus, size_t *pLongestNWords) {
size_t source_words = pm_count_words(name, " ");
cmd_item_t *it = NULL;
size_t longest_nwords = 0;
bool ambiguous = false;
bool exact_match = false;
const cmd_item_t *cmd = NULL;
STAILQ_FOREACH(it, &s_cmd_list, next) {
size_t nwords = pm_count_words(it->sig.command, " ");
char *end = (char*) pm_skip_words(name, " ", nwords);
if (!end) continue;
char old_end = *end; // backup the old byte at the end position
*end = 0; // terminate the string there
if (1 == prefix_multipart_test(name, it->sig.command, " ", 0)) {
// use the longest matching command
if (!cmd || nwords > longest_nwords) {
cmd = it;
longest_nwords = nwords;
ambiguous = false; // we found a longer match
exact_match = (PM_TEST_MATCH == prefix_multipart_test(name, it->sig.command, " ", PREFIXMATCH_NOABBREV));
if (exact_match && nwords == source_words) {
goto fuzzy_done;
}
} else if (nwords == longest_nwords && !exact_match) {
if (PM_TEST_MATCH == prefix_multipart_test(name, it->sig.command, " ", PREFIXMATCH_NOABBREV)) {
ambiguous = false; // we found a longer match
exact_match = true;
cmd = it;
} else {
if (cmd->alias_of == it->sig.command || it->alias_of == cmd->sig.command) {
// We have two aliases of the same command, not really ambiguous
} else {
exact_match = false;
ambiguous = true; // there is an ambiguity between two commands
}
}
}
}
*end = old_end;
}
fuzzy_done:
if (pLongestNWords) *pLongestNWords = longest_nwords;
if (ambiguous) {
if (pStatus) *pStatus = FUZZY_CMD_AMBIGUOUS;
return NULL;
} else {
if (pStatus) *pStatus = cmd ?
(exact_match ? FUZZY_CMD_EXACT_MATCH : FUZZY_CMD_PREFIX_MATCH) :
FUZZY_CMD_NO_MATCH;
return cmd;
}
}
/**
* Command: Show help
*/
static int cmd_help(console_ctx_t *ctx, cmd_signature_t *reg)
{
// this struct is only used to build the hint
static struct {
struct arg_str *cmd;
struct arg_end *end;
} args;
if (reg) {
args.cmd = arg_str1(NULL, NULL, "<cmd>", "Command to describe (can be multi-part)");
args.end = arg_end(2);
reg->custom_args = true;
reg->help = "Show the help page for a command, or list all commands and their basic usage";
reg->argtable = &args;
return 0;
}
/* recreate the original multi-part command, if given as multiple space-separated arguments */
// HACK: using s_line_buf as a scratch buffer
char *const cmdname = s_line_buf;
char *p = cmdname;
*p = 0; // clear the string
for (size_t i = 1; i < ctx->argc; i++) {
p += sprintf(p, "%s ", ctx->argv[i]);
}
if (p != cmdname) p--; // remove the trailing space
*p = 0; // add terminator after the built sequence
if (*s_line_buf != 0) {
// a command is selected
enum fuzzymatch_cmd_result status = FUZZY_CMD_NO_MATCH;
size_t longest_nwords = 0;
const cmd_item_t *cmd = fuzzy_recognize_command(s_line_buf, &status, &longest_nwords);
if (status == FUZZY_CMD_NO_MATCH) {
console_color_printf(COLOR_RED, "\nHelp: No command matches: \"%s\"\n", s_line_buf);
print_command_suggestions(s_line_buf);
return CONSOLE_ERR_UNKNOWN_CMD;
}
if (status == FUZZY_CMD_AMBIGUOUS) {
console_color_printf(COLOR_RED, "\nHelp: Ambiguous command: \"%s\"\n", s_line_buf);
print_ambiguous_command_suggestions(s_line_buf, longest_nwords);
return CONSOLE_ERR_UNKNOWN_CMD;
}
assert(cmd != NULL);
if (!ctx->exit_allowed && cmd->func == cmd_exit) {
console_print("\n\"exit\" is not available in this session.\n");
return 0;
}
print_help_onecmd(cmd, true);
return 0;
} else {
cmd_item_t *it;
/* Print summary of each command */
STAILQ_FOREACH(it, &s_cmd_list, next) {
if (it->alias_of) continue; // aliases are listed as part of the regular entry
if (!ctx->exit_allowed && it->func == cmd_exit) {
continue;
}
print_help_onecmd(it, false);
}
return 0;
}
}
const char* LS_USE_HELP_TO_LEARN_MORE = EXPENDABLE_STRING("Use `cmd -h` or `help cmd` to learn more about a command.\n");
/**
* Command: List all commands in a short format.
*/
static int cmd_list(console_ctx_t *ctx, cmd_signature_t *reg)
{
// this struct is only used to build the hint
static struct {
struct arg_lit *all;
struct arg_lit *aliases;
struct arg_lit *describe;
struct arg_lit *groupsonly;
struct arg_str *group;
struct arg_end *end;
} args;
if (reg) {
args.group = arg_str0(NULL, NULL, "GROUP", EXPENDABLE_STRING("Show commands in a group, or starting with..."));
args.all = arg_lit0("a", "all", EXPENDABLE_STRING("Show all commands, disable grouping"));
args.describe = arg_lit0("d", "descr", EXPENDABLE_STRING("Show one command per line, with descriptions"));
args.groupsonly = arg_lit0("g", "groups", EXPENDABLE_STRING("Show only groups"));
args.aliases = arg_lit0("A", "aliases", EXPENDABLE_STRING("Include command aliases"));
args.end = arg_end(3);
reg->argtable = &args;
reg->help = EXPENDABLE_STRING("List available commands");
return 0;
}
const char * const filter_group = args.group->count ? args.group->sval[0] : NULL;
// fake the -a flag if executed as "la"
if (0 == strcmp("la", ctx->argv[0])) {
args.all->count = 1;
}
// fake the -d flag if executed as "ll"
if (0 == strcmp("ll", ctx->argv[0])) {
args.describe->count = 1;
}
bool show_groups = !args.all->count && !args.group->count;
bool show_commands = !args.groupsonly->count;
bool show_aliases = args.aliases->count;
if (args.all->count && filter_group) {
console_color_printf(COLOR_RED, "Filter argument cannot be used together with \"-a\"");
return CONSOLE_ERR_INVALID_ARG;
}
return do_list_commands(ctx, args.all->count ? NULL : filter_group,
(show_groups ? CMDLIST_SHOW_GROUPS : 0) |
(show_commands ? CMDLIST_SHOW_CMDS : 0) |
(show_aliases ? CMDLIST_SHOW_ALIASES : 0) |
(args.describe->count ? CMDLIST_DETAILED : 0));
}
static console_err_t do_list_commands(console_ctx_t *ctx, const char *filter_group, uint16_t flags) {
#define SKIPLIST_LEN 32
uint32_t skipmask[SKIPLIST_LEN] = {};// should be enough
bool have_spaces = pm_count_words(filter_group, " \t") > 0;
int first_word_len = pm_word_len(filter_group, " \t");
// Count commands
int count = 0;
int index = 0;
int max_cmd_len = 0;
cmd_item_t *it;
cmd_groups_item_t *grp;
const bool show_groups = flags & CMDLIST_SHOW_GROUPS;
const bool show_commands = flags & CMDLIST_SHOW_CMDS;
const bool show_aliases = flags & CMDLIST_SHOW_ALIASES;
const bool detailed = flags & CMDLIST_DETAILED;
const bool ingroup = flags & CMDLIST_GROUPCONTENT;
bool any_matches = false;
bool any_aliases_shown = false;
bool any_groups_shown = false;
if (show_groups) {
any_matches = true;
STAILQ_FOREACH(grp, &s_cmd_groups, next) {
int len = (int) strlen(grp->group);
if (len > max_cmd_len) {
max_cmd_len = len;
}
count++;
index++;
any_groups_shown = true;
}
}
if (show_commands) {
STAILQ_FOREACH(it, &s_cmd_list, next) {
if (!ctx->exit_allowed && it->func == cmd_exit) {
goto skip;
}
if (!show_aliases && it->alias_of) {
goto skip;
}
if (filter_group) {
// user tries to filter.
// that means groups are hidden and then "all" listing is used.
bool grp_matches = (it->group &&
0 == strncasecmp(filter_group, it->group, MAX(first_word_len, strlen(it->group))) &&
(have_spaces || it->group[strlen(filter_group)]==0)
);
bool prefix_matches = 0 == strncasecmp(filter_group, it->sig.command, strlen(filter_group));
if (!(grp_matches || (!ingroup && prefix_matches))) {
goto skip;
}
if (grp_matches && !prefix_matches) {
// bad prefix
goto skip;
}
}
else {
// no filter word, this is the basic listing
if (show_groups && it->group) {
// do not show command that is grouped
goto skip;
}
}
int len = (int) strlen(it->sig.command);
if (len > max_cmd_len) {
max_cmd_len = len;
}
index++;
count++;
any_matches = true;
if (it->alias_of) {
any_aliases_shown = true;
}
continue;
skip:
assert((index >> 5) < SKIPLIST_LEN);
skipmask[index >> 5] |= (1 << (index & 31));
index++;
}
}
if (!any_matches) {
console_print("No matching command or group found.\n");
return CONSOLE_OK;
}
if (detailed) {
// Show descriptions for all matched commands
index = 0;
if (show_groups) {
STAILQ_FOREACH(grp, &s_cmd_groups, next) {
if (skipmask[index>>5] & (1<<(index&31))) {
index++; continue;
}
index++;
if (console_active_ctx->use_colors) {
console_printf("\x1b[1m%s\x1b[m\x1b[22;35m Group with %d sub-commands\x1b[m\n", grp->group, grp->num_commands);
if (grp->description) {
console_printf(" %s\n", grp->description);
}
} else {
console_printf("%s - Group with %d sub-commands\n", grp->group, grp->num_commands);
if (grp->description) {
console_printf(" %s\n", grp->description);
}
}
}
}
if (show_commands) {
STAILQ_FOREACH(it, &s_cmd_list, next) {
if (skipmask[index >> 5] & (1 << (index & 31))) {
index++;
continue;
}
index++;
print_help_onecmd(it, false);
}
}
console_print("\n");
console_print(LS_USE_HELP_TO_LEARN_MORE);
return CONSOLE_OK;
}
// this is a multi-column ls-like list of all commands
const int COLW = max_cmd_len + 2;
const int COLN = (
count < 6 ? 1 : count <= 12 ? 3 :
(80 / COLW));
const int LINELEN = COLW * COLN + 1;
// resolve number of rows needed for the full output
const int paddedcount = count + (COLN>1?(COLN - count % COLN):0); // round up to a multiple of COLN
const int rows = paddedcount / COLN;
const size_t buffers_cap = (size_t) rows * LINELEN;
char *buffers = console_malloc(buffers_cap);
if (!buffers) {
return CONSOLE_ERR_NO_MEM;
}
memset(buffers, ' ', buffers_cap);
buffers[buffers_cap-1] = 0;
int col = 0;
int row = 0;
index = 0;
if (show_groups) {
STAILQ_FOREACH(grp, &s_cmd_groups, next) {
if (skipmask[index>>5] & (1<<(index&31))) {
index++; continue;
}
index++;
// see command printing below for explanation
const size_t offset = row * LINELEN + col * COLW;
int grpln = (int)strlen(grp->group);
snprintf(buffers + offset, COLW+1, "%s/", grp->group);
buffers[offset + grpln + 1] = ' '; // replace the terminator with space
row++;
if (row >= rows) { row = 0; col++; }
}
}
if (show_commands) {
STAILQ_FOREACH(it, &s_cmd_list, next) {
if (skipmask[index >> 5] & (1 << (index & 31))) {
index++;
continue;
}
index++;
// this always replaces the earlier '\0' in the line and adds a new one.
// lines are 1 char longer than needed to accomodate the last column's '\0' without
// overwriting the next line's first char.
const size_t offset = row * LINELEN + col * COLW;
bool alias = (it->alias_of != NULL);
snprintf(buffers + offset, COLW + 1, "%s%-*s", (alias ? "*" : ""), COLW - alias, it->sig.command);
buffers[offset + COLW] = ' '; // replace the terminator with space
row++;
if (row >= rows) {
row = 0;
col++;
}
}
}
for (int i = 0; i < rows; i++) {
console_write(buffers + i * LINELEN, LINELEN);
console_write("\n", 1);
}
console_free(buffers);
console_write("\n", 1);
EXPENDABLE_CODE({
if (any_groups_shown) {
console_print("\"/\" marks a group. Use \"ls GROUP\" to see its members.\n");
}
if (any_aliases_shown) {
console_print("\"*\" marks an alias.\n");
}
})
console_print(LS_USE_HELP_TO_LEARN_MORE);
return CONSOLE_OK;
}
/**
* Command: Exit the console
*/
static int cmd_exit(console_ctx_t *ctx, cmd_signature_t *reg)
{
if (reg) {
assert(NULL == ctx);
assert(NULL != reg);
reg->help = EXPENDABLE_STRING("Quit console.");
reg->no_history = true;
return 0;
}
// These will catch bugs when exiting console while testing
assert(NULL != ctx);
assert(NULL == reg);
assert(NULL != ctx->argv);
assert(NULL != ctx->cmd);
if (!ctx->exit_allowed) {
console_println("Exit not allowed in this session.");
return CONSOLE_ERR_NOT_POSSIBLE;
}
ctx->exit_requested = true;
console_println("Leaving console.");
return 0;
}
/**
* Command: Delay
*/
static int cmd_sleep(console_ctx_t *ctx, cmd_signature_t *reg)
{
(void) ctx; // unused
static struct {
struct arg_int *seconds;
struct arg_int *millis;
struct arg_end *end;
} args;
if (reg) {
args.millis = arg_int0("m", "millis", "<ms>", "milliseconds");
args.seconds = arg_int0(NULL, NULL, "<secs>", "seconds");
args.end = arg_end(2);
reg->argtable = &args;
reg->help = EXPENDABLE_STRING("Stop the console for a given time (max 60s); useful in batched scripts. Both parameters may be combined.");
return 0;
}
uint32_t s = args.seconds->count ? args.seconds->ival[0] : 0;
uint32_t ms = s*1000 + (args.millis->count ? args.millis->ival[0] : 0);
if (ms > 60000) {
console_color_printf(COLOR_RED, "Interval out of range\n");
return CONSOLE_ERR_INVALID_ARG;
}
if (ms > 0) {
vTaskDelay(pdMS_TO_TICKS(ms));
}
return 0;
}
static void register_default_commands()
{
console_cmd_register(cmd_list, "list");
console_cmd_add_alias_fn(cmd_list, "ls");
console_cmd_add_alias_fn(cmd_list, "la");
console_cmd_add_alias_fn(cmd_list, "ll");
console_cmd_register(cmd_help, "help");
console_cmd_register(cmd_exit, "exit");
console_cmd_add_alias_fn(cmd_exit, "quit");
console_cmd_add_alias_fn(cmd_exit, "q");
console_cmd_register(cmd_sleep, "sleep");
}
console_ctx_t *console_ctx_init(
console_ctx_t *ctx,
#if CONSOLE_USE_FILE_IO_STREAMS
FILE* inf, FILE* outf
#else
void * ioctx
#endif
) {
if (ctx == NULL) {
ctx = console_calloc(sizeof(struct console_ctx), 1);
if (!ctx) {
console_internal_error_print("console_ctx_init alloc fail!");
// Alloc failed
return NULL;
}
ctx->__internal_heap_allocated = true;
}
// fill defaults and set magic
console_ctx_defaults(ctx);
#if CONSOLE_USE_FILE_IO_STREAMS
ctx->in = inf;
ctx->out = outf;
#else
ctx->ioctx = ioctx;
#endif
return ctx;
}
void console_ctx_destroy(console_ctx_t *ctx)
{
if (!ctx) {
console_internal_error_print("console_ctx_destroy NULL param!");
return;
}
if (ctx->__internal_magic != CONSOLE_CTX_MAGIC) {
console_internal_error_print("console_ctx_destroy bad magic!");
return;
}
if (ctx->__internal_heap_allocated) {
console_free(ctx);
}
}
void *console_task_posix(void *param)
{
console_task(param);
return NULL;
}
static int my_linenoiseWrite(void *ctx, const char *text, int len) {
return console_write_ctx(ctx, text, len);
}
static int my_linenoiseRead(void *ctx, char *dest, int count) {
return console_read_ctx(ctx, dest, count);
}
void console_task(void *param)
{
if (!console_inited) {
console_internal_error_print("console_task - console not inited!");
return;
}
console_ctx_t *ctx = param;
if (!ctx) {
console_internal_error_print("console_task NULL param!");
return;
}
if (ctx->__internal_magic != CONSOLE_CTX_MAGIC) {
console_internal_error_print("console_task bad magic!");
return;
}
struct linenoiseState ln;
consLnStateInit(&ln);
// multi-line does not work reliably
consLnSetMultiLine(&ln, 0);
// Set pointer to the prompt buffer
consLnSetPrompt(&ln, ctx->prompt);
consLnSetBuf(&ln, ctx->line_buffer, CONSOLE_LINE_BUF_LEN);
ln.allowCtrlDExit = ctx->exit_allowed;
/* Tell linenoise where to get command completions and hints */
consLnSetCompletionCallback(&ln, &console_get_completion);
consLnSetHintsCallback(&ln, (linenoiseHintsCallback *) &console_get_hint);
#if CONSOLE_USE_TERMIOS
if (ctx->in) {
enableRawMode(ctx, fileno(ctx->in));
}
#endif
consLnSetReadWrite(&ln, my_linenoiseRead, my_linenoiseWrite, ctx);
/* Set command history size */
consLnHistorySetMaxLen(&ln, CONSOLE_HISTORY_LEN);
#if CONSOLE_FILE_SUPPORT
if (ctx->history_file) {
consLnHistoryLoad(&ln, ctx->history_file);
}
#endif
/* Main loop */
while (true) {
// Shutdown check
if (ctx->exit_allowed && ctx->exit_requested) {
// exiting, save history
goto exit;
}
/* Get a line using linenoise.
* The line is returned when ENTER is pressed.
*/
if (ctx->loop_handler) {
ctx->loop_handler(ctx);
}
int len = consLnReadLine(&ln);
if (len == 0) { /* Ignore empty lines */
continue;
}
if (len < 0) {
// This happens if ^C is input at stdin
//console_internal_error_print("read < 1");
goto exit;
}
/* Try to run the command */
int ret;
bool add_to_history = true;
const struct cmd_signature *the_cmd = NULL;
int err = console_handle_cmd(ctx, ln.buf, &ret, &the_cmd);
if (err == CONSOLE_ERR_UNKNOWN_CMD) {
if (ctx->use_colors) {
console_print_ctx(ctx, "\x1b[31;1mUnrecognized command:\x1b[22m \"");
} else {
console_print_ctx(ctx, "Unrecognized command: \"");
}
// pseudo-hexdump to make it more obvious when the terminal sends garbage
// (this will miss some codes - like ESC - because linenoise tries to sanitize the line)
for (char *pc = ln.buf; *pc != 0; pc++) {
const char c = *pc;
if (c >= 32 && c < 127) {
console_write_ctx(ctx, &c, 1);
}
else {
console_printf_ctx(ctx, COLOR_RESET, "\\x%02X", c);
}
}
if (ctx->use_colors) {
console_print_ctx(ctx, "\"\x1b[m\n");
} else {
console_print_ctx(ctx, "\"\n");
}
EXPENDABLE_CODE({
console_print_ctx(ctx, "Use `ls` or `help` for a list of commands.\n");
})
}
else if (err == CONSOLE_ERR_BAD_CALL) {
add_to_history = false;
// command was empty
}
else if (err == CONSOLE_OK && ret != 0) {
if (ctx->use_colors) {
console_print_ctx(ctx, "\x1b[31;1mCommand err:\x1b[22m ");
console_err_print_ctx(ctx, ret);
console_print_ctx(ctx, "\nUse `-h` for help.\x1b[m\n");
} else {
console_print_ctx(ctx, "Command err: ");
console_err_print_ctx(ctx, ret);
console_print_ctx(ctx, "\nUse `-h` for help.\n");
}
}
else if (err != CONSOLE_OK) {
if (ctx->use_colors) {
console_print_ctx(ctx, "\x1b[31;1mConsole err:\x1b[22m ");
console_err_print_ctx(ctx, err);
console_print_ctx(ctx, "\x1b[m\n");
} else {
console_print_ctx(ctx, "Console err: ");
console_err_print_ctx(ctx, err);
console_print_ctx(ctx, "\n");
}
}
if (the_cmd && the_cmd->no_history) {
add_to_history = false;
}
if (add_to_history) {
/* Add the command to the history */
consLnHistoryAdd(&ln, ln.buf);
}
}
exit:
#if CONSOLE_FILE_SUPPORT
if (ctx->history_file) {
consLnHistorySave(&ln, ctx->history_file);
}
#endif
consLnHistoryFree(&ln);
// ln lives on stack, do not free!
#if CONSOLE_USE_TERMIOS && CONSOLE_USE_FILE_IO_STREAMS
if (ctx->in) {
// Restore STDIN to normal state
disableRawMode(ctx, fileno(ctx->in));
}
#endif
// Run shutdown handler. Shutdown handler could release user data & destroy the context, for example.
if (ctx->shutdown_handler) {
ctx->shutdown_handler(ctx);
}
}
#if CONSOLE_USE_TERMIOS && CONSOLE_USE_FILE_IO_STREAMS
#include <termios.h>
#include <unistd.h>
/* Raw mode: 1960 magic shit. */
static int enableRawMode(console_ctx_t *ctx, int fd) {
struct termios raw;
if (!isatty(fd)) goto fatal;
if (tcgetattr(fd,&ctx->orig_termios) == -1) goto fatal;
raw = ctx->orig_termios; /* modify the original mode */
/* input modes: no break, no CR to NL, no parity check, no strip char,
* no start/stop output control. */
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
/* output modes - disable post processing */
raw.c_oflag = OPOST | ONLCR;
/* control modes - set 8 bit chars */
raw.c_cflag |= (CS8);
/* local modes - choing off, canonical off, no extended functions,
* no signal chars (^Z,^C) */
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
/* control chars - set return condition: min number of bytes and timer.
* We want read to return every single byte, without timeout. */
raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */
/* put terminal in raw mode after flushing */
if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal;
return 0;
fatal:
errno = ENOTTY;
return -1;
}
static void disableRawMode(console_ctx_t *ctx, int fd) {
tcsetattr(fd, TCSAFLUSH, &ctx->orig_termios);
}
#endif