failed approach
This commit is contained in:
754
gomspace/libutil/src/gosh/command.c
Normal file
754
gomspace/libutil/src/gosh/command.c
Normal file
@ -0,0 +1,754 @@
|
||||
/* Copyright (c) 2013-2017 GomSpace A/S. All rights reserved. */
|
||||
|
||||
#include "command_local.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include <gs/util/vmem.h> // register commands
|
||||
#include <gs/util/log.h> // register commands
|
||||
#include <gs/util/string.h>
|
||||
#include <gs/util/clock.h>
|
||||
#include "../lock.h"
|
||||
|
||||
#define MAX_ARGC 30
|
||||
|
||||
#ifdef __AVR__
|
||||
#include <avr/pgmspace.h>
|
||||
#define cmd_strcmp strcmp_P
|
||||
#define cmd_strncmp strncmp_P
|
||||
#define cmd_strlen strlen_P
|
||||
#define cmd_strcpy strcpy_P
|
||||
#define cmd_read_ptr(ptr) ((void *) pgm_read_word(ptr))
|
||||
#define cmd_read_int(ptr) pgm_read_word(ptr)
|
||||
#else
|
||||
#define cmd_strcmp strcmp
|
||||
#define cmd_strncmp strncmp
|
||||
#define cmd_strlen strlen
|
||||
#define cmd_strcpy strcpy
|
||||
#define cmd_read_ptr(ptr) *ptr
|
||||
#define cmd_read_int(ptr) *ptr
|
||||
#endif
|
||||
|
||||
// define common command log group.
|
||||
static GS_LOG_GROUP(gs_command_log, "command", GS_LOG_CAT_COMMAND, LOG_DEFAULT_MASK | LOG_INFO_MASK);
|
||||
#define LOG_DEFAULT gs_command_log
|
||||
|
||||
/**
|
||||
Compile check that size of gs_command_t is multiplum of 4.
|
||||
*/
|
||||
GS_STATIC_ASSERT((sizeof(gs_command_t) % 4) == 0, gs_command_t_is_not_a_multiplum_of_4);
|
||||
|
||||
// Private context
|
||||
typedef struct process_context {
|
||||
// command context - must be first, as it is used to access private context (same address)
|
||||
gs_command_context_t context;
|
||||
// process function
|
||||
gs_error_t (*process)(const gs_command_t * const cmds, int cmd_count, int arg_offset, struct process_context * pc);
|
||||
// command error
|
||||
gs_error_t error;
|
||||
// only exact match (space after last argument)
|
||||
bool requires_exact_match;
|
||||
// first command match
|
||||
const gs_command_t * cmd;
|
||||
// number of hits when hunting commands, completion etc.
|
||||
unsigned int hits;
|
||||
// complete result
|
||||
struct {
|
||||
char * line;
|
||||
size_t token_start;
|
||||
} complete;
|
||||
} private_context_t;
|
||||
|
||||
// command block
|
||||
typedef struct gs_command_block {
|
||||
//! Pointer to command block.
|
||||
const gs_command_t * commands;
|
||||
//! Number of commands in command block.
|
||||
size_t count;
|
||||
//! Reference to next command block.
|
||||
struct gs_command_block * next;
|
||||
} gs_command_block_t;
|
||||
|
||||
// commands
|
||||
static gs_command_block_t g_commands;
|
||||
|
||||
// minimum stack size in bytes.
|
||||
static size_t g_stack_size;
|
||||
|
||||
// command logger callback
|
||||
static gs_command_log_t g_command_logger = NULL;
|
||||
static void * g_command_logger_ctx = NULL;
|
||||
|
||||
static gs_error_t command_stdio_set_result(gs_command_context_t *ctx, const char *group, const char *key, const char *value)
|
||||
{
|
||||
static const char* printed_group_header = NULL;
|
||||
/* Print Group header if Group string is non-empty */
|
||||
if ((group != NULL) && (group[0] != '\0')) {
|
||||
if (printed_group_header != group) {
|
||||
fprintf(ctx->out, "%s:\r\n", group);
|
||||
printed_group_header = group;
|
||||
}
|
||||
}
|
||||
/* Print "<key>: <value>" if key string is non-empty */
|
||||
if (key != NULL) {
|
||||
if (key[0] != '\0') {
|
||||
if ((group != NULL) && (group[0] != '\0')) {
|
||||
fprintf(ctx->out, " %s: ", key);
|
||||
} else {
|
||||
fprintf(ctx->out, "%s: ", key);
|
||||
}
|
||||
}
|
||||
}
|
||||
fprintf(ctx->out, "%s\r\n", value);
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_stdio_flush(gs_command_context_t *ctx)
|
||||
{
|
||||
fflush(ctx->out);
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_stdio_wait_for_key(gs_command_context_t *ctx, int *ch, int timeout_ms)
|
||||
{
|
||||
return gs_stdio_getchar_timed(timeout_ms, ch);
|
||||
}
|
||||
|
||||
static const gs_command_io_functions_t stdio_functions = {
|
||||
.set_result = command_stdio_set_result,
|
||||
.flush = gs_command_stdio_flush,
|
||||
.wait_for_key = gs_command_stdio_wait_for_key
|
||||
};
|
||||
|
||||
const char * gs_command_args(gs_command_context_t *ctx)
|
||||
{
|
||||
if (ctx->argc > 1) {
|
||||
// find first matching argument (= starts with) - this is not 100% and doesn't handle arguments with spaces (quoted)
|
||||
const char * arg = ctx->command_line;
|
||||
while (arg && arg[0]) {
|
||||
if (strncmp(arg, ctx->argv[1], strlen(ctx->argv[1])) == 0) {
|
||||
return arg;
|
||||
}
|
||||
// skip argument
|
||||
for (; *arg && (*arg != ' '); ++arg);
|
||||
// skip spaces
|
||||
// cppcheck-suppress redundantCondition
|
||||
for (; *arg && (*arg == ' '); ++arg);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool gs_command_build_argv(char *line, int *argc, char **argv, int max_argc)
|
||||
{
|
||||
// Skip spaces
|
||||
for (; line && *line && isspace((unsigned int)*line); ++line);
|
||||
|
||||
*argc = 0;
|
||||
argv[*argc] = line;
|
||||
|
||||
char quote = 0;
|
||||
|
||||
while (*line) {
|
||||
// check for quote's: ' or "
|
||||
if ((*line == '\'') || (*line == '\"')) {
|
||||
if (quote == 0) {
|
||||
quote = *line;
|
||||
argv[*argc]++;
|
||||
} else if (quote == *line) {
|
||||
quote = 0;
|
||||
*line = '\0';
|
||||
}
|
||||
}
|
||||
// check for whitespace and no quotes active
|
||||
else if (isspace((unsigned int)*line) && quote == 0) {
|
||||
/* Delete space */
|
||||
*line++ = '\0';
|
||||
|
||||
// skip spaces
|
||||
for (; *line && isspace((unsigned int)*line); ++line);
|
||||
|
||||
/* If there is more data, we have another argument */
|
||||
if (*line) {
|
||||
(*argc)++;
|
||||
if (*argc >= max_argc) {
|
||||
return false;
|
||||
}
|
||||
argv[*argc] = line;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
line++;
|
||||
}
|
||||
|
||||
(*argc)++;
|
||||
if (*argc >= max_argc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// According to C11 section 5.1.2.2.1, argv[argc] must be NULL
|
||||
argv[*argc] = NULL;
|
||||
|
||||
// Check for invalid number of quotes
|
||||
return (quote == 0) ? true : false;
|
||||
}
|
||||
|
||||
static inline gs_error_t command_logger(const char *cmd_line, gs_error_t ret, gs_error_t cmd_ret, gs_timestamp_t ts, gs_timestamp_t te)
|
||||
{
|
||||
gs_lock_lock();
|
||||
gs_command_log_t logger = g_command_logger;
|
||||
void * log_ctx = g_command_logger_ctx;
|
||||
gs_lock_unlock();
|
||||
|
||||
if (logger) {
|
||||
return logger(cmd_line, ret, cmd_ret, ts, te, log_ctx);
|
||||
}
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
static gs_error_t command_execute(const gs_command_t * const cmds, int cmd_count, int arg_offset, private_context_t * pc)
|
||||
{
|
||||
for (int i = 0; i < cmd_count; i++) {
|
||||
const gs_command_t * cmd = &cmds[i];
|
||||
|
||||
if (cmd_strcmp(pc->context.argv[arg_offset], cmd->name) == 0) {
|
||||
// check for sub-commands
|
||||
const gs_command_t * list = (void*) cmd_read_ptr(&cmd->chain.list);
|
||||
if (list) {
|
||||
++arg_offset;
|
||||
if (arg_offset >= pc->context.argc) {
|
||||
return GS_ERROR_TYPE;
|
||||
}
|
||||
return command_execute(list, cmd_read_int(&cmd->chain.count), arg_offset, pc);
|
||||
}
|
||||
|
||||
gs_command_handler_t handler = (void *) cmd_read_ptr(&cmd->handler);
|
||||
if (handler == NULL) {
|
||||
return GS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
pc->context.argc -= arg_offset;
|
||||
pc->context.argv = &pc->context.argv[arg_offset];
|
||||
pc->context.command = cmd;
|
||||
|
||||
// check arguments - if specified
|
||||
if (cmd->mandatory_args || cmd->optional_args) {
|
||||
const int min_args = (cmd->mandatory_args == GS_COMMAND_NO_ARGS) ? 0 : cmd->mandatory_args;
|
||||
const int args = (pc->context.argc - 1);
|
||||
if (args < min_args) {
|
||||
return GS_ERROR_ARG;
|
||||
}
|
||||
if (args > (min_args + cmd->optional_args)) {
|
||||
return GS_ERROR_ARG;
|
||||
}
|
||||
}
|
||||
|
||||
pc->error = handler(&pc->context);
|
||||
return GS_OK; // command was excecuted
|
||||
}
|
||||
}
|
||||
|
||||
return GS_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
static gs_error_t command_process(private_context_t * pc)
|
||||
{
|
||||
const char * command = gs_string_skip_leading_spaces(pc->context.command_line);
|
||||
|
||||
// Make copy of string, because command parser mangles destroys it
|
||||
const size_t command_len = strlen(command);
|
||||
char command_copy[command_len + 1];
|
||||
strcpy(command_copy, command);
|
||||
|
||||
if (command_len && command[command_len-1] == ' ') {
|
||||
pc->requires_exact_match = true;
|
||||
}
|
||||
|
||||
pc->context.optsp = 1;
|
||||
pc->context.optind = 1;
|
||||
pc->context.optopt = '?';
|
||||
pc->context.command_line = command;
|
||||
|
||||
// split into arguments
|
||||
char *argv[MAX_ARGC + 1];
|
||||
if (gs_command_build_argv(command_copy, &pc->context.argc, argv, MAX_ARGC + 1) == false) {
|
||||
return GS_ERROR_ARG;
|
||||
}
|
||||
pc->context.argv = argv;
|
||||
|
||||
gs_error_t error = GS_ERROR_NOT_FOUND;
|
||||
for (const gs_command_block_t * block = &g_commands; block && (error == GS_ERROR_NOT_FOUND); block = block->next) {
|
||||
if (block->commands) {
|
||||
error = (pc->process)(block->commands, block->count, 0, pc);
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_run(const char * command, gs_error_t * return_command_result)
|
||||
{
|
||||
return gs_command_execute_stdio(command, return_command_result);
|
||||
}
|
||||
|
||||
gs_error_t gs_command_execute_stdio(const char * command, gs_error_t * return_command_result)
|
||||
{
|
||||
return gs_command_execute(command, return_command_result, stdout, &stdio_functions, NULL);
|
||||
}
|
||||
|
||||
gs_error_t gs_command_execute(const char * command, gs_error_t * return_command_result, FILE *out,
|
||||
const gs_command_io_functions_t *iof, void *iof_ctx)
|
||||
{
|
||||
command = gs_string_skip_leading_spaces(command);
|
||||
GS_CHECK_ARG(gs_string_empty(command) == false);
|
||||
|
||||
private_context_t pc = {
|
||||
.process = command_execute,
|
||||
.error = GS_OK,
|
||||
.context = {
|
||||
.command_line = command,
|
||||
.out = out,
|
||||
.io_functions = iof,
|
||||
.io_ctx = iof_ctx,
|
||||
}
|
||||
};
|
||||
gs_timestamp_t tm_start, tm_end;
|
||||
gs_clock_get_time(&tm_start);
|
||||
gs_error_t error = command_process(&pc);
|
||||
gs_clock_get_time(&tm_end);
|
||||
command_logger(pc.context.command_line, error, pc.error, tm_start, tm_end);
|
||||
if ((error == GS_OK) && return_command_result) {
|
||||
*return_command_result = pc.error;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_set_output(gs_command_context_t *ctx, const char* group, const char* key, const char* value)
|
||||
{
|
||||
GS_CHECK_ARG(ctx);
|
||||
|
||||
if (ctx->io_functions && ctx->io_functions->set_result) {
|
||||
return ctx->io_functions->set_result(ctx, group, key, value);
|
||||
}
|
||||
|
||||
/* If no IO-function set - ignore the data and send Success */
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_set_output_printf(gs_command_context_t *ctx, const char* group, const char* key, const char * format, ...)
|
||||
{
|
||||
GS_CHECK_ARG(ctx);
|
||||
|
||||
if (ctx->io_functions && ctx->io_functions->set_result)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
char value[256];
|
||||
int size = vsnprintf(value, sizeof(value), format, args);
|
||||
va_end(args);
|
||||
|
||||
/* Don't allow to set truncated results - Return error in this case */
|
||||
if (size >= (int)sizeof(value)) {
|
||||
return GS_ERROR_ALLOC;
|
||||
}
|
||||
|
||||
return ctx->io_functions->set_result(ctx, group, key, value);
|
||||
}
|
||||
|
||||
/* If no IO-function set - ignore the data and send Success */
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_flush_output(gs_command_context_t *ctx)
|
||||
{
|
||||
GS_CHECK_ARG(ctx);
|
||||
|
||||
if (ctx->io_functions && ctx->io_functions->flush) {
|
||||
return ctx->io_functions->flush(ctx);
|
||||
}
|
||||
|
||||
/* If no IO-function set - ignore the data and send Success */
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
bool gs_command_wait_any_key(gs_command_context_t *ctx, int timeout_ms)
|
||||
{
|
||||
int ch;
|
||||
gs_error_t ret = gs_command_wait_key(ctx, &ch, timeout_ms);
|
||||
|
||||
if (ret == GS_ERROR_TIMEOUT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Ensure that a commands handler will not stall if IO function if not available etc.
|
||||
False will only be returned in case of a positive timeout */
|
||||
return true;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_wait_key(gs_command_context_t *ctx, int* ch, int timeout_ms)
|
||||
{
|
||||
if (ctx && ctx->io_functions && ctx->io_functions->wait_for_key)
|
||||
{
|
||||
return ctx->io_functions->wait_for_key(ctx, ch, timeout_ms);
|
||||
}
|
||||
|
||||
/* If no IO-function set set return GS_ERROR_HANDLE */
|
||||
return GS_ERROR_HANDLE;
|
||||
}
|
||||
|
||||
unsigned int gs_command_completer_add_token(gs_command_context_t * ctx, const char * token, bool exact)
|
||||
{
|
||||
private_context_t * pc = (private_context_t *) ctx;
|
||||
char * line = &pc->complete.line[pc->complete.token_start];
|
||||
|
||||
if (token == NULL) {
|
||||
// mark any pending partial token as exact
|
||||
if ((line[0] == 0) || (pc->hits != 1)) {
|
||||
return pc->hits;
|
||||
}
|
||||
exact = true;
|
||||
}
|
||||
|
||||
if (exact) {
|
||||
if (token) {
|
||||
strcpy(line, token);
|
||||
}
|
||||
strcat(line, " ");
|
||||
pc->complete.token_start = strlen(pc->complete.line);
|
||||
pc->hits = 1;
|
||||
} else {
|
||||
if (pc->hits == 0) {
|
||||
strcpy(line, token);
|
||||
} else {
|
||||
for (; *line && *token && (*line == *token); ++line, ++token);
|
||||
*line = 0;
|
||||
}
|
||||
++pc->hits;
|
||||
}
|
||||
|
||||
return pc->hits;
|
||||
}
|
||||
|
||||
static unsigned int command_complete_add(private_context_t * pc, const gs_command_t * cmd, bool exact)
|
||||
{
|
||||
if (cmd) {
|
||||
pc->cmd = cmd;
|
||||
return gs_command_completer_add_token(&pc->context, cmd->name, exact);
|
||||
} else {
|
||||
return gs_command_completer_add_token(&pc->context, NULL, exact);
|
||||
}
|
||||
}
|
||||
|
||||
static gs_error_t command_complete(const gs_command_t * const cmds, int cmd_count, int arg_offset, private_context_t * pc)
|
||||
{
|
||||
if (arg_offset > 0) {
|
||||
// command we are looking for must be in this block
|
||||
pc->hits = 0;
|
||||
}
|
||||
pc->cmd = NULL;
|
||||
bool exact_match = false;
|
||||
|
||||
for (int i = 0; i < cmd_count; i++) {
|
||||
const gs_command_t * cmd = &cmds[i];
|
||||
|
||||
if (cmd_read_int(&cmd->mode) & GS_COMMAND_FLAG_HIDDEN) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (gs_string_empty(pc->context.argv[arg_offset])) {
|
||||
// exceeding known token(s) - partial match
|
||||
command_complete_add(pc, cmd, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pc->requires_exact_match || ((arg_offset+1) < pc->context.argc)) {
|
||||
// must be an exact match
|
||||
if (cmd_strcmp(pc->context.argv[arg_offset], cmd->name) == 0) {
|
||||
command_complete_add(pc, cmd, true);
|
||||
exact_match = true;
|
||||
break;
|
||||
}
|
||||
} else if (cmd_strncmp(pc->context.argv[arg_offset], cmd->name,
|
||||
strlen(pc->context.argv[arg_offset])) == 0) {
|
||||
// partial match
|
||||
command_complete_add(pc, cmd, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (exact_match || ((arg_offset > 0) && (pc->hits == 1))) {
|
||||
command_complete_add(pc, NULL, true);
|
||||
|
||||
if (strlen(pc->complete.line) > strlen(pc->context.command_line)) {
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
if (pc->cmd->chain.list) {
|
||||
return command_complete(pc->cmd->chain.list, pc->cmd->chain.count, arg_offset+1, pc);
|
||||
}
|
||||
|
||||
// command arguments
|
||||
pc->context.argc -= arg_offset;
|
||||
pc->context.argv = &pc->context.argv[arg_offset];
|
||||
pc->context.command = pc->cmd;
|
||||
|
||||
// add the "already" completed ones
|
||||
int arg_to_complete = 1;
|
||||
for (; arg_to_complete < (pc->context.argc - 1); ++arg_to_complete) {
|
||||
gs_command_completer_add_token(&pc->context, pc->context.argv[arg_to_complete], true);
|
||||
}
|
||||
// add the last - if its completed (space after)
|
||||
if ((arg_to_complete < pc->context.argc) && pc->requires_exact_match) {
|
||||
// cppcheck-suppress unreadVariable - not used on __AVR__ because it doesn't support 'completer'
|
||||
gs_command_completer_add_token(&pc->context, pc->context.argv[arg_to_complete], true);
|
||||
++arg_to_complete;
|
||||
}
|
||||
|
||||
#if (__AVR__ == 0)
|
||||
if (pc->cmd->completer) {
|
||||
pc->hits = 0;
|
||||
(pc->cmd->completer)(&pc->context, arg_to_complete);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
pc->hits = 1; // no completer - assume single hit
|
||||
}
|
||||
|
||||
return GS_OK; // only used for breaking loop
|
||||
}
|
||||
|
||||
return GS_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_complete(char *line, size_t max_line_length, FILE* out)
|
||||
{
|
||||
const size_t line_len = strlen(line);
|
||||
char buffer[max_line_length];
|
||||
buffer[0] = 0;
|
||||
private_context_t pc = {
|
||||
.process = command_complete,
|
||||
.context = {
|
||||
.command_line = line,
|
||||
.out = out,
|
||||
},
|
||||
.complete = {
|
||||
.line = buffer,
|
||||
},
|
||||
};
|
||||
command_process(&pc);
|
||||
gs_command_completer_add_token(&pc.context, NULL, true);
|
||||
if (strlen(buffer) > line_len ) {
|
||||
strcpy(line, buffer);
|
||||
}
|
||||
switch (pc.hits) {
|
||||
case 0:
|
||||
return GS_ERROR_NOT_FOUND;
|
||||
case 1:
|
||||
return GS_OK;
|
||||
default:
|
||||
return GS_ERROR_AMBIGUOUS;
|
||||
}
|
||||
}
|
||||
|
||||
static void command_help_print(const gs_command_t * const cmd, private_context_t * pc)
|
||||
{
|
||||
if (pc->hits == 1) {
|
||||
if (cmd->help) {
|
||||
fprintf(pc->context.out, "%s\r\n", cmd->help);
|
||||
}
|
||||
if (cmd->chain.count == 0) {
|
||||
fprintf(pc->context.out, "usage: %s %s\r\n", cmd->name, cmd->usage ? cmd->usage : "");
|
||||
} else {
|
||||
for (unsigned int i = 0; i < cmd->chain.count; ++i) {
|
||||
const gs_command_t * scmd = &cmd->chain.list[i];
|
||||
|
||||
if (scmd->mode & GS_COMMAND_FLAG_HIDDEN) {
|
||||
continue;
|
||||
}
|
||||
fprintf(pc->context.out, " %-19s %s\r\n", scmd->name, scmd->help ? scmd->help : "");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fprintf(pc->context.out, " %-19s %s\r\n", cmd->name, cmd->help ? cmd->help : "");
|
||||
}
|
||||
}
|
||||
|
||||
static void command_help_hit(const gs_command_t * const cmd, private_context_t * pc)
|
||||
{
|
||||
pc->error = GS_OK;
|
||||
++pc->hits;
|
||||
if (pc->hits == 1) {
|
||||
// single hit so far - hold off printing until we know if we get more
|
||||
pc->cmd = cmd;
|
||||
} else {
|
||||
if (pc->cmd) {
|
||||
command_help_print(pc->cmd, pc);
|
||||
pc->cmd = NULL;
|
||||
}
|
||||
command_help_print(cmd, pc);
|
||||
}
|
||||
}
|
||||
|
||||
static gs_error_t command_help(const gs_command_t * const cmds, int cmd_count, int arg_offset, private_context_t * pc)
|
||||
{
|
||||
for (int i = 0; i < cmd_count; i++) {
|
||||
const gs_command_t * cmd = &cmds[i];
|
||||
|
||||
if (cmd_read_int(&cmd->mode) & GS_COMMAND_FLAG_HIDDEN) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pc->requires_exact_match || ((arg_offset+1) < pc->context.argc)) {
|
||||
// must be an exact match
|
||||
if (cmd_strcmp(pc->context.argv[arg_offset], cmd->name) == 0) {
|
||||
const gs_command_t * list = (void*) cmd_read_ptr(&cmd->chain.list);
|
||||
if (list && ((arg_offset+1) < pc->context.argc)) {
|
||||
return command_help(list, cmd_read_int(&cmd->chain.count), arg_offset+1, pc);
|
||||
}
|
||||
command_help_hit(cmd, pc);
|
||||
}
|
||||
|
||||
} else if (cmd_strncmp(pc->context.argv[arg_offset], cmd->name,
|
||||
strlen(pc->context.argv[arg_offset])) == 0) {
|
||||
command_help_hit(cmd, pc);
|
||||
}
|
||||
}
|
||||
|
||||
return GS_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_show_help(const char * command, FILE* out)
|
||||
{
|
||||
private_context_t pc = {
|
||||
.process = command_help,
|
||||
.error = GS_ERROR_NOT_FOUND,
|
||||
.context = {
|
||||
.command_line = command,
|
||||
.out = out,
|
||||
}
|
||||
};
|
||||
gs_error_t error = command_process(&pc);
|
||||
if (pc.cmd) {
|
||||
command_help_print(pc.cmd, &pc);
|
||||
error = GS_OK;
|
||||
} else if ((error == GS_ERROR_NOT_FOUND) && pc.hits) {
|
||||
error = GS_OK;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_register(const gs_command_t * commands, size_t count)
|
||||
{
|
||||
GS_CHECK_ARG(commands != NULL);
|
||||
GS_CHECK_ARG(count > 0);
|
||||
|
||||
gs_error_t error = GS_OK;
|
||||
|
||||
gs_lock_lock();
|
||||
{
|
||||
// check if command block already installed
|
||||
gs_command_block_t * last_block = NULL;
|
||||
for (gs_command_block_t * block = &g_commands; block; block = block->next) {
|
||||
if (block->commands) {
|
||||
const gs_command_t * cmd = block->commands;
|
||||
// loop through because it may be in the linked blocks
|
||||
for (size_t i = 0; i < block->count; ++i, ++cmd) {
|
||||
if (cmd == commands) {
|
||||
error = GS_ERROR_EXIST;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
last_block = block;
|
||||
}
|
||||
|
||||
if (error == GS_OK) {
|
||||
gs_command_block_t * block = calloc(1, sizeof(*block));
|
||||
if (block) {
|
||||
// Insert command last, so lock isn't needed when accessing commands
|
||||
block->commands = commands;
|
||||
block->count = count;
|
||||
block->next = NULL;
|
||||
last_block->next = block;
|
||||
} else {
|
||||
error = GS_ERROR_ALLOC;
|
||||
}
|
||||
}
|
||||
}
|
||||
gs_lock_unlock();
|
||||
|
||||
return (error != GS_ERROR_EXIST) ? error : GS_OK;
|
||||
}
|
||||
|
||||
size_t gs_command_get_stack_size(void)
|
||||
{
|
||||
return g_stack_size;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_init_no_commands(size_t stack_size)
|
||||
{
|
||||
g_stack_size = stack_size;
|
||||
|
||||
gs_error_t error = gs_lock_init();
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
gs_log_group_register(gs_command_log);
|
||||
|
||||
#if (__linux__ == 0)
|
||||
// look up static linked commands - only embedded (= none linux) systems
|
||||
gs_command_block_t * block = &g_commands;
|
||||
extern volatile unsigned int __command_start __attribute__ ((__weak__));
|
||||
extern volatile unsigned int __command_end __attribute__ ((__weak__));
|
||||
if (&__command_start) {
|
||||
block->count = ((ptrdiff_t)&__command_end - (ptrdiff_t)&__command_start) / sizeof(gs_command_t);
|
||||
block->commands = (gs_command_t *) &__command_start;
|
||||
}
|
||||
#endif
|
||||
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_init(size_t stack_size)
|
||||
{
|
||||
gs_error_t error = gs_command_init_no_commands(stack_size);
|
||||
if (error == GS_OK) {
|
||||
// register default commands
|
||||
gs_command_register_default_commands();
|
||||
gs_log_register_commands();
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_logger_default(const char* cmd_line, gs_error_t ret, gs_error_t cmd_ret, gs_timestamp_t t_start, gs_timestamp_t t_end, void *log_ctx)
|
||||
{
|
||||
(void)log_ctx;
|
||||
|
||||
timestamp_diff(&t_end, &t_start);
|
||||
if (ret == GS_OK) {
|
||||
log_info_group(gs_command_log, "'%s' returned '%s' ["
|
||||
"t: <%04"PRIu32".%06"PRIu32">, dt: <%01"PRIu32".%06"PRIu32">]",
|
||||
cmd_line, gs_error_string(cmd_ret),
|
||||
t_start.tv_sec, t_start.tv_nsec/1000, t_end.tv_sec, t_end.tv_nsec/1000);
|
||||
} else {
|
||||
log_info_group(gs_command_log, "'%s' could not be run, returned '%s' ["
|
||||
"t: <%04"PRIu32".%06"PRIu32">]",
|
||||
cmd_line, gs_error_string(ret),
|
||||
t_start.tv_sec, t_start.tv_nsec/1000);
|
||||
}
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
gs_error_t gs_command_register_logger(gs_command_log_t log_cb, void *log_ctx)
|
||||
{
|
||||
gs_lock_lock();
|
||||
g_command_logger = log_cb;
|
||||
g_command_logger_ctx = log_ctx;
|
||||
gs_lock_unlock();
|
||||
|
||||
return GS_OK;
|
||||
}
|
||||
|
35
gomspace/libutil/src/gosh/command_local.h
Normal file
35
gomspace/libutil/src/gosh/command_local.h
Normal file
@ -0,0 +1,35 @@
|
||||
/* Copyright (c) 2013-2018 GomSpace A/S. All rights reserved. */
|
||||
|
||||
#include <gs/util/gosh/command.h>
|
||||
|
||||
/**
|
||||
Command I/O function - flush stdout.
|
||||
*/
|
||||
gs_error_t gs_command_stdio_flush(gs_command_context_t *ctx);
|
||||
|
||||
/**
|
||||
Command I/O function - wait for a key.
|
||||
*/
|
||||
gs_error_t gs_command_stdio_wait_for_key(gs_command_context_t *ctx, int *ch, int timeout_ms);
|
||||
|
||||
/**
|
||||
Complete command.
|
||||
@param[in] line command line to complete
|
||||
@param[in] max \a length (size)
|
||||
@param[in] out output stream, e.g. stdout
|
||||
*/
|
||||
gs_error_t gs_command_complete(char *line, size_t max_line_length, FILE* out);
|
||||
|
||||
/**
|
||||
Show help.
|
||||
@param line command line to show help for.
|
||||
@param out output stream, e.g. stdout
|
||||
*/
|
||||
gs_error_t gs_command_show_help(const char * command, FILE * out);
|
||||
|
||||
/**
|
||||
Change console mode.
|
||||
@param[in] mode console mode, 'cci'
|
||||
@return_gs_error_t
|
||||
*/
|
||||
int gs_console_change_mode(const char * mode);
|
758
gomspace/libutil/src/gosh/console.c
Normal file
758
gomspace/libutil/src/gosh/console.c
Normal file
@ -0,0 +1,758 @@
|
||||
/* Copyright (c) 2013-2017 GomSpace A/S. All rights reserved. */
|
||||
/**
|
||||
@file
|
||||
|
||||
The console interface provides support for executing commands over stdout (typically a serial port).
|
||||
|
||||
The connection can run in 2 modes:
|
||||
- normal, standard GOSH interface (Human Machine Interface), echo characters, prompt, etc.
|
||||
- cci, Computer Computer Interface. Simple text interface, but with tagged output format - easier to parse by a computer.
|
||||
*/
|
||||
#include "console_local.h"
|
||||
#include "command_local.h"
|
||||
|
||||
#include <gs/util/string.h>
|
||||
#include <gs/util/stdio.h>
|
||||
#include <gs/util/thread.h>
|
||||
#include <gs/util/mutex.h>
|
||||
#include <gs/util/time.h>
|
||||
#include <gs/util/log/appender/console.h>
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
#include <conf_util.h> // console defines set through Waf options
|
||||
|
||||
#if (__linux__)
|
||||
#include <gs/util/linux/signal.h>
|
||||
#include <gs/util/linux/command_line.h>
|
||||
#include <unistd.h>
|
||||
#include <termios.h>
|
||||
#endif
|
||||
|
||||
/* Max history length (elements) */
|
||||
#ifndef GS_CONSOLE_HISTORY_LEN
|
||||
#define GS_CONSOLE_HISTORY_LEN 10
|
||||
#endif
|
||||
|
||||
/* Max input length */
|
||||
#ifndef GS_CONSOLE_INPUT_LEN
|
||||
#define GS_CONSOLE_INPUT_LEN 100
|
||||
#endif
|
||||
|
||||
#define CONTROL(X) ((X) - '@')
|
||||
|
||||
typedef enum {
|
||||
CONSOLE_NORMAL = 0,
|
||||
CONSOLE_ESCAPE = 1,
|
||||
CONSOLE_PRE_ESCAPE = 2,
|
||||
} console_escape_t;
|
||||
|
||||
static const char hash_prompt[] = "\033[1;30m # ";
|
||||
|
||||
static const char backspace_char = '\b';
|
||||
static const char space_char = ' ';
|
||||
static const char cr_char = '\r';
|
||||
static const char nl_char = '\n';
|
||||
|
||||
static const char * user_prompt = "gosh";
|
||||
|
||||
static console_escape_t escape = CONSOLE_NORMAL;
|
||||
|
||||
#if (GS_CONSOLE_HISTORY_LEN > 0)
|
||||
static int history_elements;
|
||||
static int history_cur;
|
||||
static int history_browse;
|
||||
static char history[GS_CONSOLE_HISTORY_LEN][GS_CONSOLE_INPUT_LEN+1];
|
||||
#endif
|
||||
|
||||
static int size;
|
||||
static int pos;
|
||||
static char buf[GS_CONSOLE_INPUT_LEN+1];
|
||||
static gs_thread_t console_thread;
|
||||
|
||||
#if (__linux__)
|
||||
static bool termios_changed;
|
||||
static struct termios old_stdin;
|
||||
static struct termios old_stdout;
|
||||
#endif
|
||||
|
||||
static gs_mutex_t g_cci_lock; // Lock for protecting stdout for async output, e.g. log messages
|
||||
static gs_error_t command_io_cci_set_result(gs_command_context_t *ctx, const char *group, const char *key, const char *value);
|
||||
static const gs_command_io_functions_t cci_io_functions = {
|
||||
.set_result = command_io_cci_set_result,
|
||||
.flush = gs_command_stdio_flush,
|
||||
.wait_for_key = gs_command_stdio_wait_for_key,
|
||||
};
|
||||
#define CCI_START_TAG "[X["
|
||||
#define CCI_END_TAG "]X]"
|
||||
|
||||
static void gs_console_write(const char *str, int length)
|
||||
{
|
||||
for (int i = 0; i < length; i++) {
|
||||
putchar(str[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void gs_console_prompt(void)
|
||||
{
|
||||
static const char col_start[] = "\033[1;32m";
|
||||
static const char col_end[] = "\033[0m";
|
||||
|
||||
gs_console_write(col_start, sizeof(col_start) - 1);
|
||||
gs_console_write(user_prompt, strlen(user_prompt));
|
||||
gs_console_write(hash_prompt, sizeof(hash_prompt) - 1);
|
||||
gs_console_write(col_end, sizeof(col_end) - 1);
|
||||
}
|
||||
|
||||
void gs_console_set_prompt(const char * _prompt)
|
||||
{
|
||||
if (gs_string_empty(_prompt) == false) {
|
||||
user_prompt = _prompt;
|
||||
}
|
||||
}
|
||||
|
||||
static void gs_console_reset(void)
|
||||
{
|
||||
pos = size = 0;
|
||||
buf[pos] = 0;
|
||||
gs_console_prompt();
|
||||
}
|
||||
|
||||
static void gs_console_rewind(void)
|
||||
{
|
||||
int plen = strlen(hash_prompt) + strlen(user_prompt);
|
||||
gs_console_write(&cr_char, 1);
|
||||
while (size-- + plen) {
|
||||
gs_console_write(&space_char, 1);
|
||||
}
|
||||
pos = size = 0;
|
||||
gs_console_write(&cr_char, 1);
|
||||
}
|
||||
|
||||
void gs_console_clear(void)
|
||||
{
|
||||
static const char clear[] = "\033[H\033[2J";
|
||||
gs_console_write(clear, sizeof(clear) - 1);
|
||||
gs_console_rewind();
|
||||
gs_console_reset();
|
||||
}
|
||||
|
||||
void gs_console_update(void)
|
||||
{
|
||||
gs_console_rewind();
|
||||
gs_console_prompt();
|
||||
pos = size = strlen(buf);
|
||||
gs_console_write(buf, size);
|
||||
}
|
||||
|
||||
#if (GS_CONSOLE_HISTORY_LEN > 0)
|
||||
|
||||
static void gs_console_history_add(void)
|
||||
{
|
||||
strncpy(history[history_cur], buf, GS_CONSOLE_INPUT_LEN);
|
||||
history[history_cur][GS_CONSOLE_INPUT_LEN] = 0;
|
||||
|
||||
history_browse = 0;
|
||||
history_cur = (history_cur + 1) % GS_CONSOLE_HISTORY_LEN;
|
||||
|
||||
if (history_elements < GS_CONSOLE_HISTORY_LEN) {
|
||||
history_elements++;
|
||||
}
|
||||
}
|
||||
|
||||
static void gs_console_last_line(void)
|
||||
{
|
||||
if (history_elements < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (history_browse >= history_elements) {
|
||||
return;
|
||||
}
|
||||
|
||||
gs_console_rewind();
|
||||
history_browse++;
|
||||
strcpy(buf, history[(history_cur - history_browse + GS_CONSOLE_HISTORY_LEN) % GS_CONSOLE_HISTORY_LEN]);
|
||||
gs_console_update();
|
||||
}
|
||||
|
||||
static void gs_console_next_line(void)
|
||||
{
|
||||
if (history_elements < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (history_browse < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
gs_console_rewind();
|
||||
history_browse--;
|
||||
if (history_browse > 0) {
|
||||
strcpy(buf, history[(history_cur - history_browse + GS_CONSOLE_HISTORY_LEN) % GS_CONSOLE_HISTORY_LEN]);
|
||||
} else {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
gs_console_update();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
static void gs_console_forward_char(void)
|
||||
{
|
||||
if (pos < size) {
|
||||
gs_console_write(&buf[pos], 1);
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
static void gs_console_end_of_line(void)
|
||||
{
|
||||
while (pos < size) {
|
||||
gs_console_forward_char();
|
||||
}
|
||||
}
|
||||
|
||||
static void gs_console_backward_char(void)
|
||||
{
|
||||
if (pos > 0) {
|
||||
pos--;
|
||||
gs_console_write(&backspace_char, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void gs_console_beginning_of_line(void)
|
||||
{
|
||||
while (pos) {
|
||||
gs_console_backward_char();
|
||||
}
|
||||
}
|
||||
|
||||
static void gs_console_newline(void)
|
||||
{
|
||||
gs_console_write(&cr_char, 1);
|
||||
gs_console_write(&nl_char, 1);
|
||||
}
|
||||
|
||||
static bool gs_command_not_empty(const char *ibuf)
|
||||
{
|
||||
while (*ibuf) {
|
||||
if (!isblank((int) *ibuf++)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void show_help(const char * command)
|
||||
{
|
||||
gs_error_t error = gs_command_show_help(command, stdout);
|
||||
if (error) {
|
||||
printf("Could not show help for \'%s\': %s (%d)\r\n", command, gs_error_string(error), error);
|
||||
}
|
||||
}
|
||||
|
||||
static void gs_console_execute(void)
|
||||
{
|
||||
gs_console_newline();
|
||||
buf[GS_CONSOLE_INPUT_LEN] = 0; // ensure 0 termination
|
||||
if (size > 0 && gs_command_not_empty(buf)) {
|
||||
#if (GS_CONSOLE_HISTORY_LEN > 0)
|
||||
gs_console_history_add();
|
||||
#endif
|
||||
gs_error_t result = GS_OK;
|
||||
gs_error_t error = gs_command_execute_stdio(buf, &result);
|
||||
if (error == GS_ERROR_TYPE) {
|
||||
show_help(buf);
|
||||
} else if (error == GS_ERROR_NOT_FOUND) {
|
||||
printf("Unknown command \'%s\'\r\n", buf);
|
||||
} else if (error == GS_ERROR_ARG) {
|
||||
show_help(buf);
|
||||
} else if (error) {
|
||||
printf("Command \'%s\' did not execute: %s (%d)\r\n", buf, gs_error_string(error), error);
|
||||
} else if (result == GS_ERROR_ARG) {
|
||||
show_help(buf);
|
||||
} else if (result) {
|
||||
printf("Command \'%s\' executed, but returned error: %s (%d)\r\n", buf, gs_error_string(result), result);
|
||||
}
|
||||
}
|
||||
gs_console_reset();
|
||||
}
|
||||
|
||||
static void gs_console_complete(void)
|
||||
{
|
||||
/* We don't expand in the middle of a line */
|
||||
if (size != pos) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t old_buf_len = strlen(buf);
|
||||
gs_error_t ret = gs_command_complete(buf, sizeof(buf), stdout);
|
||||
if ((ret == GS_OK) && (old_buf_len == strlen(buf))) {
|
||||
// completed (again) and no change - show help
|
||||
ret = GS_ERROR_AMBIGUOUS;
|
||||
}
|
||||
switch (ret) {
|
||||
case GS_ERROR_AMBIGUOUS:
|
||||
gs_console_newline();
|
||||
show_help(buf);
|
||||
gs_console_update();
|
||||
break;
|
||||
case GS_OK:
|
||||
gs_console_update();
|
||||
break;
|
||||
default:
|
||||
case GS_ERROR_NOT_FOUND:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void gs_console_insert(char c)
|
||||
{
|
||||
int i;
|
||||
int diff = size - pos;
|
||||
|
||||
if (size >= GS_CONSOLE_INPUT_LEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
memmove(&buf[pos + 1], &buf[pos], diff);
|
||||
buf[pos] = c;
|
||||
|
||||
gs_console_write(&buf[pos], diff + 1);
|
||||
for (i = 0; i < diff; i++) {
|
||||
gs_console_write(&backspace_char, 1);
|
||||
}
|
||||
|
||||
size++;
|
||||
pos++;
|
||||
buf[size] = '\0';
|
||||
}
|
||||
|
||||
static void gs_console_insert_overwrite(char c)
|
||||
{
|
||||
buf[pos++] = c;
|
||||
|
||||
if (pos > size) {
|
||||
size++;
|
||||
}
|
||||
|
||||
gs_console_write(&c, 1);
|
||||
}
|
||||
|
||||
static void gs_console_delete(void)
|
||||
{
|
||||
int i;
|
||||
int diff = size - pos;
|
||||
|
||||
/* Nothing to delete */
|
||||
if (size == pos) {
|
||||
return;
|
||||
}
|
||||
|
||||
size--;
|
||||
memmove(&buf[pos], &buf[pos + 1], diff - 1);
|
||||
buf[size] = '\0';
|
||||
|
||||
gs_console_write(&buf[pos], diff - 1);
|
||||
gs_console_write(&space_char, 1);
|
||||
for (i = 0; i < diff; i++) {
|
||||
gs_console_write(&backspace_char, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void gs_console_backspace(void)
|
||||
{
|
||||
if (pos < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
gs_console_backward_char();
|
||||
gs_console_delete();
|
||||
}
|
||||
|
||||
static void gs_console_kill_line(void)
|
||||
{
|
||||
int i;
|
||||
int diff;
|
||||
|
||||
diff = size - pos;
|
||||
|
||||
if (diff == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < diff; i++) {
|
||||
gs_console_write(&space_char, 1);
|
||||
}
|
||||
for (i = 0; i < diff; i++) {
|
||||
gs_console_write(&backspace_char, 1);
|
||||
}
|
||||
|
||||
memset(&buf[pos], 0, diff);
|
||||
size = pos;
|
||||
}
|
||||
|
||||
static void gs_console_kill_line_from_beginning(void)
|
||||
{
|
||||
gs_console_beginning_of_line();
|
||||
gs_console_kill_line();
|
||||
}
|
||||
|
||||
static void gs_console_backward_kill_word(void)
|
||||
{
|
||||
while (pos > 0 && buf[pos - 1] == ' ') {
|
||||
gs_console_backspace();
|
||||
}
|
||||
while (pos > 0 && buf[pos - 1] != ' ') {
|
||||
gs_console_backspace();
|
||||
}
|
||||
}
|
||||
|
||||
static void gs_console_transpose_chars(void)
|
||||
{
|
||||
char c1, c2;
|
||||
|
||||
if (size < 2 || pos < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pos == size) {
|
||||
c1 = buf[pos - 1];
|
||||
c2 = buf[pos - 2];
|
||||
|
||||
gs_console_backward_char();
|
||||
gs_console_backward_char();
|
||||
gs_console_insert_overwrite(c1);
|
||||
gs_console_insert_overwrite(c2);
|
||||
} else {
|
||||
c1 = buf[pos];
|
||||
c2 = buf[pos - 1];
|
||||
|
||||
gs_console_backward_char();
|
||||
gs_console_insert_overwrite(c1);
|
||||
gs_console_insert_overwrite(c2);
|
||||
}
|
||||
}
|
||||
|
||||
static void gs_console_normal(char c)
|
||||
{
|
||||
switch (c) {
|
||||
case CONTROL('A'):
|
||||
gs_console_beginning_of_line();
|
||||
break;
|
||||
case CONTROL('B'):
|
||||
gs_console_backward_char();
|
||||
break;
|
||||
case CONTROL('C'):
|
||||
// Either ignored or handled through signals
|
||||
break;
|
||||
case CONTROL('D'):
|
||||
gs_console_delete();
|
||||
break;
|
||||
case CONTROL('E'):
|
||||
gs_console_end_of_line();
|
||||
break;
|
||||
case CONTROL('F'):
|
||||
gs_console_forward_char();
|
||||
break;
|
||||
case CONTROL('K'):
|
||||
gs_console_kill_line();
|
||||
break;
|
||||
case CONTROL('L'):
|
||||
gs_console_clear();
|
||||
break;
|
||||
#if (GS_CONSOLE_HISTORY_LEN > 0)
|
||||
case CONTROL('N'):
|
||||
gs_console_next_line();
|
||||
break;
|
||||
case CONTROL('P'):
|
||||
gs_console_last_line();
|
||||
break;
|
||||
#endif
|
||||
case CONTROL('T'):
|
||||
gs_console_transpose_chars();
|
||||
break;
|
||||
case CONTROL('U'):
|
||||
gs_console_kill_line_from_beginning();
|
||||
break;
|
||||
case CONTROL('W'):
|
||||
gs_console_backward_kill_word();
|
||||
break;
|
||||
case CONTROL('Z'):
|
||||
// We cannot suspend
|
||||
break;
|
||||
case CONTROL('H'):
|
||||
case 0x7f:
|
||||
gs_console_backspace();
|
||||
break;
|
||||
case '\r':
|
||||
case '\n':
|
||||
gs_console_execute();
|
||||
break;
|
||||
case '\t':
|
||||
gs_console_complete();
|
||||
break;
|
||||
case '\033':
|
||||
escape = CONSOLE_ESCAPE;
|
||||
break;
|
||||
default:
|
||||
if (escape == CONSOLE_ESCAPE) {
|
||||
if ((c == '[') || (c == 'O')) {
|
||||
c = getchar();
|
||||
if (c == 'F')
|
||||
gs_console_end_of_line();
|
||||
if (c == 'H')
|
||||
gs_console_beginning_of_line();
|
||||
#if (GS_CONSOLE_HISTORY_LEN > 0)
|
||||
if (c == 'A')
|
||||
gs_console_last_line();
|
||||
if (c == 'B')
|
||||
gs_console_next_line();
|
||||
#endif
|
||||
if (c == 'C')
|
||||
gs_console_forward_char();
|
||||
if (c == 'D')
|
||||
gs_console_backward_char();
|
||||
if (c == '1')
|
||||
if (getchar() == '~')
|
||||
gs_console_beginning_of_line();
|
||||
if (c == '3')
|
||||
if (getchar() == '~')
|
||||
gs_console_delete();
|
||||
}
|
||||
escape = CONSOLE_NORMAL;
|
||||
break;
|
||||
}
|
||||
|
||||
if (isprint((unsigned char) c)) {
|
||||
gs_console_insert(c);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static gs_error_t command_io_cci_set_result(gs_command_context_t *ctx, const char *group, const char *key, const char *value)
|
||||
{
|
||||
gs_mutex_lock(g_cci_lock);
|
||||
{
|
||||
printf(CCI_START_TAG "cmd_res,%s,%s,%s" CCI_END_TAG, group, key, value);
|
||||
}
|
||||
gs_mutex_unlock(g_cci_lock);
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
static void gs_console_cci_log(gs_log_appender_t *appender, gs_log_level_t level, const gs_log_group_t *group, const gs_timestamp_t * ts, const char * format, va_list va)
|
||||
{
|
||||
va_list my_va;
|
||||
va_copy(my_va, va);
|
||||
|
||||
gs_mutex_lock(g_cci_lock);
|
||||
{
|
||||
printf(CCI_START_TAG "log,%04"PRIu32".%06"PRIu32",%c,%s,", ts->tv_sec, ts->tv_nsec / 1000, gs_log_level_to_char(level), group->name);
|
||||
vprintf(format, my_va);
|
||||
printf(CCI_END_TAG "\r\n");
|
||||
}
|
||||
gs_mutex_unlock(g_cci_lock);
|
||||
|
||||
va_end(my_va);
|
||||
}
|
||||
|
||||
static void gs_console_cci(char c)
|
||||
{
|
||||
switch (c) {
|
||||
case CONTROL('C'):
|
||||
case CONTROL('L'):
|
||||
size = 0;
|
||||
buf[0] = 0;
|
||||
break;
|
||||
case '\r':
|
||||
case '\n':
|
||||
buf[GS_CONSOLE_INPUT_LEN] = 0; // ensure 0 termination
|
||||
if (size > 0 && gs_command_not_empty(buf)) {
|
||||
static unsigned int seq; // simple sequence number keep incrementing
|
||||
|
||||
gs_mutex_lock(g_cci_lock);
|
||||
++seq;
|
||||
printf(CCI_START_TAG "cmd_exec_begin,%u,%s" CCI_END_TAG "\r\n", seq, buf);
|
||||
gs_mutex_unlock(g_cci_lock);
|
||||
|
||||
gs_error_t result = GS_OK;
|
||||
gs_error_t error = gs_command_execute(buf, &result, stdout, &cci_io_functions, NULL);
|
||||
|
||||
gs_mutex_lock(g_cci_lock);
|
||||
printf(CCI_START_TAG "cmd_exec_end,%u,%d,%d" CCI_END_TAG "\r\n", seq, error, result);
|
||||
gs_mutex_unlock(g_cci_lock);
|
||||
}
|
||||
size = 0;
|
||||
buf[0] = 0;
|
||||
break;
|
||||
default:
|
||||
if (isprint((unsigned char) c) && (size < GS_CONSOLE_INPUT_LEN)) {
|
||||
buf[size++] = c;
|
||||
buf[size] = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Currrent mode handler, switch by sending command
|
||||
static void (*console_handler)(char c) = gs_console_normal;
|
||||
|
||||
int gs_console_change_mode(const char * mode)
|
||||
{
|
||||
if (strcasecmp(mode, "cci") == 0) {
|
||||
gs_error_t error = GS_OK;
|
||||
if (console_handler != gs_console_cci) {
|
||||
error = gs_mutex_create(&g_cci_lock);
|
||||
if (error == GS_OK) {
|
||||
gs_log_appender_console_set_cb(gs_console_cci_log);
|
||||
console_handler = gs_console_cci; // change console handler
|
||||
}
|
||||
}
|
||||
return error;
|
||||
}
|
||||
return GS_ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
static void * gs_console_thread(void * param)
|
||||
{
|
||||
gs_console_reset();
|
||||
while (1) {
|
||||
char c = getchar();
|
||||
console_handler(c);
|
||||
}
|
||||
|
||||
gs_thread_exit(NULL);
|
||||
}
|
||||
|
||||
gs_error_t gs_console_exit(void)
|
||||
{
|
||||
#if (__linux__)
|
||||
if (termios_changed) {
|
||||
tcsetattr(STDIN_FILENO, TCSANOW, &old_stdin);
|
||||
tcsetattr(STDOUT_FILENO, TCSANOW, &old_stdout);
|
||||
}
|
||||
#endif
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
#if (__linux__)
|
||||
static inline void exithandler(void)
|
||||
{
|
||||
printf("\n");
|
||||
gs_console_exit();
|
||||
}
|
||||
#endif
|
||||
|
||||
static gs_error_t gs_console_init2(uint32_t flags)
|
||||
{
|
||||
#if (__linux__)
|
||||
// save current stdio setting, for restoring when terminating process
|
||||
tcgetattr(STDIN_FILENO, &old_stdin);
|
||||
tcgetattr(STDOUT_FILENO, &old_stdout);
|
||||
|
||||
// change stdin settings
|
||||
{
|
||||
struct termios new = old_stdin;
|
||||
new.c_iflag &= ~(IGNCR | ICRNL);
|
||||
new.c_lflag &= ~(ECHO | ICANON | IEXTEN);
|
||||
new.c_cc[VTIME]=0;
|
||||
new.c_cc[VMIN]=1;
|
||||
tcsetattr(STDIN_FILENO, TCSANOW, &new);
|
||||
}
|
||||
// change stdout settings
|
||||
{
|
||||
struct termios new = old_stdout;
|
||||
new.c_iflag &= ~(IGNCR | ICRNL);
|
||||
new.c_lflag &= ~(ECHO | ICANON | IEXTEN);
|
||||
new.c_cc[VTIME]=0;
|
||||
new.c_cc[VMIN]=1;
|
||||
tcsetattr(STDOUT_FILENO, TCSANOW, &new);
|
||||
}
|
||||
|
||||
termios_changed = true;
|
||||
|
||||
// add exit-handler to restore original termianl settings
|
||||
atexit(exithandler);
|
||||
|
||||
// install signal handlers to ensure terminal settings are restored
|
||||
if ((flags & GS_CONSOLE_F_NO_SIGNAL_HANDLER) == 0) {
|
||||
// install signal handler(s) to ensure atexit() is called
|
||||
gs_signal_catch(SIGTERM, NULL);
|
||||
|
||||
if (gs_command_line_ignore_ctrlc() == false) {
|
||||
gs_signal_catch(SIGINT, NULL);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if (__AVR__ == 0)
|
||||
/** This is very important on AVR32 */
|
||||
setvbuf(stdin, NULL, _IONBF, 0);
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
setvbuf(stderr, NULL, _IONBF, 0);
|
||||
#endif
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
gs_error_t gs_console_init()
|
||||
{
|
||||
return gs_console_init2(0);
|
||||
}
|
||||
|
||||
static gs_error_t _console_create_thread(gs_thread_priority_t priority, gs_thread_t * handle, uint32_t thread_create_flags)
|
||||
{
|
||||
gs_error_t error = gs_thread_create("CONSOLE",
|
||||
gs_console_thread, NULL,
|
||||
gs_command_get_stack_size(),
|
||||
priority,
|
||||
thread_create_flags,
|
||||
handle);
|
||||
if (error == GS_OK) {
|
||||
// give thread a few moments to print prompt
|
||||
gs_time_sleep_ms(20);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
gs_error_t gs_console_create_thread(gs_thread_t * handle)
|
||||
{
|
||||
return _console_create_thread(GS_THREAD_PRIORITY_LOW, handle, 0);
|
||||
}
|
||||
|
||||
gs_error_t gs_console_create_thread_with_priority(gs_thread_priority_t priority, gs_thread_t * handle)
|
||||
{
|
||||
return _console_create_thread(priority, handle, 0);
|
||||
}
|
||||
|
||||
gs_error_t gs_console_start(const char * prompt, uint32_t flags)
|
||||
{
|
||||
if (console_thread) {
|
||||
return GS_ERROR_EXIST;
|
||||
}
|
||||
|
||||
gs_console_init2(flags);
|
||||
gs_console_set_prompt(prompt);
|
||||
|
||||
return _console_create_thread(GS_THREAD_PRIORITY_LOW, &console_thread, GS_THREAD_CREATE_JOINABLE);
|
||||
}
|
||||
|
||||
gs_error_t gs_console_stop(void)
|
||||
{
|
||||
if (console_thread == 0) {
|
||||
return GS_ERROR_HANDLE;
|
||||
}
|
||||
#if (__linux__)
|
||||
if (pthread_cancel(console_thread) != 0) {
|
||||
return GS_ERROR_IO;
|
||||
}
|
||||
gs_error_t error = gs_thread_join(console_thread, NULL);
|
||||
if (error == GS_OK) {
|
||||
console_thread = 0;
|
||||
}
|
||||
return error;
|
||||
#else
|
||||
return GS_ERROR_NOT_SUPPORTED;
|
||||
#endif
|
||||
}
|
10
gomspace/libutil/src/gosh/console_local.h
Normal file
10
gomspace/libutil/src/gosh/console_local.h
Normal file
@ -0,0 +1,10 @@
|
||||
/* Copyright (c) 2013-2018 GomSpace A/S. All rights reserved. */
|
||||
|
||||
#include <gs/util/gosh/console.h>
|
||||
|
||||
/**
|
||||
Change console mode.
|
||||
@param[in] mode console mode, 'rgosh', 'normal'
|
||||
@return_gs_error_t
|
||||
*/
|
||||
int gs_console_change_mode(const char * mode);
|
277
gomspace/libutil/src/gosh/default_commands.c
Normal file
277
gomspace/libutil/src/gosh/default_commands.c
Normal file
@ -0,0 +1,277 @@
|
||||
/* Copyright (c) 2013-2017 GomSpace A/S. All rights reserved. */
|
||||
|
||||
#include "command_local.h"
|
||||
#include "console_local.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <unistd.h>
|
||||
#include <termios.h>
|
||||
#endif
|
||||
|
||||
#include <gs/util/stdio.h>
|
||||
#include <gs/util/clock.h>
|
||||
#include <gs/util/time.h>
|
||||
#include <gs/util/string.h>
|
||||
#include <conf_util.h>
|
||||
|
||||
static int cmd_help(gs_command_context_t * context)
|
||||
{
|
||||
return gs_command_show_help(gs_command_args(context), context->out);
|
||||
}
|
||||
|
||||
static int cmd_sleep(gs_command_context_t * context)
|
||||
{
|
||||
uint32_t sleep_ms;
|
||||
gs_error_t error = gs_string_to_uint32(context->argv[1], &sleep_ms);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
gs_time_sleep_ms(sleep_ms);
|
||||
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
static int cmd_watch(gs_command_context_t * context, bool check_error)
|
||||
{
|
||||
uint32_t sleep_ms;
|
||||
gs_error_t error = gs_string_to_uint32(context->argv[1], &sleep_ms);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
fprintf(context->out, "Execution delay: %" PRIu32 "\r\n", sleep_ms);
|
||||
|
||||
char * new_command = strstr(gs_command_args(context), " ");
|
||||
if (new_command == NULL) {
|
||||
return GS_ERROR_ARG;
|
||||
} else {
|
||||
new_command = new_command + 1;
|
||||
}
|
||||
|
||||
fprintf(context->out, "Command: %s\r\n", new_command);
|
||||
|
||||
while(1) {
|
||||
|
||||
gs_error_t cmd_result;
|
||||
error = gs_command_execute(new_command, &cmd_result, context->out, context->io_functions, context->io_ctx);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
if (check_error && cmd_result) {
|
||||
return cmd_result;
|
||||
}
|
||||
|
||||
if (gs_stdio_getchar_timed(sleep_ms, NULL) != GS_ERROR_TIMEOUT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
static int cmd_watch_nocheck(gs_command_context_t * context)
|
||||
{
|
||||
return cmd_watch(context, false);
|
||||
}
|
||||
|
||||
static int cmd_watch_check(gs_command_context_t * context)
|
||||
{
|
||||
return cmd_watch(context, true);
|
||||
}
|
||||
|
||||
#define CONTROL(X) ((X) - '@')
|
||||
|
||||
static int cmd_batch(gs_command_context_t * ctx)
|
||||
{
|
||||
char c;
|
||||
int quit = 0, execute = 0;
|
||||
unsigned int batch_size = 100;
|
||||
unsigned int batch_input = 0;
|
||||
unsigned int batch_count = 0;
|
||||
char * batch[20] = {};
|
||||
printf("Type each command followed by enter, hit ctrl+e to end typing, ctrl+x to cancel:\r\n");
|
||||
|
||||
/* Wait for ^q to quit. */
|
||||
while (quit == 0) {
|
||||
|
||||
/* Get character */
|
||||
c = getchar();
|
||||
|
||||
switch (c) {
|
||||
|
||||
/* CTRL + X */
|
||||
case 0x18:
|
||||
quit = 1;
|
||||
break;
|
||||
|
||||
/* CTRL + E */
|
||||
case 0x05:
|
||||
execute = 1;
|
||||
quit = 1;
|
||||
break;
|
||||
|
||||
/* Backspace */
|
||||
case CONTROL('H'):
|
||||
case 0x7f:
|
||||
if (batch_input > 0) {
|
||||
putchar('\b');
|
||||
putchar(' ');
|
||||
putchar('\b');
|
||||
batch_input--;
|
||||
}
|
||||
break;
|
||||
|
||||
case '\r':
|
||||
putchar('\r');
|
||||
putchar('\n');
|
||||
if ((batch[batch_count] != NULL) && (batch_input < batch_size))
|
||||
batch[batch_count][batch_input++] = '\r';
|
||||
if ((batch[batch_count] != NULL) && (batch_input < batch_size))
|
||||
batch[batch_count][batch_input++] = '\0';
|
||||
batch_count++;
|
||||
batch_input = 0;
|
||||
if (batch_count == 20)
|
||||
quit = 1;
|
||||
break;
|
||||
|
||||
default:
|
||||
putchar(c);
|
||||
if (batch[batch_count] == NULL) {
|
||||
batch[batch_count] = calloc(GS_CONSOLE_INPUT_LEN+1, 1);
|
||||
}
|
||||
|
||||
if ((batch[batch_count] != NULL) && (batch_input < batch_size))
|
||||
batch[batch_count][batch_input++] = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (execute) {
|
||||
printf("\r\n");
|
||||
for (unsigned int i = 0; i <= batch_count; i++) {
|
||||
if (batch[i])
|
||||
printf("[%02u] %s\r\n", i, batch[i]);
|
||||
}
|
||||
printf("Press ctrl+e to execute, or any key to abort\r\n");
|
||||
c = getchar();
|
||||
if (c != 0x05)
|
||||
execute = 0;
|
||||
}
|
||||
|
||||
/* Run/Free batch job */
|
||||
for (unsigned int i = 0; i <= batch_count; i++) {
|
||||
if (execute && batch[i]) {
|
||||
printf("EXEC [%02u] %s\r\n", i, batch[i]);
|
||||
gs_command_run(batch[i], NULL);
|
||||
}
|
||||
free(batch[i]);
|
||||
}
|
||||
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
static int cmd_exit(gs_command_context_t * context)
|
||||
{
|
||||
gs_console_exit();
|
||||
exit(EXIT_SUCCESS);
|
||||
return GS_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
static int cmd_clock(gs_command_context_t * ctx)
|
||||
{
|
||||
if (ctx->argc > 1) {
|
||||
gs_timestamp_t ts;
|
||||
gs_error_t error = gs_clock_from_string(ctx->argv[1], &ts);
|
||||
if (error) {
|
||||
return GS_ERROR_ARG;
|
||||
}
|
||||
error = gs_clock_set_time(&ts);
|
||||
if (error) {
|
||||
fprintf(ctx->out, "Failed to set time, error=%s\r\n", gs_error_string(error));
|
||||
return GS_ERROR_DATA;
|
||||
}
|
||||
}
|
||||
|
||||
timestamp_t clock;
|
||||
gs_clock_get_monotonic(&clock);
|
||||
fprintf(ctx->out, "monotonic: %10"PRIu32".%09"PRIu32" sec\r\n", clock.tv_sec, clock.tv_nsec);
|
||||
gs_command_set_output_printf(ctx, "", "monotonic", "%10"PRIu32".%09"PRIu32"", clock.tv_sec, clock.tv_nsec);
|
||||
|
||||
char tbuf[25];
|
||||
gs_clock_get_time(&clock);
|
||||
gs_clock_to_iso8601_string(&clock, tbuf, sizeof(tbuf));
|
||||
fprintf(ctx->out, "realtime: %10"PRIu32".%09"PRIu32" sec -> %s\r\n", clock.tv_sec, clock.tv_nsec, tbuf);
|
||||
gs_command_set_output_printf(ctx, "", "realtime", "%10"PRIu32".%09"PRIu32"", clock.tv_sec, clock.tv_nsec);
|
||||
|
||||
return GS_OK;
|
||||
}
|
||||
|
||||
static int cmd_console_mode(gs_command_context_t * ctx)
|
||||
{
|
||||
return gs_console_change_mode(ctx->argv[1]);
|
||||
}
|
||||
|
||||
static const gs_command_t GS_COMMAND_ROOT cmd_default[] = {
|
||||
{
|
||||
.name = "help",
|
||||
.help = "Show help",
|
||||
.usage = "[command[ subcommand[ arg ...]]]",
|
||||
.handler = cmd_help,
|
||||
.optional_args = 100,
|
||||
},{
|
||||
.name = "sleep",
|
||||
.help = "Sleep X ms",
|
||||
.usage = "<mS>",
|
||||
.handler = cmd_sleep,
|
||||
.mandatory_args = 1,
|
||||
},{
|
||||
.name = "watch",
|
||||
.help = "Run commands at intervals (abort with key)",
|
||||
.usage = "<interval mS> <command> [arg ...]",
|
||||
.handler = cmd_watch_nocheck,
|
||||
.mandatory_args = 2,
|
||||
.optional_args = 100,
|
||||
},{
|
||||
.name = "watch_check",
|
||||
.help = "Like 'watch', but abort if command fails",
|
||||
.usage = "<interval mS> <command [arg ...]>",
|
||||
.handler = cmd_watch_check,
|
||||
.mandatory_args = 2,
|
||||
.optional_args = 100,
|
||||
},{
|
||||
.name = "batch",
|
||||
.help = "Run multiple commands",
|
||||
.handler = cmd_batch,
|
||||
.mode = GS_COMMAND_FLAG_HIDDEN,
|
||||
},{
|
||||
.name = "clock",
|
||||
.help = "Get/set system clock",
|
||||
.usage = "[<sec.nsec> | <YYYY-MM-DDTHH:MM:SSZ>]",
|
||||
.handler = cmd_clock,
|
||||
.optional_args = 1,
|
||||
},{
|
||||
.name = "console_mode",
|
||||
.help = "Console mode(s): cci",
|
||||
.usage = "<mode>",
|
||||
.handler = cmd_console_mode,
|
||||
.mode = GS_COMMAND_FLAG_HIDDEN,
|
||||
.mandatory_args = 1,
|
||||
},
|
||||
#if defined(__linux__)
|
||||
{
|
||||
.name = "exit",
|
||||
.help = "Exit program",
|
||||
.handler = cmd_exit,
|
||||
},
|
||||
#endif
|
||||
};
|
||||
|
||||
gs_error_t gs_command_register_default_commands(void)
|
||||
{
|
||||
return GS_COMMAND_REGISTER(cmd_default);
|
||||
}
|
55
gomspace/libutil/src/gosh/getopt.c
Normal file
55
gomspace/libutil/src/gosh/getopt.c
Normal file
@ -0,0 +1,55 @@
|
||||
/* Copyright (c) 2013-2017 GomSpace A/S. All rights reserved. */
|
||||
|
||||
#include <gs/util/gosh/command.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int gs_command_getopt(gs_command_context_t *ctx, const char *opts)
|
||||
{
|
||||
int c;
|
||||
char *cp;
|
||||
|
||||
if (ctx->optsp == 1) {
|
||||
if (ctx->optind >= ctx->argc ||
|
||||
ctx->argv[ctx->optind][0] != '-' ||
|
||||
ctx->argv[ctx->optind][1] == '\0') {
|
||||
return EOF;
|
||||
} else if (!strcmp(ctx->argv[ctx->optind], "--")) {
|
||||
ctx->optind++;
|
||||
return EOF;
|
||||
}
|
||||
}
|
||||
|
||||
ctx->optopt = c = ctx->argv[ctx->optind][ctx->optsp];
|
||||
if (c == ':' || (cp = strchr(opts, c)) == NULL) {
|
||||
printf("illegal option -- %c\r\n", c);
|
||||
if (ctx->argv[ctx->optind][++ctx->optsp] == '\0') {
|
||||
ctx->optind++;
|
||||
ctx->optsp = 1;
|
||||
}
|
||||
return '?';
|
||||
}
|
||||
|
||||
if (*++cp == ':') {
|
||||
if (ctx->argv[ctx->optind][ctx->optsp+1] != '\0') {
|
||||
ctx->optarg = &ctx->argv[ctx->optind++][ctx->optsp+1];
|
||||
} else if(++ctx->optind >= ctx->argc) {
|
||||
printf("option requires an argument -- %c\r\n", c);
|
||||
ctx->optsp = 1;
|
||||
return '?';
|
||||
} else {
|
||||
ctx->optarg = ctx->argv[ctx->optind++];
|
||||
}
|
||||
ctx->optsp = 1;
|
||||
} else {
|
||||
if (ctx->argv[ctx->optind][++ctx->optsp] == '\0') {
|
||||
ctx->optsp = 1;
|
||||
ctx->optind++;
|
||||
}
|
||||
ctx->optarg = NULL;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
Reference in New Issue
Block a user