From 65dcd06ca16a347c8053a5c8858f480ddae0b7da Mon Sep 17 00:00:00 2001 From: Kurtis Rader Date: Fri, 15 Sep 2017 13:43:45 -0700 Subject: [PATCH] mplement `history search` glob searches Instead of treating the search term as a literal string to be matched treat it as a glob. This allows the user to get a more useful set of results by using the `*` glob character in the search term. Partial fix for #3136 --- CHANGELOG.md | 1 + src/builtin_history.cpp | 6 ++--- src/history.cpp | 54 ++++++++++++++++++++++------------------ src/history.h | 10 +++++++- tests/history.expect | 8 ++++++ tests/history.expect.out | 1 + 6 files changed, 52 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01aca162a..cb3c40f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This section is for changes merged to the `major` branch that are not also merge - Setting variables is much faster (#4200, #4341). - Using a read-only variable in a for loop is now an error. Note that this never worked. It simply failed to set the for loop var and thus silently produced incorrect results (#4342). - `math` is now a builtin rather than a wrapper around `bc` (#3157). +- `history search` supports globs for wildcard searching (#3136). ## Other significant changes - Command substitution output is now limited to 10 MB by default (#3822). diff --git a/src/builtin_history.cpp b/src/builtin_history.cpp index c7c86480c..5e15cefac 100644 --- a/src/builtin_history.cpp +++ b/src/builtin_history.cpp @@ -140,12 +140,12 @@ static int parse_cmd_opts(history_cmd_opts_t &opts, int *optind, //!OCLINT(high break; } case 'p': { - opts.search_type = HISTORY_SEARCH_TYPE_PREFIX; + opts.search_type = HISTORY_SEARCH_TYPE_PREFIX_GLOB; opts.history_search_type_defined = true; break; } case 'c': { - opts.search_type = HISTORY_SEARCH_TYPE_CONTAINS; + opts.search_type = HISTORY_SEARCH_TYPE_CONTAINS_GLOB; opts.history_search_type_defined = true; break; } @@ -241,7 +241,7 @@ int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **argv) { // 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_SEARCH) opts.search_type = HISTORY_SEARCH_TYPE_CONTAINS_GLOB; if (opts.hist_cmd == HIST_DELETE) opts.search_type = HISTORY_SEARCH_TYPE_EXACT; } diff --git a/src/history.cpp b/src/history.cpp index 255de7eb2..115fc5dbf 100644 --- a/src/history.cpp +++ b/src/history.cpp @@ -40,6 +40,7 @@ #include "parse_util.h" #include "path.h" #include "reader.h" +#include "wildcard.h" // IWYU pragma: keep #include "wutil.h" // IWYU pragma: keep // Our history format is intended to be valid YAML. Here it is: @@ -456,31 +457,36 @@ history_item_t::history_item_t(const wcstring &str, time_t when, history_identif bool history_item_t::matches_search(const wcstring &term, enum history_search_type_t type, bool case_sensitive) const { - // We don't use a switch below because there are only three cases and if the strings are the - // same length we can use the faster HISTORY_SEARCH_TYPE_EXACT for the other two cases. - // - // Too, we consider equal strings to match a prefix search, so that autosuggest will allow - // suggesting what you've typed. - if (case_sensitive) { - if (type == HISTORY_SEARCH_TYPE_EXACT || term.size() == contents.size()) { - return term == contents; - } else if (type == HISTORY_SEARCH_TYPE_CONTAINS) { - return contents.find(term) != wcstring::npos; - } else if (type == HISTORY_SEARCH_TYPE_PREFIX) { - return string_prefixes_string(term, contents); - } - } else { - wcstring lterm(L""); - for (wcstring::const_iterator it = term.begin(); it != term.end(); ++it) { - lterm.push_back(towlower(*it)); - } + // Note that this->term has already been lowercased when constructing the + // search object if we're doing a case insensitive search. + const wcstring &content_to_match = case_sensitive ? contents : contents_lower; - if (type == HISTORY_SEARCH_TYPE_EXACT || lterm.size() == contents.size()) { - return lterm == contents_lower; - } else if (type == HISTORY_SEARCH_TYPE_CONTAINS) { - return contents_lower.find(lterm) != wcstring::npos; - } else if (type == HISTORY_SEARCH_TYPE_PREFIX) { - return string_prefixes_string(lterm, contents_lower); + switch (type) { + case HISTORY_SEARCH_TYPE_EXACT: { + return term == content_to_match; + } + case HISTORY_SEARCH_TYPE_CONTAINS: { + return content_to_match.find(term) != wcstring::npos; + } + case HISTORY_SEARCH_TYPE_PREFIX: { + return string_prefixes_string(term, content_to_match); + } + case HISTORY_SEARCH_TYPE_CONTAINS_GLOB: { + wcstring wcpattern1 = parse_util_unescape_wildcards(term); + if (wcpattern1.front() != ANY_STRING) wcpattern1.insert(0, 1, ANY_STRING); + if (wcpattern1.back() != ANY_STRING) wcpattern1.push_back(ANY_STRING); + return wildcard_match(content_to_match, wcpattern1); + } + case HISTORY_SEARCH_TYPE_PREFIX_GLOB: { + wcstring wcpattern2 = parse_util_unescape_wildcards(term); + if (wcpattern2.back() != ANY_STRING) wcpattern2.push_back(ANY_STRING); + return wildcard_match(content_to_match, wcpattern2); + } + case HISTORY_SEARCH_TYPE_CONTAINS_PCRE: { + abort(); + } + case HISTORY_SEARCH_TYPE_PREFIX_PCRE: { + abort(); } } DIE("unexpected history_search_type_t value"); diff --git a/src/history.h b/src/history.h index 6b7148fc4..a706d24ae 100644 --- a/src/history.h +++ b/src/history.h @@ -48,7 +48,15 @@ enum history_search_type_t { // Search for commands containing the given string. HISTORY_SEARCH_TYPE_CONTAINS, // Search for commands starting with the given string. - HISTORY_SEARCH_TYPE_PREFIX + HISTORY_SEARCH_TYPE_PREFIX, + // Search for commands containing the given glob pattern. + HISTORY_SEARCH_TYPE_CONTAINS_GLOB, + // Search for commands starting with the given glob pattern. + HISTORY_SEARCH_TYPE_PREFIX_GLOB, + // Search for commands containing the given PCRE pattern. + HISTORY_SEARCH_TYPE_CONTAINS_PCRE, + // Search for commands starting with the given PCRE pattern. + HISTORY_SEARCH_TYPE_PREFIX_PCRE }; typedef uint32_t history_identifier_t; diff --git a/tests/history.expect b/tests/history.expect index 260d5bd09..661a0f0d2 100644 --- a/tests/history.expect +++ b/tests/history.expect @@ -110,6 +110,14 @@ expect_prompt -re {history search --exact 'echo hell'\r\n} { puts stderr "history function explicit exact search 'echo hell' failed" } +# Verify that glob searching works. +send "history search --prefix 'echo start*echo end'\r" +expect_prompt -re {echo start1; builtin history; echo end1\r\n} { + puts "history function explicit glob search 'echo start*echo end' succeeded" +} timeout { + puts stderr "history function explicit glob search 'echo start*echo end' failed" +} + # ========== # Delete a single command we recently ran. send "history delete -e -C 'echo hello'\r" diff --git a/tests/history.expect.out b/tests/history.expect.out index 6e495d4fa..c4eb84d75 100644 --- a/tests/history.expect.out +++ b/tests/history.expect.out @@ -6,6 +6,7 @@ history function implicit search with timestamps succeeded history function explicit exact search 'echo goodbye' succeeded history function explicit exact search 'echo hello' succeeded history function explicit exact search 'echo hell' succeeded +history function explicit glob search 'echo start*echo end' succeeded history function explicit exact delete 'echo hello' succeeded history function explicit prefix delete 'echo hello AGAIN' succeeded history function explicit exact search 'echo hello again' succeeded