diff --git a/include/slash/dflopt.h b/include/slash/dflopt.h index 0150cdf..73b6324 100644 --- a/include/slash/dflopt.h +++ b/include/slash/dflopt.h @@ -1,5 +1,9 @@ #pragma once +/* TODO: determine when to enable this */ +#if 0 +#pragma GCC warning "dflopt.h is deprecated, use #include instead" +#endif #include #include @@ -15,5 +19,3 @@ extern unsigned int rdp_dfl_ack_timeout; extern unsigned int rdp_dfl_ack_count; void rdp_opt_add(optparse_t * parser); -void rdp_opt_set(); -void rdp_opt_reset(); diff --git a/include/slash/optparse.h b/include/slash/optparse.h index 523419d..c09d7f7 100644 --- a/include/slash/optparse.h +++ b/include/slash/optparse.h @@ -13,7 +13,29 @@ typedef struct optparse optparse_t; typedef struct optparse_opt optparse_opt_t; +/** + * @brief Create a new option parser context, remeber to free it using optparse_del() when done + * @param progname name to be associated with the option parser + * @param arg_summary short list of optional command line options and arguments + * @return pointer to new context, NULL if something went wrong + * @warning the progname and arg_summary arguments, if not NULL, must be valid for the entire lifecycle of the option parser! + */ optparse_t *optparse_new(const char *progname, const char *arg_summary); + +/** + * @brief Create a new options parser context, remeber to free it using optparse_del() when done + * @param progname name to be associated with the option parser + * @param arg_summary short list of optional command line options and arguments, can be NULL if not relevant + * @param help longer description of the command, can be NULL if not relevant. + * @return pointer to new context, NULL if something went wrong + * @warning the progname, arg_summary and help arguments, if not NULL, must be valid for the entire lifecycle of the option parser! + */ +optparse_t *optparse_new_ex(const char *progname, const char *arg_summary, const char *help); + +/** + * @brief Release the memory for the given option parser object + * @param parser pointer to valid option parser obtained by calling optparse_new() or optparse_new_ex() + */ void optparse_del(optparse_t *parser); int optparse_parse(optparse_t *parser, int argc, const char *argv[]); @@ -33,4 +55,27 @@ optparse_opt_t *optparse_add_double(optparse_t *parser, int short_opt, const cha optparse_opt_t *optparse_add_string(optparse_t *parser, int short_opt, const char *long_opt, const char *arg_desc, char **ptr, char *help); +/** + * @brief interface to be implemented for custom option parsers + * + * This function will be called by the optparse framework when it processes an + * option for which this custom option value function was registered + * @param data an opaque pointer where the parsed option value must be stored + * @param arg the part of the command line containing the option value as a string to be parsed + */ +typedef int (*optparse_custom_func_t)(void *data, const char * arg); + +/** + * @brief Register an option with a custom parser (see the "func" param) + * @param parser pointer to valid optparse object the option will be attached to + * @param short_opt single character option (for example: 'c' for the corresponding "-c" cmd line option). Set to '\0' if you do not want a short option + * @param long_opt string option (for example "test" for the corresponding "--test" cmd line option). Set to NULL if you do not want a long option + * @param arg_desc string describing the argument (will be shown when help is invoked), may be NULL + * @param help string describing waht this option is/does + * @param func ptr to a cutom option value parser, see the optparse_custom_func_t typedef documentation + * @param data opaque pointer to a valid memory where the "func" above will store the parsed option value + * @return reference to newly created optparse_opt_t object + */ +optparse_opt_t *optparse_add_custom(optparse_t * parser, int short_opt, const char * long_opt, const char * arg_desc, const char * help, optparse_custom_func_t func, void * data); + #endif /* _OPTPARSE_H */ \ No newline at end of file diff --git a/include/slash/slash.h b/include/slash/slash.h index f4c69ac..4a7a2a7 100644 --- a/include/slash/slash.h +++ b/include/slash/slash.h @@ -26,6 +26,7 @@ #include #include +#include #include @@ -60,6 +61,7 @@ .func = _func,\ .completer = _completer,\ .args = _args,\ + .help = _help, \ .next = {NULL}, /* Next pointer in case the user wants to implement custom ordering within or across APMs. It should not required by the default implementation. */\ }; @@ -96,6 +98,7 @@ /* Command prototype */ struct slash; typedef int (*slash_func_t)(struct slash *slash); +typedef int (*slash_func_context_t)(struct slash *slash, void *context); /* Wait function prototype, * this function is implemented by user, so use a void* instead of struct slash* */ @@ -119,14 +122,24 @@ typedef void (*slash_completer_func_t)(struct slash *slash, char * token); struct slash_command { /* Static data */ char *name; - const slash_func_t func; + union { + /* Traditional function used by the `slash_command()` macros. */ + const slash_func_t func; + /* Function with context pointer, for advanced API usage. */ + const slash_func_context_t func_ctx; + }; const char *args; + const char *help; const slash_completer_func_t completer; /* Next pointer in case the user wants to implement custom ordering within or across APMs. It should not required by the default implementation. */ /* single linked list: * The weird definition format comes from sys/queue.h SLINST_ENTRY() macro */ struct { struct slash_command *sle_next; } next; + + /* Optional context pointer (after `next` for ABI compatibility). + Will be supplied to `func_ctx` if specified. */ + void *context; }; /* Slash context */ @@ -140,6 +153,8 @@ struct slash { int fd_read; slash_waitfunc_t waitfunc; bool use_activate; + int signal; + int busy; /* Line editing */ size_t line_size; @@ -161,7 +176,7 @@ struct slash { char *history_head; char *history_tail; char *history_cursor; - + FILE *history_file; /* Command interface */ char **argv; int argc; @@ -176,8 +191,12 @@ struct slash { /* Command list */ struct slash_command * cmd_list; - /* Completions */ - bool in_completion; + /* Allow calling complete functions while already in in one of those functions. + * Use case: when completing "help", you only want to complete the *name* of commands to get help for + * BUT while completing "watch" (or watch-like commands), you want to carry on completing as much as possible, + * for instance, typing: "wgs" would result in the completed command line "watch get serial0" in 6 keystrokes + */ + bool complete_in_completion; }; /** @@ -195,6 +214,8 @@ void slash_destroy(struct slash *slash); char *slash_readline(struct slash *slash); +void slash_sigint(struct slash *slash, int signum); + /** * @brief Implement this function to do something with the current line (logging, etc) * @@ -202,6 +223,44 @@ char *slash_readline(struct slash *slash); */ void slash_on_execute_hook(const char *line); +/** + * @param line the slash line which was just executed + */ +void slash_on_execute_post_hook(const char *line, struct slash_command *command); + +/** + * @brief Will be called before a file is executed by "run ", implement it to set environment variables for example. + * + * @param filename name of the file to be executed. + * @param ctx_for_post Assignable context pointer which will be passed to the corresponding post_hook call. If it is malloc, it should be freed in the post-hook. + */ +void slash_on_run_pre_hook(const char * const filename, void ** ctx_for_post); + +/** + * @brief Will be called after a file has been executed by "run ", implement it to set clear variables for example. + * + * @param filename name of the file which was executed. + * @param ctx customizable context from corresponding pre-hook. + */ +void slash_on_run_post_hook(const char * const filename, void * ctx); + + +typedef char *(*slash_process_cmd_line_hook_t)(const char *line); +/** + * @brief Set this variable to a function if you wish to modify the command line about to be executed + * + * @param line the slash line about to be executed + * @return a malloc'ed pointer to the processed command line, this will be free()'d for you, NULL is a valid + * return value and will cause the original line to be used as-is. + */ +extern slash_process_cmd_line_hook_t slash_process_cmd_line_hook; + +/** + * @brief Set this variable to a function if you wish to execute general completions after specific completions have been executed + * @see slash_completer_func_t + */ +extern slash_completer_func_t slash_global_completer; + int slash_execute(struct slash *slash, char *line); int slash_loop(struct slash *slash); @@ -239,6 +298,13 @@ void slash_command_description(struct slash *slash, struct slash_command *comman int slash_run(struct slash *slash, char * filename, int printcmd); +/** + * @brief Populate Slash history buffer with the content of the given file + * @param slash pointer to valid slash context + * @param history_filename filename to use (will be created if it doesn't exist) to read and write history + */ +void slash_init_history_from_file(struct slash *slash, const char *history_filename); + void slash_history_add(struct slash *slash, char *line); typedef struct slash_list_iterator_s { @@ -251,4 +317,19 @@ int slash_list_add(struct slash_command * item); int slash_list_remove(const struct slash_command * item); int slash_list_init(); +/** + * @brief let slash handle stdout/stdin + * @param slash pointer to valid slash instance + */ +void slash_acquire_std_in_out(struct slash *slash); + +/** + * @brief let slash release handling of stdout/stdin to default. Use this function if you have for instance an APM that needs stdin/stdout control + * @warning remember to call slash_acquire_std_in_out when you are done! + * @param slash pointer to valid slash instance + * @return 0 if success, -ENOTTY in case of failure + */ +void slash_release_std_in_out(struct slash *slash); + + #endif /* _SLASH_H_ */ diff --git a/meson.build b/meson.build index 3b04151..70dda53 100644 --- a/meson.build +++ b/meson.build @@ -10,7 +10,6 @@ slash_sources = files([ if get_option('builtins') slash_sources += files([ 'src/builtins.c', - 'src/dflopt.c', 'src/run.c', ]) endif @@ -40,9 +39,14 @@ slash_lib = library('slash', dependencies : dependencies, install : false, ) - + slash_dep = declare_dependency( include_directories : slash_inc, link_with : slash_lib, ) +slash_link_whole_dep = declare_dependency( + include_directories : slash_inc, + link_with : slash_lib, + link_whole: [slash_lib] +) diff --git a/src/builtins.c b/src/builtins.c index 5b9ef2d..7c7459f 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -2,9 +2,9 @@ #include #include #include +#include #include -#include #include #include @@ -99,7 +99,7 @@ slash_command(confirm, slash_builtin_confirm, "", "Block until user confirmation static int slash_builtin_watch(struct slash *slash) { - unsigned int interval = slash_dfl_timeout; + unsigned int interval = 1000; unsigned int count = 0; optparse_t * parser = optparse_new("watch", ""); @@ -130,19 +130,31 @@ static int slash_builtin_watch(struct slash *slash) char cmd_exec[slash->line_size]; strncpy(cmd_exec, line, slash->line_size); + /* Read time it takes to execute command */ + struct timespec time_before, time_after; + clock_gettime(CLOCK_MONOTONIC, &time_before); + /* Execute command */ slash_execute(slash, cmd_exec); + clock_gettime(CLOCK_MONOTONIC, &time_after); + if ((count > 0) && (count-- == 1)) { break; } + /* Calculate remaining time to ensure execution is running no more often than what interval dictates */ + unsigned int elapsed = (time_after.tv_sec-time_before.tv_sec)*1000 + (time_after.tv_nsec-time_before.tv_nsec)/1000000; + unsigned int remaining = 0; + if (interval > elapsed) remaining = interval - elapsed; + /* Delay (press enter to exit) */ - if (slash_wait_interruptible(slash, interval) != 0) + if (slash_wait_interruptible(slash, remaining) != 0) break; } + optparse_del(parser); return SLASH_SUCCESS; } slash_command_completer(watch, slash_builtin_watch, slash_watch_completer, "", "Repeat a command") \ No newline at end of file diff --git a/src/builtins.h b/src/builtins.h index c7c40ba..881c9ba 100644 --- a/src/builtins.h +++ b/src/builtins.h @@ -3,12 +3,17 @@ #include +/* Configuration */ +#define SLASH_ARG_MAX 32 /* Maximum number of arguments */ +#define SLASH_SHOW_MAX 25 /* Maximum number of commands to list */ + /* Declarations for required implementation functions in slash.c */ void slash_command_usage(struct slash *slash, struct slash_command *command); char *slash_history_increment(struct slash *slash, char *ptr); int slash_putchar(struct slash *slash, char c); struct slash_command * slash_command_find(struct slash *slash, char *line, size_t linelen, char **args); void slash_command_description(struct slash *slash, struct slash_command *command); +int slash_build_args(char *args, char **argv, int *argc); /* Define and initialize section variables */ /* __attribute__((visibility("hidden"))) prevents the section symbols from linking with diff --git a/src/completer.c b/src/completer.c index c422172..0f9b3fb 100644 --- a/src/completer.c +++ b/src/completer.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -6,6 +7,9 @@ #include #include #include +#include + +#include "builtins.h" static void ls_appended(const char* tok, const char* app) { char cmd[PATH_MAX + 3]; @@ -17,7 +21,6 @@ static void ls_appended(const char* tok, const char* app) { } sprintf(cmd, "ls -p %s%s", (tok != NULL ? tok : ""), (app != NULL ? app : "")); - int ret = system(cmd); (void)ret; } @@ -72,37 +75,43 @@ void slash_completer_skip_flagged_prefix(struct slash *slash, char * tgt_prefix) /* if slash buffer begins with tgt_prefix */ if (!strncmp(slash->buffer, tgt_prefix, prefix_len)) { - char * tmp_buf = (char *) malloc(buffer_len+1); - if (tmp_buf != NULL) { - strncpy(tmp_buf, slash->buffer + prefix_len, buffer_len-prefix_len); - } else { - printf("Memory allocation error"); + char * const tmp_buf = calloc(1, buffer_len+1); + + if (tmp_buf == NULL) { + fprintf(stderr, "Memory allocation error\n"); return; - } + } + + strncpy(tmp_buf, slash->buffer + prefix_len, buffer_len-prefix_len); char * token = strtok(tmp_buf, " "); /* start at 1 in case first word following tgt_prefix is not a flag */ int consecutive_ctr = !strcmp(tgt_prefix, "") ? 0 : 1; - - /* if flagged, search for 2nd word not starting with '-' or first word not starting with '--' */ - while (token != NULL) { - if (token[0] != '-') { - consecutive_ctr++; - - if (consecutive_ctr == 2) { - /* move buffer pointer ahead of "tgt_prefix [OPTIONS] ..." */ - slash->buffer = slash->buffer + (prefix_len+(token-tmp_buf)); - slash->length = strlen(slash->buffer); - slash->cursor = slash->length; - break; - } - } else if (token[1] == '-') { - consecutive_ctr++; - } else { - consecutive_ctr = 0; - } - token = strtok(NULL, " "); - } + if(!token) { + slash->buffer = slash->buffer + prefix_len + 1; + slash->length = strlen(slash->buffer); + slash->cursor = slash->length; + } else { + /* if flagged, search for 2nd word not starting with '-' or first word not starting with '--' */ + while (token != NULL) { + if (token[0] != '-') { + consecutive_ctr++; + + if (consecutive_ctr == 2) { + /* move buffer pointer ahead of "tgt_prefix [OPTIONS] ..." */ + slash->buffer = slash->buffer + (prefix_len+(token-tmp_buf)); + slash->length = strlen(slash->buffer); + slash->cursor = slash->length; + break; + } + } else if (token[1] == '-') { + consecutive_ctr++; + } else { + consecutive_ctr = 0; + } + token = strtok(NULL, " "); + } + } free(tmp_buf); } } @@ -117,102 +126,143 @@ void slash_completer_revert_skip(struct slash *slash, char * orig_slash_buf) { int slash_prefix_length(const char *s1, const char *s2) { int len = 0; - - while (*s1 && *s2 && *s1 == *s2) { - len++; - s1++; - s2++; - } - + if (s1 && s2) { + while (*s1 && *s2 && *s1 == *s2) { + len++; + s1++; + s2++; + } + } return len; } +SLIST_HEAD( completion_list, completion_entry ); +struct completion_entry { + struct slash_command * cmd; + SLIST_ENTRY(completion_entry) list; +}; + +slash_completer_func_t slash_global_completer = NULL; + /** * @brief For tab auto completion, calls other completion functions when matched command has them - * + * * @param slash Slash context */ void slash_complete(struct slash *slash) { int matches = 0; - size_t prefixlen = -1; - struct slash_command *prefix = NULL; - struct slash_command * cmd; + struct completion_list completions; + struct completion_entry *completion = NULL; + struct completion_entry *cur_completion; + SLIST_INIT( &completions ); slash_list_iterator i = {}; - while ((cmd = slash_list_iterate(&i)) != NULL) { - - if (strncmp(slash->buffer, cmd->name, slash_min(strlen(cmd->name), slash->length)) == 0) { - - if(strlen(cmd->name) < slash->length && slash->buffer[slash->length - 1] != ' ') { - if (NULL == prefix) { - /* Count matches */ + size_t cmd_len; + int cur_prefix; + int cmd_match; + { + /* Let's take care of multiple consecutive trailing spaces */ + int nof_trailing_spaces = 0; + while (nof_trailing_spaces < slash->length && slash->buffer[slash->length - 1 - nof_trailing_spaces] == ' ') { + nof_trailing_spaces++; + } + if(nof_trailing_spaces > 1) { + slash->length -= (nof_trailing_spaces - 1); + slash->cursor = slash->length; + slash->buffer[slash->length] = '\0'; + } + } + int len_to_compare_to = slash->length>0?slash->buffer[slash->length-1] == ' '?slash->length-1:slash->length:0; + while ((cmd = slash_list_iterate(&i)) != NULL) { + cmd_len = strlen(cmd->name); + cmd_match = strncmp(slash->buffer, cmd->name, slash_min(len_to_compare_to, cmd_len)); + /* Do we have an exact match on the buffer ?*/ + if (cmd_match == 0) { + if((cmd_len < len_to_compare_to && cmd->completer) || (len_to_compare_to <= cmd_len)) { + completion = malloc(sizeof(struct completion_entry)); + if (completion) { matches++; - slash->in_completion = true; - prefix = cmd; - if (matches == 1) - slash_printf(slash, "\n"); + completion->cmd = cmd; + SLIST_INSERT_HEAD(&completions, completion, list); } - continue; } - if(false == slash->in_completion) { - if(strlen(cmd->name) < (slash->length - 1) && slash->buffer[slash->length - 1] == ' ') { - continue; - } + } + } + if(matches > 1) { + /* We only print all commands over 1 match here */ + slash_printf(slash, "\n"); + } + struct completion_entry *prev_completion = NULL; + int prefix_len = INT_MAX; + + SLIST_FOREACH(cur_completion, &completions, list) { + /* Compute the length of prefix common to all completions */ + if (prev_completion != 0) { + cur_prefix = slash_prefix_length(prev_completion->cmd->name, cur_completion->cmd->name); + if(cur_prefix < prefix_len) { + prefix_len = cur_prefix; + completion = cur_completion; } - /* Count matches */ - matches++; - slash->in_completion = true; - /* Find common prefix */ - if (prefixlen == (size_t) -1) { - prefix = cmd; - prefixlen = strlen(prefix->name); - } else { - size_t new_prefixlen = slash_prefix_length(prefix->name, cmd->name); - if (new_prefixlen < prefixlen) - prefixlen = new_prefixlen; - } - - /* Print newline on first match */ - if (matches == 1) - slash_printf(slash, "\n"); - - /* We only print all commands over 1 match here */ - if (matches > 1) - slash_command_description(slash, cmd); - - } - - } - - if (!matches) { - slash->in_completion = false; - slash_bell(slash); - } else if (matches == 1) { - if (prefixlen != -1) { - if (slash->cursor <= prefixlen) { - strncpy(slash->buffer, prefix->name, prefixlen); - slash->buffer[prefixlen] = '\0'; - strcat(slash->buffer, " "); - slash->cursor = slash->length = strlen(slash->buffer); - } else { - if (prefix->completer) { - prefix->completer(slash, slash->buffer + prefixlen + 1); + } + prev_completion = cur_completion; + if(matches > 1) { + slash_command_description(slash, cur_completion->cmd); + } + } + if (matches == 1) { + /* Reassign cmd_len to the current completion as it may have changed during the loop */ + cmd_len = strlen(completion->cmd->name); + if(slash->length < strlen(completion->cmd->name)) { + /* The buffer uniquely completes to a longer command */ + strncpy(slash->buffer, completion->cmd->name, slash->line_size); + slash->buffer[cmd_len] = '\0'; + slash->cursor = slash->length = strlen(slash->buffer); + } + if (completion->cmd->completer) { + /* Call the matching command completer with the rest of the buffer but only if the current + completer allows it */ + if(slash->complete_in_completion == true) { + if(slash->length == cmd_len) { + slash->buffer[slash->length] = ' '; + slash->buffer[slash->length+1] = '\0'; + slash->cursor++; + slash->length++; + } + char *argv[SLASH_ARG_MAX]; + slash->argv = argv; + char args[slash->line_size]; + /* Skip the found command name when building the command line */ + strcpy(args, slash->buffer + cmd_len + 1); + slash_build_args(args, slash->argv, &slash->argc); + completion->cmd->completer(slash, slash->buffer + cmd_len + 1); + if (slash_global_completer) { + slash_global_completer(slash, slash->buffer + cmd_len + 1); } } - } else { - if (prefix->completer) { - prefix->completer(slash, slash->buffer + strlen(prefix->name) + 1); - } - } - } else if (slash->last_char != '\t') { - /* Print the first match as well */ - slash_command_description(slash, prefix); - strncpy(slash->buffer, prefix->name, prefixlen); - slash->buffer[prefixlen] = '\0'; - slash->cursor = slash->length = strlen(slash->buffer); - slash_bell(slash); - } + } + } else if(matches > 1) { + /* Fill the buffer with as much characters as possible: + * if what the user typed in doesn't end with a space, we might + * as well put all the common prefix in the buffer + */ + if(slash->buffer[slash->length-1] != ' ' && len_to_compare_to < prefix_len) { + strncpy(slash->buffer, completion->cmd->name, prefix_len); + slash->buffer[prefix_len] = '\0'; + slash->cursor = slash->length = strlen(slash->buffer); + } + } else { + if (slash_global_completer) { + slash_global_completer(slash, slash->buffer); + } + } + /* Free up the completion list we built up earlier */ + while (!SLIST_EMPTY(&completions)) + { + completion = SLIST_FIRST(&completions); + SLIST_REMOVE(&completions, completion, completion_entry, list); + free(completion); + } } /** @@ -223,8 +273,11 @@ void slash_complete(struct slash *slash) */ void slash_path_completer(struct slash * slash, char * token) { // TODO: Add windows support - char cwd_buf[256]; - char file_name_buf[256]; + char *cwd_buf = calloc(sizeof(char), PATH_MAX); + if(!cwd_buf) { + return; + } + char file_name_buf[FILENAME_MAX]; size_t match_list_size; uint16_t match_count; char ** match_list; @@ -232,6 +285,7 @@ void slash_path_completer(struct slash * slash, char * token) { struct dirent * entry; if (token[0]=='\0') { + printf("\n"); ls_appended(NULL, NULL); return; } @@ -245,64 +299,81 @@ void slash_path_completer(struct slash * slash, char * token) { but it breaks tab-completion of sub commands ¯\_(ツ)_/¯ */ //token = slash->buffer; - char* res = getcwd(cwd_buf, sizeof(cwd_buf)); + char* res = getcwd(cwd_buf, PATH_MAX); if (res != cwd_buf) { printf("Path error\n"); slash_completer_revert_skip(slash, orig_slash_buffer); + free(cwd_buf); return; } // TODO: Add support for absolute paths /* handle home shortcut (~) and subdirectories in token */ - int subdir_idx = last_char_occ(token, '/'); + int subdir_idx; if (token[0] == '~') { - memset(cwd_buf, '\0', 256); strcpy(cwd_buf, getenv("HOME")); - strncpy(&cwd_buf[strlen(cwd_buf)], &token[1], subdir_idx); - } else if (subdir_idx != -1) { - strcat(cwd_buf, "/"); - strncat(cwd_buf, token, subdir_idx); + strcat(cwd_buf, &token[1]); + subdir_idx = last_char_occ(cwd_buf, '/'); + if (subdir_idx != -1) { + cwd_buf[subdir_idx] = '\0'; + } + } else { + subdir_idx = last_char_occ(token, '/'); + strcpy(cwd_buf, token); + if (subdir_idx != -1) { + cwd_buf[subdir_idx] = '\0'; + } } cwd_ptr = opendir(cwd_buf); + if (cwd_ptr == NULL) { + cwd_ptr = opendir("."); + } if (cwd_ptr == NULL) { printf("No such file or directory:\n"); ls_appended(NULL, NULL); slash_completer_revert_skip(slash, orig_slash_buffer); + free(cwd_buf); return; } - strcpy(file_name_buf, token+subdir_idx+1); + strcpy(file_name_buf, cwd_buf+subdir_idx+1); /* allocate memory dynamically as we don't know no. of matches */ match_list_size = 16; match_count = 0; match_list = (char**) malloc(match_list_size * sizeof(char*)); - - while ((entry = readdir(cwd_ptr)) != NULL) { - - /* compare token with filename */ - char* pmatch = path_cmp(file_name_buf, entry->d_name); - - if (pmatch != NULL && strcmp(pmatch, ".")) { - /* allocate more memory if necessary */ - if (match_count+1 >= match_list_size) { - match_list_size += 16; - char** tmp = (char**) reallocarray(match_list, match_list_size, sizeof(char*)); - if (tmp == NULL) { + if(match_list) { + while ((entry = readdir(cwd_ptr)) != NULL) { + + /* compare token with filename */ + char* pmatch = path_cmp(file_name_buf, entry->d_name); + + if (pmatch != NULL && strcmp(pmatch, ".")) { + /* allocate more memory if necessary */ + if (match_count+1 >= match_list_size) { + match_list_size += 16; + char** tmp = (char**) reallocarray(match_list, match_list_size, sizeof(char*)); + if (tmp == NULL) { + printf("Unable to find all matches: No memory\n"); + break; + } + match_list = tmp; + } + char *match_tmp = (char*)malloc(strlen(pmatch) + 3); + if(match_tmp) { + match_list[match_count] = match_tmp; + strcpy(match_list[match_count], pmatch); + if (entry->d_type == DT_DIR) { + strcat(match_list[match_count], "/"); + } + match_count++; + } else { printf("Unable to find all matches: No memory\n"); break; } - match_list = tmp; - } - - match_list[match_count] = (char*)malloc(strlen(pmatch) + 3); - strcpy(match_list[match_count], pmatch); - if (entry->d_type == DT_DIR) { - strcat(match_list[match_count], "/"); } - match_count++; } } @@ -314,8 +385,12 @@ void slash_path_completer(struct slash * slash, char * token) { break; case 1: - strcpy(token+subdir_idx+1, match_list[0]); - slash->length = (token - slash->buffer) + strlen(token); + if(subdir_idx != -1) { + cwd_buf[subdir_idx] = '/'; + } + strcpy(cwd_buf+subdir_idx+1, match_list[0]); + strcpy(token, cwd_buf); + slash->length = strlen(cwd_buf); slash->cursor = slash->length; break; @@ -326,11 +401,12 @@ void slash_path_completer(struct slash * slash, char * token) { token[subdir_idx+prefix_idx+1] = 0; slash->length = (token - slash->buffer) + strlen(token); slash->cursor = slash->length; + printf("\n"); ls_appended(token, "* -d"); break; } } - + free(cwd_buf); slash_completer_revert_skip(slash, orig_slash_buffer); closedir(cwd_ptr); @@ -371,6 +447,10 @@ void slash_watch_completer(struct slash *slash, char * token) { * @param token Slash buffer after first space */ void slash_help_completer(struct slash *slash, char * token) { + bool previous_state = slash->complete_in_completion; + /* Do not complete past the command name */ + slash->complete_in_completion = false; // skip help slash_complete_other_commands(slash, token, "help"); + slash->complete_in_completion = previous_state; } diff --git a/src/dflopt.c b/src/dflopt.c deleted file mode 100644 index 1f1c544..0000000 --- a/src/dflopt.c +++ /dev/null @@ -1,41 +0,0 @@ -#include -#include -#include - -unsigned int slash_dfl_node = 0; -unsigned int slash_dfl_timeout = 1000; - -static int cmd_node(struct slash *slash) { - - if (slash->argc < 1) { - /* Only print node when explicitly asked, - as it should be shown in the prompt. - (it may be truncated when the hostname is too long) */ - printf("Default node = %d\n", slash_dfl_node); - } - - if (slash->argc == 2) { - - /* We rely on user to provide known hosts implementation */ - int known_hosts_get_node(char * find_name); - slash_dfl_node = known_hosts_get_node(slash->argv[1]); - if (slash_dfl_node == 0) - slash_dfl_node = atoi(slash->argv[1]); - } - - return SLASH_SUCCESS; -} -slash_command(node, cmd_node, "[node]", "Set global default node"); - - -static int cmd_timeout(struct slash *slash) { - - if (slash->argc == 2) { - slash_dfl_timeout = atoi(slash->argv[1]); - } - - printf("timeout = %d\n", slash_dfl_timeout); - - return SLASH_SUCCESS; -} -slash_command(timeout, cmd_timeout, "[timeout ms]", "Set global default timeout"); diff --git a/src/optparse.c b/src/optparse.c index aac2bab..faba802 100644 --- a/src/optparse.c +++ b/src/optparse.c @@ -7,6 +7,7 @@ #include +#include #include #include #include @@ -31,7 +32,7 @@ struct optparse { int argi; int argc; const char ** argv; - + const char *help; void * ptr; }; @@ -53,10 +54,15 @@ struct optparse_opt { unsigned num_base; int set_value; } spec; + bool should_free_data; }; optparse_t * optparse_new(const char * progname, const char * arg_summary) { + return optparse_new_ex(progname, arg_summary, NULL); +} + +optparse_t *optparse_new_ex(const char *progname, const char *arg_summary, const char *help) { optparse_t * parser; parser = malloc(sizeof(*parser)); @@ -71,6 +77,8 @@ optparse_new(const char * progname, const char * arg_summary) { if (arg_summary) parser->arg_summary = arg_summary; + parser->help = help; + return parser; } @@ -79,6 +87,9 @@ void optparse_del(optparse_t * parser) { while ((opt = parser->options)) { parser->options = opt->next; + if(opt->should_free_data) { + free(opt->data); + } free(opt); } free(parser); @@ -125,18 +136,22 @@ handle_short_opt(optparse_t * parser, char c, char c2) { for (opt = parser->options; opt; opt = opt->next) { if (opt->short_opt && c == opt->short_opt) { if (opt->arg_desc) { - if (parser->argi >= parser->argc) { - fprintf(stderr, "%s: \"-%c\" requires an argument\n", - parser->progname, c); + if (parser->argi > parser->argc) { + fprintf(stderr, "%s: \"-%c\" requires an argument\n", parser->progname, c); return 0; } if (c2 == '\0') { + if (parser->argi >= parser->argc) { + fprintf(stderr, "%s: \"-%c\" requires an argument\n", parser->progname, c); + return 0; + } return opt->func(opt, parser->argv[parser->argi++]); + } else { + return opt->func(opt, parser->argv[parser->argi - 1] + 2); } - return opt->func(opt, parser->argv[parser->argi - 1] + 2); - - } else + } else { return opt->func(opt, NULL); + } } } @@ -155,7 +170,9 @@ int optparse_parse(optparse_t * parser, int argc, const char * argv[]) { parser->argi = 0; while (parser->argi < parser->argc) { const char * s = argv[parser->argi++]; - + if(!s) { + break; + } if (s[0] == '-' && s[1] == '-') { if (!s[2]) break; @@ -196,6 +213,7 @@ optparse_opt_new(optparse_t * parser, opt->help = help; opt->func = func; opt->data = data; + opt->should_free_data = false; *parser->last_option = opt; parser->last_option = &opt->next; @@ -203,6 +221,45 @@ optparse_opt_new(optparse_t * parser, return opt; } +struct custom_opt_ctx { + optparse_custom_func_t func; + void *org_data; +}; + +static int optparse_custom_func_trampoline(optparse_opt_t * opt, const char * arg) { + struct custom_opt_ctx * custom_ctx = opt->data; + return custom_ctx->func(custom_ctx->org_data, arg); +} + +optparse_opt_t *optparse_add_custom(optparse_t * parser, int short_opt, const char * long_opt, const char * arg_desc, const char * help, optparse_custom_func_t func, void * data) { + optparse_opt_t * opt = NULL; + struct custom_opt_ctx *custom_ctx = calloc(1, sizeof(*custom_ctx)); + if (custom_ctx) { + custom_ctx->func = func; + custom_ctx->org_data = data; + opt = malloc(sizeof(*opt)); + if(opt) { + memset(opt, 0, sizeof(*opt)); + + opt->parser = parser; + opt->short_opt = short_opt; + opt->long_opt = long_opt; + if (opt->long_opt) + opt->long_opt_len = strlen(opt->long_opt); + opt->arg_desc = arg_desc; + opt->help = help; + opt->func = optparse_custom_func_trampoline; + opt->data = custom_ctx; + opt->should_free_data = true; + *parser->last_option = opt; + parser->last_option = &opt->next; + } else { + free(custom_ctx); + } + } + return opt; +} + optparse_opt_t * optparse_arg_optional(optparse_opt_t * opt) { opt->flags |= OPTPARSE_FLAG_OPTIONAL; @@ -218,6 +275,11 @@ void optparse_help(optparse_t * parser, FILE * fp) { fprintf(fp, " %s", parser->arg_summary); fprintf(fp, "\n\n"); + if (parser->help) { + fprintf(fp, "%s", parser->help); + fprintf(fp, "\n\n"); + } + for (opt = parser->options; opt; opt = opt->next) { int col = 0; col += fprintf(fp, " "); diff --git a/src/run.c b/src/run.c index 9b6a9ff..a645bec 100644 --- a/src/run.c +++ b/src/run.c @@ -3,6 +3,16 @@ #include #include #include +#include + + +/* Implement this function to set environment variables for example */ +__attribute__((weak)) void slash_on_run_pre_hook(const char * const filename, void ** ctx_for_post) { /* Set up environemnt variables for "run" command. */ +} + +/* Implement this function to clear environment variables for example */ +__attribute__((weak)) void slash_on_run_post_hook(const char * const filename, void * ctx) { +} int slash_run(struct slash *slash, char * filename, int printcmd) { @@ -22,6 +32,10 @@ int slash_run(struct slash *slash, char * filename, int printcmd) { return SLASH_EIO; } + + void *ctx_for_post = NULL; + slash_on_run_pre_hook(filename_local, &ctx_for_post); + char line[512]; int ret = SLASH_SUCCESS; while(fgets(line, sizeof(line), stream)) { @@ -34,7 +48,7 @@ int slash_run(struct slash *slash, char * filename, int printcmd) { continue; /* Skip comments */ - if (line[0] == '/') { + if (line[0] == '#') { continue; } @@ -50,13 +64,18 @@ int slash_run(struct slash *slash, char * filename, int printcmd) { fclose(stream); + slash_on_run_post_hook(filename_local, ctx_for_post); + return ret; } static int cmd_run(struct slash *slash) { + int verbosity = 2; + optparse_t * parser = optparse_new("run", ""); + optparse_add_int(parser, 'v', "verbosity", "NUM", 0, &verbosity, "verbosity (default = 2, max = 2)"); optparse_add_help(parser); int argi = optparse_parse(parser, slash->argc - 1, (const char **) slash->argv + 1); @@ -72,14 +91,22 @@ static int cmd_run(struct slash *slash) { return SLASH_EINVAL; } - char * name = slash->argv[argi]; + char * const name = slash->argv[argi]; + + if (verbosity >= 1) { + printf("Running %s\n", name); + } - printf("Running %s\n", name); + const bool printcmd = verbosity >= 2; - int res = slash_run(slash, name, 1); + const int res = slash_run(slash, name, printcmd); optparse_del(parser); return res; } -slash_command_completer(run, cmd_run, slash_path_completer, "", "Runs commands in specified file"); - \ No newline at end of file +slash_command_completer(run, cmd_run, slash_path_completer, "", "Runs commands in the specified file. \n"\ + "Sets the following environment variables during execution:\n\n"\ + "- __FILE__ to the path and name of the executed file\n"\ + "- __FILE_DIR__ to the directory containing the executed file, useful for running other files located relative to __FILE__"); +/* TODO: Documenting __FILE__ and __FILE_DIR__ is incorrect. + They are implemented in hooks, but right now we don't have a way to document them there. */ diff --git a/src/slash.c b/src/slash.c index 87930c2..36406a6 100644 --- a/src/slash.c +++ b/src/slash.c @@ -50,10 +50,6 @@ #include "builtins.h" -/* Configuration */ -#define SLASH_ARG_MAX 16 /* Maximum number of arguments */ -#define SLASH_SHOW_MAX 25 /* Maximum number of commands to list */ - /* Terminal codes */ #define ESC '\x1b' #define DEL '\x7f' @@ -123,7 +119,7 @@ static int slash_rawmode_enable(struct slash *slash) raw = slash->original; raw.c_cflag |= (CS8); - raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN); raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; @@ -158,6 +154,21 @@ static int slash_restore_term(struct slash *slash) return 0; } +static int slash_wait_select(void *slashp, unsigned int ms); +void slash_acquire_std_in_out(struct slash *slash) { + slash_configure_term(slash); +#ifdef SLASH_HAVE_SELECT + slash->waitfunc = slash_wait_select; +#endif +} + +void slash_release_std_in_out(struct slash *slash) { + slash_restore_term(slash); +#ifdef SLASH_HAVE_SELECT + slash->waitfunc = NULL; +#endif +} + int slash_write(struct slash *slash, const char *buf, size_t count) { return write(slash->fd_write, buf, count); @@ -229,6 +240,7 @@ int slash_wait_interruptible(struct slash *slash, unsigned int ms) int slash_printf(struct slash *slash, const char *format, ...) { + (void)slash; int ret; va_list args; @@ -259,25 +271,38 @@ static bool slash_line_empty(char *line, size_t linelen) struct slash_command * slash_command_find(struct slash *slash, char *line, size_t linelen, char **args) { + (void)slash; /* Maximum length match */ size_t max_matchlen = 0; struct slash_command *max_match_cmd = NULL; struct slash_command * cmd; slash_list_iterator i = {}; + size_t cmd_length; while ((cmd = slash_list_iterate(&i)) != NULL) { - + cmd_length = strlen(cmd->name); /* Find an exact match */ - if (strncmp(line, cmd->name, strlen(cmd->name)) != 0) + if (strncmp(line, cmd->name, cmd_length) == 0) { + /* Now, let's see if it is an actual exact match, or simply a prefix match + as a prefix match can lead to "listttt" being interpreted as the valid "list" command + */ + if (linelen > cmd_length) { + /* If the input > match, check that the next character is a separator */ + if(line[cmd_length] != ' ') { + continue; + } + } + } else { continue; + } /* Update the max-length match */ - if (strlen(cmd->name) > max_matchlen) { + if (cmd_length > max_matchlen) { max_match_cmd = cmd; - max_matchlen = strlen(cmd->name); + max_matchlen = cmd_length; /* Calculate arguments position */ - *args = line + strlen(cmd->name); + *args = line + cmd_length; } } @@ -347,7 +372,12 @@ void slash_command_usage(struct slash *slash, struct slash_command *command) { const char *args = command->args ? command->args : ""; const char *type = command->func ? "usage" : "group"; - slash_printf(slash, "%s: %s %s\n", type, command->name, args); + const char *help = command->help; + if(NULL != help) { + slash_printf(slash, "%s: %s %s\n\n%s\n\n", type, command->name, args, help); + } else { + slash_printf(slash, "%s: %s %s\n", type, command->name, args); + } } void slash_command_description(struct slash *slash, struct slash_command *command) @@ -358,27 +388,53 @@ void slash_command_description(struct slash *slash, struct slash_command *comman /* Implement this function to perform logging for example */ __attribute__((weak)) void slash_on_execute_hook(const char *line) { + (void)line; +} + +__attribute__((weak)) void slash_on_execute_post_hook(const char *line, struct slash_command *command) { + (void)line; + (void)command; } /* A default no-prompt implementation is provided as a __attribute__((weak)) */ __attribute__((weak)) int slash_prompt(struct slash *slash) { + (void)slash; return 0; } +slash_process_cmd_line_hook_t slash_process_cmd_line_hook = NULL; + int slash_execute(struct slash *slash, char *line) { struct slash_command *command; char *args, *argv[SLASH_ARG_MAX]; + char *processed_cmd_line = NULL, *line_to_use; int ret, argc = 0; + slash->busy = 1; + slash->signal = 0; + /* Skip comments */ if (line[0] == '#') { return SLASH_SUCCESS; } - command = slash_command_find(slash, line, strlen(line), &args); + if(NULL != slash_process_cmd_line_hook) { + processed_cmd_line = slash_process_cmd_line_hook(line); + } + + if (processed_cmd_line != NULL) { + line_to_use = processed_cmd_line; + } else { + line_to_use = line; + } + + command = slash_command_find(slash, line_to_use, strlen(line_to_use), &args); if (!command) { + /* Print the original line here, not the possibly processed one */ slash_printf(slash, "No such command: %s\n", line); + /* Yes, processed_cmd_line maybe NULL, but the man page says it's ok, so we save an "if" statement */ + free(processed_cmd_line); return -ENOENT; } @@ -386,12 +442,16 @@ int slash_execute(struct slash *slash, char *line) slash_on_execute_hook(line); if (!command->func) { + /* Yes, processed_cmd_line maybe NULL, but the free() man page says it's ok, so we save an "if" statement */ + free(processed_cmd_line); return -EINVAL; } /* Build args */ if (slash_build_args(args, argv, &argc) < 0) { slash_printf(slash, "Mismatched quotes\n"); + /* Yes, processed_cmd_line maybe NULL, but the free() man page says it's ok, so we save an "if" statement */ + free(processed_cmd_line); return -EINVAL; } @@ -404,11 +464,25 @@ int slash_execute(struct slash *slash, char *line) slash->argc = argc; slash->argv = argv; - ret = command->func(slash); + if (command->context) { + /* If the user has attached context to the command, + we assume they also specified a function which can accept it. */ + ret = command->func_ctx(slash, command->context); + } else { + /* Otherwise call the traditional (`slash_command()` macro) function without context. */ + ret = command->func(slash); + } if (ret == SLASH_EUSAGE) slash_command_usage(slash, command); + slash_on_execute_post_hook(line, command); + + /* Yes, processed_cmd_line maybe NULL, but the free() man page says it's ok, so we save an "if" statement */ + free(processed_cmd_line); + + slash->busy = 0; + return ret; } @@ -568,8 +642,13 @@ void slash_history_add(struct slash *slash, char *line) return; /* Push including trailing zero */ - if (!slash_line_empty(line, strlen(line))) + if (!slash_line_empty(line, strlen(line))) { + if(slash->history_file) { + fprintf(slash->history_file, "%s\n", line); + fflush(slash->history_file); + } slash_history_push(slash, line, strlen(line) + 1); + } } static void slash_history_next(struct slash *slash) @@ -717,12 +796,6 @@ static void slash_delete(struct slash *slash) } } -void slash_clear_screen(struct slash *slash) -{ - const char *esc = ESCAPE("H") ESCAPE("2J"); - slash_write(slash, esc, strlen(esc)); -} - static void slash_backspace(struct slash *slash) { if (slash->cursor > 0) { @@ -781,6 +854,22 @@ static void slash_swap(struct slash *slash) slash->cursor++; } } + + +void slash_clear_screen(struct slash *slash) { + const char *esc = ESCAPE("H") ESCAPE("2J"); + slash_write(slash, esc, strlen(esc)); +} + +void slash_sigint(struct slash *slash, int signum) { + if (slash->busy) { + slash->signal = signum; + } else { + slash_reset(slash); + slash_refresh(slash, 0); + } +} + #include char *slash_readline(struct slash *slash) { @@ -830,6 +919,18 @@ char *slash_readline(struct slash *slash) slash->cursor = 0; } else if (esc[0] == '[' && esc[1] == 'F') { slash->cursor = slash->length; + } else if (esc[0] == 'O') { + /* If the first escape character is 'O', we're likely in a TMUX session. + The HOME and END keys (unless remapped by the user in their tmux config) send "OH" and "OF" respetively + so we handle those too + */ + if(NULL != getenv("TMUX")) { + if (esc[1] == 'H') { + slash->cursor = 0; + } else if (esc[1] == 'F') { + slash->cursor = slash->length; + } + } } else if (esc[0] == '1' && esc[1] == '~') { slash->cursor = 0; } else if (esc[0] == '4' && esc[1] == '[') { @@ -929,7 +1030,18 @@ char *slash_readline(struct slash *slash) +static void slash_trim(char *line, size_t line_len) { + if (!line_len) { + return; + } + while(--line_len) { + if(!isspace(line[line_len])) { + break; + } + line[line_len] = '\0'; + } +} /* Core */ @@ -947,8 +1059,10 @@ int slash_loop(struct slash *slash) c = slash_getchar(slash); } while (c != '\n' && c != '\r'); } - + size_t line_len = 0; while ((line = slash_readline(slash))) { + line_len = strlen(line); + slash_trim(line, line_len); if (!slash_line_empty(line, strlen(line))) { /* Run command */ ret = slash_execute(slash, line); @@ -986,8 +1100,8 @@ struct slash *slash_create(size_t line_size, size_t history_size) return NULL; } - slash->history_size = history_size; - slash->history = calloc(1, slash->history_size); + slash->history_size = history_size - 1; + slash->history = calloc(1, history_size); if (!slash->history) { free(slash->buffer); free(slash); @@ -999,6 +1113,7 @@ struct slash *slash_create(size_t line_size, size_t history_size) slash->history_tail = slash->history; slash->history_cursor = slash->history; slash->history_avail = slash->history_size - 1; + slash->complete_in_completion = true; slash_list_init(); @@ -1036,7 +1151,7 @@ void slash_create_static(struct slash *slash, char * line_buf, size_t line_size, /* Empty command list */ slash->cmd_list = 0; - slash->in_completion = false; + slash->complete_in_completion = true; tcgetattr(slash->fd_read, &slash->original); } @@ -1056,3 +1171,22 @@ void slash_destroy(struct slash *slash) free(slash); } + +void slash_init_history_from_file(struct slash *slash, const char *history_filename) { + /* Set this to NULL to disable history, will be set if all goes well */ + slash->history_file = NULL; + FILE *history = fopen(history_filename, "a+"); + if (history) { + ssize_t read; + char *line; + size_t len = 0; + while ((read = getline(&line, &len, history)) != -1) { + if(line[read - 1] == '\n') { + line[read - 1] = '\0'; + } + slash_history_add(slash, line); + } + free(line); + slash->history_file = history; + } +}