diff --git a/Makefile.in b/Makefile.in index c04324b49..73a3c8140 100644 --- a/Makefile.in +++ b/Makefile.in @@ -100,6 +100,7 @@ HAVE_DOXYGEN=@HAVE_DOXYGEN@ # FISH_OBJS := obj/autoload.o obj/builtin.o obj/builtin_bind.o obj/builtin_block.o \ obj/builtin_commandline.o obj/builtin_emit.o obj/builtin_functions.o \ + obj/builtin_history.o \ obj/builtin_complete.o obj/builtin_jobs.o obj/builtin_printf.o \ obj/builtin_set.o obj/builtin_set_color.o obj/builtin_string.o \ obj/builtin_test.o obj/builtin_ulimit.o obj/color.o obj/common.o \ @@ -946,7 +947,7 @@ obj/autoload.o: config.h src/autoload.h src/common.h src/fallback.h obj/autoload.o: src/signal.h src/lru.h src/env.h src/exec.h src/wutil.h obj/builtin.o: config.h src/signal.h src/builtin.h src/common.h obj/builtin.o: src/fallback.h src/builtin_bind.h src/builtin_block.h src/builtin_functions.h -obj/builtin.o: src/builtin_commandline.h src/builtin_complete.h +obj/builtin.o: src/builtin_commandline.h src/builtin_complete.h src/builtin_history.h obj/builtin.o: src/builtin_emit.h src/builtin_jobs.h src/builtin_printf.h obj/builtin.o: src/builtin_set.h src/builtin_set_color.h src/builtin_string.h obj/builtin.o: src/builtin_test.h src/builtin_ulimit.h src/complete.h diff --git a/src/builtin.cpp b/src/builtin.cpp index c008b7a57..9c5ffa01d 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -42,6 +42,7 @@ #include "builtin_complete.h" #include "builtin_emit.h" #include "builtin_functions.h" +#include "builtin_history.h" #include "builtin_jobs.h" #include "builtin_printf.h" #include "builtin_set.h" @@ -58,7 +59,6 @@ #include "fallback.h" // IWYU pragma: keep #include "function.h" #include "highlight.h" -#include "history.h" #include "intern.h" #include "io.h" #include "parse_constants.h" @@ -2314,267 +2314,6 @@ static int builtin_return(parser_t &parser, io_streams_t &streams, wchar_t **arg return status; } -enum hist_cmd_t { HIST_SEARCH = 1, HIST_DELETE, HIST_CLEAR, HIST_MERGE, HIST_SAVE, HIST_UNDEF }; -// Must be sorted by string, not enum or random. -const enum_map hist_enum_map[] = {{HIST_CLEAR, L"clear"}, {HIST_DELETE, L"delete"}, - {HIST_MERGE, L"merge"}, {HIST_SAVE, L"save"}, - {HIST_SEARCH, L"search"}, {HIST_UNDEF, NULL}}; -#define hist_enum_map_len (sizeof hist_enum_map / sizeof *hist_enum_map) - -/// Remember the history subcommand and disallow selecting more than one history subcommand. -static bool set_hist_cmd(wchar_t *const cmd, hist_cmd_t *hist_cmd, hist_cmd_t sub_cmd, - io_streams_t &streams) { - if (*hist_cmd != HIST_UNDEF) { - wchar_t err_text[1024]; - const wchar_t *subcmd_str1 = enum_to_str(*hist_cmd, hist_enum_map); - const wchar_t *subcmd_str2 = enum_to_str(sub_cmd, hist_enum_map); - swprintf(err_text, sizeof(err_text) / sizeof(wchar_t), - _(L"you cannot do both '%ls' and '%ls' in the same invocation"), subcmd_str1, - subcmd_str2); - streams.err.append_format(BUILTIN_ERR_COMBO2, cmd, err_text); - return false; - } - - *hist_cmd = sub_cmd; - return true; -} - -#define CHECK_FOR_UNEXPECTED_HIST_ARGS(hist_cmd) \ - if (history_search_type_defined || show_time_format || null_terminate) { \ - const wchar_t *subcmd_str = enum_to_str(hist_cmd, hist_enum_map); \ - streams.err.append_format(_(L"%ls: you cannot use any options with the %ls command\n"), \ - cmd, subcmd_str); \ - status = STATUS_INVALID_ARGS; \ - break; \ - } \ - if (args.size() != 0) { \ - const wchar_t *subcmd_str = enum_to_str(hist_cmd, hist_enum_map); \ - streams.err.append_format(BUILTIN_ERR_ARG_COUNT2, cmd, subcmd_str, 0, args.size()); \ - status = STATUS_INVALID_ARGS; \ - break; \ - } - -/// Manipulate history of interactive commands executed by the user. -static int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **argv) { - wchar_t *cmd = argv[0]; - int argc = builtin_count_args(argv); - hist_cmd_t hist_cmd = HIST_UNDEF; - history_search_type_t search_type = (history_search_type_t)-1; - long max_items = LONG_MAX; - bool history_search_type_defined = false; - const wchar_t *show_time_format = NULL; - bool case_sensitive = false; - bool null_terminate = false; - - // Use the default history if we have none (which happens if invoked non-interactively, e.g. - // from webconfig.py. - history_t *history = reader_get_history(); - if (!history) history = &history_t::history_with_name(L"fish"); - - /// Note: Do not add new flags that represent subcommands. We're encouraging people to switch to - /// the non-flag subcommand form. While many of these flags are deprecated they must be - /// supported at least until fish 3.0 and possibly longer to avoid breaking everyones - /// config.fish and other scripts. - static const wchar_t *short_options = L":Cmn:epchtz"; - static const struct woption long_options[] = {{L"prefix", no_argument, NULL, 'p'}, - {L"contains", no_argument, NULL, 'c'}, - {L"help", no_argument, NULL, 'h'}, - {L"show-time", optional_argument, NULL, 't'}, - {L"with-time", optional_argument, NULL, 't'}, - {L"exact", no_argument, NULL, 'e'}, - {L"max", required_argument, NULL, 'n'}, - {L"null", no_argument, 0, 'z'}, - {L"case-sensitive", no_argument, 0, 'C'}, - {L"delete", no_argument, NULL, 1}, - {L"search", no_argument, NULL, 2}, - {L"save", no_argument, NULL, 3}, - {L"clear", no_argument, NULL, 4}, - {L"merge", no_argument, NULL, 5}, - {NULL, 0, NULL, 0}}; - - int opt; - wgetopter_t w; - while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -1) { - switch (opt) { - case 1: { - if (!set_hist_cmd(cmd, &hist_cmd, HIST_DELETE, streams)) { - return STATUS_CMD_ERROR; - } - break; - } - case 2: { - if (!set_hist_cmd(cmd, &hist_cmd, HIST_SEARCH, streams)) { - return STATUS_CMD_ERROR; - } - break; - } - case 3: { - if (!set_hist_cmd(cmd, &hist_cmd, HIST_SAVE, streams)) { - return STATUS_CMD_ERROR; - } - break; - } - case 4: { - if (!set_hist_cmd(cmd, &hist_cmd, HIST_CLEAR, streams)) { - return STATUS_CMD_ERROR; - } - break; - } - case 5: { - if (!set_hist_cmd(cmd, &hist_cmd, HIST_MERGE, streams)) { - return STATUS_CMD_ERROR; - } - break; - } - case 'C': { - case_sensitive = true; - break; - } - case 'p': { - search_type = HISTORY_SEARCH_TYPE_PREFIX; - history_search_type_defined = true; - break; - } - case 'c': { - search_type = HISTORY_SEARCH_TYPE_CONTAINS; - history_search_type_defined = true; - break; - } - case 'e': { - search_type = HISTORY_SEARCH_TYPE_EXACT; - history_search_type_defined = true; - break; - } - case 't': { - show_time_format = w.woptarg ? w.woptarg : L"# %c%n"; - break; - } - case 'n': { - max_items = fish_wcstol(w.woptarg); - if (errno) { - streams.err.append_format(_(L"%ls: max value '%ls' is not a valid number\n"), - argv[0], w.woptarg); - return STATUS_INVALID_ARGS; - } - break; - } - case 'z': { - null_terminate = true; - break; - } - case 'h': { - builtin_print_help(parser, streams, cmd, streams.out); - return STATUS_CMD_OK; - } - case ':': { - streams.err.append_format(BUILTIN_ERR_MISSING, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - case '?': { - // Try to parse it as a number; e.g., "-123". - max_items = fish_wcstol(argv[w.woptind - 1] + 1); - if (errno) { - streams.err.append_format(BUILTIN_ERR_UNKNOWN, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - w.nextchar = NULL; - break; - } - default: { - DIE("unexpected retval from wgetopt_long"); - break; - } - } - } - - if (max_items <= 0) { - streams.err.append_format(_(L"%ls: max value '%ls' is not a valid number\n"), argv[0], - w.woptarg); - return STATUS_INVALID_ARGS; - } - - // If a history command hasn't already been specified via a flag check the first word. - // Note that this can be simplified after we eliminate allowing subcommands as flags. - // See the TODO above regarding the `long_options` array. - if (w.woptind < argc) { - hist_cmd_t subcmd = str_to_enum(argv[w.woptind], hist_enum_map, hist_enum_map_len); - if (subcmd != HIST_UNDEF) { - if (!set_hist_cmd(cmd, &hist_cmd, subcmd, streams)) { - return STATUS_INVALID_ARGS; - } - w.woptind++; - } - } - - // Every argument that we haven't consumed already is an argument for a subcommand (e.g., a - // search term). - const wcstring_list_t args(argv + w.woptind, argv + argc); - - // Establish appropriate defaults. - if (hist_cmd == HIST_UNDEF) hist_cmd = HIST_SEARCH; - if (!history_search_type_defined) { - if (hist_cmd == HIST_SEARCH) search_type = HISTORY_SEARCH_TYPE_CONTAINS; - if (hist_cmd == HIST_DELETE) search_type = HISTORY_SEARCH_TYPE_EXACT; - } - - int status = STATUS_CMD_OK; - switch (hist_cmd) { - case HIST_SEARCH: { - if (!history->search(search_type, args, show_time_format, max_items, case_sensitive, - null_terminate, streams)) { - status = STATUS_CMD_ERROR; - } - break; - } - case HIST_DELETE: { - // TODO: Move this code to the history module and support the other search types - // including case-insensitive matches. At this time we expect the non-exact deletions to - // be handled only by the history function's interactive delete feature. - if (search_type != HISTORY_SEARCH_TYPE_EXACT) { - streams.err.append_format(_(L"builtin history delete only supports --exact\n")); - status = STATUS_INVALID_ARGS; - break; - } - if (!case_sensitive) { - streams.err.append_format( - _(L"builtin history delete only supports --case-sensitive\n")); - status = STATUS_INVALID_ARGS; - break; - } - for (wcstring_list_t::const_iterator iter = args.begin(); iter != args.end(); ++iter) { - wcstring delete_string = *iter; - if (delete_string[0] == '"' && delete_string[delete_string.length() - 1] == '"') { - delete_string = delete_string.substr(1, delete_string.length() - 2); - } - history->remove(delete_string); - } - break; - } - case HIST_CLEAR: { - CHECK_FOR_UNEXPECTED_HIST_ARGS(hist_cmd) - history->clear(); - history->save(); - break; - } - case HIST_MERGE: { - CHECK_FOR_UNEXPECTED_HIST_ARGS(hist_cmd) - history->incorporate_external_changes(); - break; - } - case HIST_SAVE: { - CHECK_FOR_UNEXPECTED_HIST_ARGS(hist_cmd) - history->save(); - break; - } - case HIST_UNDEF: { - DIE("Unexpected HIST_UNDEF seen"); - break; - } - } - - return status; -} - int builtin_true(parser_t &parser, io_streams_t &streams, wchar_t **argv) { UNUSED(parser); UNUSED(streams); diff --git a/src/builtin_history.cpp b/src/builtin_history.cpp new file mode 100644 index 000000000..104476561 --- /dev/null +++ b/src/builtin_history.cpp @@ -0,0 +1,297 @@ +// Implementation of the history builtin. +#include "config.h" // IWYU pragma: keep + +#include +#include +#include +#include + +#include +#include + +#include "builtin.h" +#include "builtin_history.h" +#include "common.h" +#include "fallback.h" // IWYU pragma: keep +#include "history.h" +#include "io.h" +#include "reader.h" +#include "wgetopt.h" +#include "wutil.h" // IWYU pragma: keep + +enum hist_cmd_t { HIST_SEARCH = 1, HIST_DELETE, HIST_CLEAR, HIST_MERGE, HIST_SAVE, HIST_UNDEF }; + +// Must be sorted by string, not enum or random. +const enum_map hist_enum_map[] = {{HIST_CLEAR, L"clear"}, {HIST_DELETE, L"delete"}, + {HIST_MERGE, L"merge"}, {HIST_SAVE, L"save"}, + {HIST_SEARCH, L"search"}, {HIST_UNDEF, NULL}}; +#define hist_enum_map_len (sizeof hist_enum_map / sizeof *hist_enum_map) + +struct history_opts { + bool print_help = false; + hist_cmd_t hist_cmd = HIST_UNDEF; + history_search_type_t search_type = (history_search_type_t)-1; + long max_items = LONG_MAX; + bool history_search_type_defined = false; + const wchar_t *show_time_format = NULL; + bool case_sensitive = false; + bool null_terminate = false; +}; + +/// Note: Do not add new flags that represent subcommands. We're encouraging people to switch to +/// the non-flag subcommand form. While many of these flags are deprecated they must be +/// supported at least until fish 3.0 and possibly longer to avoid breaking everyones +/// config.fish and other scripts. +static const wchar_t *short_options = L":Cmn:epchtz"; +static const struct woption long_options[] = {{L"prefix", no_argument, NULL, 'p'}, + {L"contains", no_argument, NULL, 'c'}, + {L"help", no_argument, NULL, 'h'}, + {L"show-time", optional_argument, NULL, 't'}, + {L"with-time", optional_argument, NULL, 't'}, + {L"exact", no_argument, NULL, 'e'}, + {L"max", required_argument, NULL, 'n'}, + {L"null", no_argument, NULL, 'z'}, + {L"case-sensitive", no_argument, NULL, 'C'}, + {L"delete", no_argument, NULL, 1}, + {L"search", no_argument, NULL, 2}, + {L"save", no_argument, NULL, 3}, + {L"clear", no_argument, NULL, 4}, + {L"merge", no_argument, NULL, 5}, + {NULL, 0, NULL, 0}}; + +/// Remember the history subcommand and disallow selecting more than one history subcommand. +static bool set_hist_cmd(wchar_t *const cmd, hist_cmd_t *hist_cmd, hist_cmd_t sub_cmd, + io_streams_t &streams) { + if (*hist_cmd != HIST_UNDEF) { + wchar_t err_text[1024]; + const wchar_t *subcmd_str1 = enum_to_str(*hist_cmd, hist_enum_map); + const wchar_t *subcmd_str2 = enum_to_str(sub_cmd, hist_enum_map); + swprintf(err_text, sizeof(err_text) / sizeof(wchar_t), + _(L"you cannot do both '%ls' and '%ls' in the same invocation"), subcmd_str1, + subcmd_str2); + streams.err.append_format(BUILTIN_ERR_COMBO2, cmd, err_text); + return false; + } + + *hist_cmd = sub_cmd; + return true; +} + +#define CHECK_FOR_UNEXPECTED_HIST_ARGS(hist_cmd) \ + if (opts.history_search_type_defined || opts.show_time_format || opts.null_terminate) { \ + const wchar_t *subcmd_str = enum_to_str(hist_cmd, hist_enum_map); \ + streams.err.append_format(_(L"%ls: you cannot use any options with the %ls command\n"), \ + cmd, subcmd_str); \ + status = STATUS_INVALID_ARGS; \ + break; \ + } \ + if (args.size() != 0) { \ + const wchar_t *subcmd_str = enum_to_str(hist_cmd, hist_enum_map); \ + streams.err.append_format(BUILTIN_ERR_ARG_COUNT2, cmd, subcmd_str, 0, args.size()); \ + status = STATUS_INVALID_ARGS; \ + break; \ + } + +static int parse_history_opts(struct history_opts *opts, int *optind, //!OCLINT(high ncss method) + int argc, wchar_t **argv, parser_t &parser, io_streams_t &streams) { + wchar_t *cmd = argv[0]; + int opt; + wgetopter_t w; + while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -1) { + switch (opt) { + case 1: { + if (!set_hist_cmd(cmd, &opts->hist_cmd, HIST_DELETE, streams)) { + return STATUS_CMD_ERROR; + } + break; + } + case 2: { + if (!set_hist_cmd(cmd, &opts->hist_cmd, HIST_SEARCH, streams)) { + return STATUS_CMD_ERROR; + } + break; + } + case 3: { + if (!set_hist_cmd(cmd, &opts->hist_cmd, HIST_SAVE, streams)) { + return STATUS_CMD_ERROR; + } + break; + } + case 4: { + if (!set_hist_cmd(cmd, &opts->hist_cmd, HIST_CLEAR, streams)) { + return STATUS_CMD_ERROR; + } + break; + } + case 5: { + if (!set_hist_cmd(cmd, &opts->hist_cmd, HIST_MERGE, streams)) { + return STATUS_CMD_ERROR; + } + break; + } + case 'C': { + opts->case_sensitive = true; + break; + } + case 'p': { + opts->search_type = HISTORY_SEARCH_TYPE_PREFIX; + opts->history_search_type_defined = true; + break; + } + case 'c': { + opts->search_type = HISTORY_SEARCH_TYPE_CONTAINS; + opts->history_search_type_defined = true; + break; + } + case 'e': { + opts->search_type = HISTORY_SEARCH_TYPE_EXACT; + opts->history_search_type_defined = true; + break; + } + case 't': { + opts->show_time_format = w.woptarg ? w.woptarg : L"# %c%n"; + break; + } + case 'n': { + opts->max_items = fish_wcstol(w.woptarg); + if (errno) { + streams.err.append_format(_(L"%ls: max value '%ls' is not a valid number\n"), + cmd, w.woptarg); + return STATUS_INVALID_ARGS; + } + break; + } + case 'z': { + opts->null_terminate = true; + break; + } + case 'h': { + opts->print_help = true; + return STATUS_CMD_OK; + } + case ':': { + streams.err.append_format(BUILTIN_ERR_MISSING, cmd, argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; + } + case '?': { + // Try to parse it as a number; e.g., "-123". + opts->max_items = fish_wcstol(argv[w.woptind - 1] + 1); + if (errno) { + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; + } + w.nextchar = NULL; + break; + } + default: { + DIE("unexpected retval from wgetopt_long"); + break; + } + } + } + + *optind = w.woptind; + return STATUS_CMD_OK; +} + +/// Manipulate history of interactive commands executed by the user. +int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **argv) { + wchar_t *cmd = argv[0]; + int argc = builtin_count_args(argv); + struct history_opts opts; + + int optind; + int retval = parse_history_opts(&opts, &optind, argc, argv, parser, streams); + if (retval != STATUS_CMD_OK) return retval; + + if (opts.print_help) { + builtin_print_help(parser, streams, cmd, streams.out); + return STATUS_CMD_OK; + } + + // Use the default history if we have none (which happens if invoked non-interactively, e.g. + // from webconfig.py. + history_t *history = reader_get_history(); + if (!history) history = &history_t::history_with_name(L"fish"); + + // If a history command hasn't already been specified via a flag check the first word. + // Note that this can be simplified after we eliminate allowing subcommands as flags. + // See the TODO above regarding the `long_options` array. + if (optind < argc) { + hist_cmd_t subcmd = str_to_enum(argv[optind], hist_enum_map, hist_enum_map_len); + if (subcmd != HIST_UNDEF) { + if (!set_hist_cmd(cmd, &opts.hist_cmd, subcmd, streams)) { + return STATUS_INVALID_ARGS; + } + optind++; + } + } + + // Every argument that we haven't consumed already is an argument for a subcommand (e.g., a + // search term). + const wcstring_list_t args(argv + optind, argv + argc); + + // Establish appropriate defaults. + if (opts.hist_cmd == HIST_UNDEF) opts.hist_cmd = HIST_SEARCH; + if (!opts.history_search_type_defined) { + if (opts.hist_cmd == HIST_SEARCH) opts.search_type = HISTORY_SEARCH_TYPE_CONTAINS; + if (opts.hist_cmd == HIST_DELETE) opts.search_type = HISTORY_SEARCH_TYPE_EXACT; + } + + int status = STATUS_CMD_OK; + switch (opts.hist_cmd) { + case HIST_SEARCH: { + if (!history->search(opts.search_type, args, opts.show_time_format, opts.max_items, + opts.case_sensitive, opts.null_terminate, streams)) { + status = STATUS_CMD_ERROR; + } + break; + } + case HIST_DELETE: { + // TODO: Move this code to the history module and support the other search types + // including case-insensitive matches. At this time we expect the non-exact deletions to + // be handled only by the history function's interactive delete feature. + if (opts.search_type != HISTORY_SEARCH_TYPE_EXACT) { + streams.err.append_format(_(L"builtin history delete only supports --exact\n")); + status = STATUS_INVALID_ARGS; + break; + } + if (!opts.case_sensitive) { + streams.err.append_format( + _(L"builtin history delete only supports --case-sensitive\n")); + status = STATUS_INVALID_ARGS; + break; + } + for (wcstring_list_t::const_iterator iter = args.begin(); iter != args.end(); ++iter) { + wcstring delete_string = *iter; + if (delete_string[0] == '"' && delete_string[delete_string.length() - 1] == '"') { + delete_string = delete_string.substr(1, delete_string.length() - 2); + } + history->remove(delete_string); + } + break; + } + case HIST_CLEAR: { + CHECK_FOR_UNEXPECTED_HIST_ARGS(opts.hist_cmd) + history->clear(); + history->save(); + break; + } + case HIST_MERGE: { + CHECK_FOR_UNEXPECTED_HIST_ARGS(opts.hist_cmd) + history->incorporate_external_changes(); + break; + } + case HIST_SAVE: { + CHECK_FOR_UNEXPECTED_HIST_ARGS(opts.hist_cmd) + history->save(); + break; + } + case HIST_UNDEF: { + DIE("Unexpected HIST_UNDEF seen"); + break; + } + } + + return status; +} diff --git a/src/builtin_history.h b/src/builtin_history.h new file mode 100644 index 000000000..7c27ed932 --- /dev/null +++ b/src/builtin_history.h @@ -0,0 +1,9 @@ +// Prototypes for executing builtin_history function. +#ifndef FISH_BUILTIN_HISTORY_H +#define FISH_BUILTIN_HISTORY_H + +class parser_t; +struct io_streams_t; + +int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **argv); +#endif