Codebase list fish / HEAD src / builtin_history.cpp
HEAD

Tree @HEAD (Download .tar.gz)

builtin_history.cpp @HEADraw · history · blame

// Implementation of the history builtin.
#include "config.h"  // IWYU pragma: keep

#include "builtin_history.h"

#include <cerrno>
#include <cstddef>
#include <cstdint>
#include <cwchar>
#include <string>
#include <vector>

#include "builtin.h"
#include "common.h"
#include "fallback.h"  // IWYU pragma: keep
#include "history.h"
#include "io.h"
#include "parser.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.
static 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, nullptr}};

struct history_cmd_opts_t {
    hist_cmd_t hist_cmd = HIST_UNDEF;
    history_search_type_t search_type = static_cast<history_search_type_t>(-1);
    const wchar_t *show_time_format = nullptr;
    size_t max_items = SIZE_MAX;
    bool print_help = false;
    bool history_search_type_defined = false;
    bool case_sensitive = false;
    bool null_terminate = false;
    bool reverse = 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 *const short_options = L":CRcehmn:pt::z";
static const struct woption long_options[] = {{L"prefix", no_argument, nullptr, 'p'},
                                              {L"contains", no_argument, nullptr, 'c'},
                                              {L"help", no_argument, nullptr, 'h'},
                                              {L"show-time", optional_argument, nullptr, 't'},
                                              {L"exact", no_argument, nullptr, 'e'},
                                              {L"max", required_argument, nullptr, 'n'},
                                              {L"null", no_argument, nullptr, 'z'},
                                              {L"case-sensitive", no_argument, nullptr, 'C'},
                                              {L"delete", no_argument, nullptr, 1},
                                              {L"search", no_argument, nullptr, 2},
                                              {L"save", no_argument, nullptr, 3},
                                              {L"clear", no_argument, nullptr, 4},
                                              {L"merge", no_argument, nullptr, 5},
                                              {L"reverse", no_argument, nullptr, 'R'},
                                              {nullptr, 0, nullptr, 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);
        std::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;
}

static bool check_for_unexpected_hist_args(const history_cmd_opts_t &opts, const wchar_t *cmd,
                                           const wcstring_list_t &args, io_streams_t &streams) {
    if (opts.history_search_type_defined || opts.show_time_format || opts.null_terminate) {
        const wchar_t *subcmd_str = enum_to_str(opts.hist_cmd, hist_enum_map);
        streams.err.append_format(_(L"%ls: you cannot use any options with the %ls command\n"), cmd,
                                  subcmd_str);
        return true;
    }
    if (!args.empty()) {
        const wchar_t *subcmd_str = enum_to_str(opts.hist_cmd, hist_enum_map);
        streams.err.append_format(BUILTIN_ERR_ARG_COUNT2, cmd, subcmd_str, 0, args.size());
        return true;
    }
    return false;
}

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, nullptr)) != -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 'R': {
                opts.reverse = true;
                break;
            }
            case 'p': {
                opts.search_type = history_search_type_t::prefix_glob;
                opts.history_search_type_defined = true;
                break;
            }
            case 'c': {
                opts.search_type = history_search_type_t::contains_glob;
                opts.history_search_type_defined = true;
                break;
            }
            case 'e': {
                opts.search_type = history_search_type_t::exact;
                opts.history_search_type_defined = true;
                break;
            }
            case 't': {
                opts.show_time_format = w.woptarg ? w.woptarg : L"# %c%n";
                break;
            }
            case 'n': {
                long x = fish_wcstol(w.woptarg);
                if (errno) {
                    streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, w.woptarg);
                    return STATUS_INVALID_ARGS;
                }
                opts.max_items = static_cast<size_t>(x);
                break;
            }
            case 'z': {
                opts.null_terminate = true;
                break;
            }
            case 'h': {
                opts.print_help = true;
                break;
            }
            case ':': {
                builtin_missing_argument(parser, streams, 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 = nullptr;
                break;
            }
            default: {
                DIE("unexpected retval from wgetopt_long");
            }
        }
    }

    *optind = w.woptind;
    return STATUS_CMD_OK;
}

/// Manipulate history of interactive commands executed by the user.
maybe_t<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);
        return STATUS_CMD_OK;
    }

    // Use the default history if we have none (which happens if invoked non-interactively, e.g.
    // from webconfig.py.
    std::shared_ptr<history_t> history = reader_get_history();
    if (!history) history = history_t::with_name(history_session_id(parser.vars()));

    // 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) {
        constexpr size_t hist_enum_map_len = sizeof hist_enum_map / sizeof *hist_enum_map;
        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_t::contains_glob;
        if (opts.hist_cmd == HIST_DELETE) opts.search_type = history_search_type_t::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, opts.reverse,
                                 parser.cancel_checker(), 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_t::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 --exact requires --case-sensitive\n"));
                status = STATUS_INVALID_ARGS;
                break;
            }
            for (wcstring delete_string : args) {
                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: {
            if (check_for_unexpected_hist_args(opts, cmd, args, streams)) {
                status = STATUS_INVALID_ARGS;
                break;
            }
            history->clear();
            history->save();
            break;
        }
        case HIST_MERGE: {
            if (check_for_unexpected_hist_args(opts, cmd, args, streams)) {
                status = STATUS_INVALID_ARGS;
                break;
            }

            history->incorporate_external_changes();
            break;
        }
        case HIST_SAVE: {
            if (check_for_unexpected_hist_args(opts, cmd, args, streams)) {
                status = STATUS_INVALID_ARGS;
                break;
            }
            history->save();
            break;
        }
        case HIST_UNDEF: {
            DIE("Unexpected HIST_UNDEF seen");
        }
    }

    return status;
}