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
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++) {
|
|
vconsole_write(buffers + i * LINELEN, LINELEN);
|
|
vconsole_write("\n", 1);
|
|
}
|
|
console_free(buffers);
|
|
vconsole_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
|
|
|