split builtin history into its own module

This commit is contained in:
Kurtis Rader 2017-06-12 21:11:42 -07:00
parent 2079b4292e
commit ef8a0c93ea
4 changed files with 309 additions and 263 deletions

View file

@ -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

View file

@ -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_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)
/// 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);

297
src/builtin_history.cpp Normal file
View file

@ -0,0 +1,297 @@
// 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_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;
}

9
src/builtin_history.h Normal file
View file

@ -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