mirror of
https://github.com/fish-shell/fish-shell.git
synced 2025-01-23 02:03:01 +08:00
37b8cfaeba
Running the tests on travis revealed that some compilers (or at least with some options) call the wrong struct constructor if there is more than one struct with the same name but differing definitions.
298 lines
12 KiB
C++
298 lines
12 KiB
C++
// Implementation of the history builtin.
|
|
#include "config.h" // IWYU pragma: keep
|
|
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <stddef.h>
|
|
#include <wchar.h>
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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_cmd_t> 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_cmd_opts_t {
|
|
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_cmd_opts(history_cmd_opts_t &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;
|
|
break;
|
|
}
|
|
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);
|
|
history_cmd_opts_t opts;
|
|
|
|
int optind;
|
|
int retval = parse_cmd_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;
|
|
}
|