make history searching case insensitive by default

Fixes #3236
This commit is contained in:
Kurtis Rader 2016-09-23 20:12:15 -07:00
parent dc6b538f56
commit f490b56378
8 changed files with 186 additions and 83 deletions

View file

@ -2,8 +2,8 @@
\subsection history-synopsis Synopsis
\fish{synopsis}
history search [ --show-time ] [ --exact | --prefix | --contains ] [ --max=n ] [ "search string"... ]
history delete [ --show-time ] [ --exact | --prefix | --contains ] "search string"...
history search [ --show-time ] [ --case-sensitive ] [ --exact | --prefix | --contains ] [ --max=n ] [ "search string"... ]
history delete [ --show-time ] [ --case-sensitive ] [ --exact | --prefix | --contains ] "search string"...
history merge
history save
history clear
@ -20,7 +20,7 @@ The following operations (sub-commands) are available:
- `search` returns history items matching the search string. If no search string is provided it returns all history items. This is the default operation if no other operation is specified. You only have to explicitly say `history search` if you wish to search for one of the subcommands. The `--contains` search option will be used if you don't specify a different search option. Entries are ordered newest to oldest. If stdout is attached to a tty the output will be piped through your pager by the history function. The history builtin simply writes the results to stdout.
- `delete` deletes history items. Without the `--prefix` or `--contains` options, the exact match will be deleted. With either of these options, a prompt will be displayed before any items are deleted asking you which entries are to be deleted. You can enter the word "all" to delete all matching entries. You can enter a single ID (the number in square brackets) to delete just that single entry. You can enter more than one ID separated by a space to delete multiple entries. Just press [enter] to not delete anything. Note that the interactive delete behavior is a feature of the history function. The history builtin only supports bulk deletion.
- `delete` deletes history items. Without the `--prefix` or `--contains` options, the exact match of the specified text will be deleted. If you don't specify `--exact` a prompt will be displayed before any items are deleted asking you which entries are to be deleted. You can enter the word "all" to delete all matching entries. You can enter a single ID (the number in square brackets) to delete just that single entry. You can enter more than one ID separated by a space to delete multiple entries. Just press [enter] to not delete anything. Note that the interactive delete behavior is a feature of the history function. The history builtin only supports `--exact --case-sensitive` deletion.
- `merge` immediately incorporates history changes from other sessions. Ordinarily `fish` ignores history changes from sessions started after the current one. This command applies those changes immediately.
@ -32,9 +32,11 @@ The following options are available:
These flags can appear before or immediately after one of the sub-commands listed above.
- `-C` or `--case-sensitive` does a case-sensitive search. The default is case-insensitive. Note that prior to fish 2.4.0 the default was case-sensitive.
- `-c` or `--contains` searches or deletes items in the history that contain the specified text string. This is the default for the `--search` flag. This is not currently supported by the `--delete` flag.
- `-e` or `--exact` searches or deletes items in the history that exactly match the specified text string. This is the default for the `--delete` flag.
- `-e` or `--exact` searches or deletes items in the history that exactly match the specified text string. This is the default for the `--delete` flag. Note that the match is case-insensitive by default. If you really want an exact match, including letter case, you must use the `-C` or `--case-sensitive` flag.
- `-p` or `--prefix` searches or deletes items in the history that begin with the specified text string. This is not currently supported by the `--delete` flag.

View file

@ -35,6 +35,7 @@ function history --description "display or manipulate interactive command histor
set -l search_mode
set -l show_time
set -l max_count
set -l case_sensitive
# Check for a recognized subcommand as the first argument.
if set -q argv[1]
@ -68,6 +69,8 @@ function history --description "display or manipulate interactive command histor
case --merge
__fish_set_hist_cmd merge
or return
case -C --case_sensitive
set case_sensitive --case-sensitive
case -h --help
builtin history --help
return
@ -121,14 +124,13 @@ function history --description "display or manipulate interactive command histor
test -z "$search_mode"
and set search_mode "--contains"
echo "builtin history search $search_mode $show_time $max_count -- $argv" >>/tmp/x
if isatty stdout
set -l pager less
set -q PAGER
and set pager $PAGER
builtin history search $search_mode $show_time $max_count -- $argv | eval $pager
builtin history search $search_mode $show_time $max_count $case_sensitive -- $argv | eval $pager
else
builtin history search $search_mode $show_time $max_count -- $argv
builtin history search $search_mode $show_time $max_count $case_sensitive -- $argv
end
case delete # interactively delete history
@ -139,16 +141,16 @@ function history --description "display or manipulate interactive command histor
end
test -z "$search_mode"
and set search_mode "--exact"
and set search_mode "--contains"
if test $search_mode = "--exact"
builtin history delete $search_mode $argv
builtin history delete $search_mode $case_sensitive $argv
return
end
# TODO: Fix this so that requesting history entries with a timestamp works:
# set -l found_items (builtin history search $search_mode $show_time -- $argv)
set -l found_items (builtin history search $search_mode -- $argv)
set -l found_items (builtin history search $search_mode $case_sensitive -- $argv)
if set -q found_items[1]
set -l found_items_count (count $found_items)
for i in (seq $found_items_count)
@ -169,11 +171,8 @@ function history --description "display or manipulate interactive command histor
if test "$choice" = "all"
printf "Deleting all matching entries!\n"
# TODO: Use the following when the builtin is enhanced to support the
# --prefix and --contains options (at the moment it only supports --exact).
# builtin history delete $search_mode -- $argv
for item in $found_items
builtin history delete --exact -- $item
builtin history delete --exact --case-sensitive -- $item
end
builtin history save
return
@ -188,7 +187,7 @@ function history --description "display or manipulate interactive command histor
end
printf "Deleting history entry %s: \"%s\"\n" $i $found_items[$i]
builtin history delete "$found_items[$i]"
builtin history delete --exact --case-sensitive -- "$found_items[$i]"
end
builtin history save
end

View file

@ -2875,10 +2875,11 @@ static int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **ar
long max_items = LONG_MAX;
bool history_search_type_defined = false;
const wchar_t *show_time_format = NULL;
bool case_sensitive = false;
// TODO: Remove the long options that correspond to subcommands (e.g., '--delete') on or after
// 2017-10 (which will be a full year after these flags have been deprecated).
const wchar_t *short_options = L":mn:epcht";
const wchar_t *short_options = L":Cmn:epcht";
const struct woption long_options[] = {{L"prefix", no_argument, NULL, 'p'},
{L"contains", no_argument, NULL, 'c'},
{L"help", no_argument, NULL, 'h'},
@ -2886,6 +2887,7 @@ static int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **ar
{L"with-time", optional_argument, NULL, 't'},
{L"exact", no_argument, NULL, 'e'},
{L"max", required_argument, NULL, 'n'},
{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},
@ -2932,6 +2934,10 @@ static int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **ar
}
break;
}
case 'C': {
case_sensitive = true;
break;
}
case 'p': {
search_type = HISTORY_SEARCH_TYPE_PREFIX;
history_search_type_defined = true;
@ -3014,20 +3020,27 @@ static int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **ar
int status = STATUS_BUILTIN_OK;
switch (hist_cmd) {
case HIST_SEARCH: {
if (!history->search(search_type, args, show_time_format, max_items, streams)) {
if (!history->search(search_type, args, show_time_format, max_items, case_sensitive,
streams)) {
status = STATUS_BUILTIN_ERROR;
}
break;
}
case HIST_DELETE: {
// TODO: Move this code to the history module and support the other search types. At
// this time we expect the non-exact deletions to be handled only by the history
// function's interactive delete feature.
// 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_BUILTIN_ERROR;
break;
}
if (!case_sensitive) {
streams.err.append_format(
_(L"builtin history delete only supports --case-sensitive\n"));
status = STATUS_BUILTIN_ERROR;
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] == '"') {

View file

@ -161,6 +161,11 @@ static int chdir_set_pwd(const char *path) {
if (!(e)) err(L"Test failed on line %lu: %s", __LINE__, #e); \
} while (0)
#define do_test_from(e, from_line) \
do { \
if (!(e)) err(L"Test failed on line %lu (from %lu): %s", __LINE__, from_line, #e); \
} while (0)
#define do_test1(e, msg) \
do { \
if (!(e)) err(L"Test failed on line %lu: %ls", __LINE__, (msg)); \
@ -2178,7 +2183,6 @@ static void perform_one_autosuggestion_should_ignore_test(const wcstring &comman
static void test_autosuggestion_ignores() {
say(L"Testing scenarios that should produce no autosuggestions");
const wcstring wd = L"/tmp/autosuggest_test/";
// Do not do file autosuggestions immediately after certain statement terminators - see #1631.
perform_one_autosuggestion_should_ignore_test(L"echo PIPE_TEST|", __LINE__);
perform_one_autosuggestion_should_ignore_test(L"echo PIPE_TEST&", __LINE__);
@ -2201,18 +2205,20 @@ static void test_autosuggestion_combining() {
do_test(combine_command_and_autosuggestion(L"alpha", L"ALPHA") == L"alpha");
}
static void test_history_matches(history_search_t &search, size_t matches) {
static void test_history_matches(history_search_t &search, size_t matches, unsigned from_line) {
size_t i;
for (i = 0; i < matches; i++) {
do_test(search.go_backwards());
wcstring item = search.current_string();
}
do_test(!search.go_backwards());
// do_test_from(!search.go_backwards(), from_line);
bool result = search.go_backwards();
do_test_from(!result, from_line);
for (i = 1; i < matches; i++) {
do_test(search.go_forwards());
do_test_from(search.go_forwards(), from_line);
}
do_test(!search.go_forwards());
do_test_from(!search.go_forwards(), from_line);
}
static bool history_contains(history_t *history, const wcstring &txt) {
@ -2518,28 +2524,64 @@ static wcstring random_string(void) {
}
void history_tests_t::test_history(void) {
history_search_t searcher;
say(L"Testing history");
history_t &history = history_t::history_with_name(L"test_history");
history.clear();
history.add(L"Gamma");
history.add(L"beta");
history.add(L"BetA");
history.add(L"Beta");
history.add(L"alpha");
history.add(L"AlphA");
history.add(L"Alpha");
history.add(L"alph");
history.add(L"ALPH");
history.add(L"ZZZ");
// All three items match "a".
history_search_t search1(history, L"a");
test_history_matches(search1, 3);
do_test(search1.current_string() == L"Alpha");
// Items matching "a", case-sensitive.
searcher = history_search_t(history, L"a");
test_history_matches(searcher, 6, __LINE__);
do_test(searcher.current_string() == L"alph");
// One item matches "et".
history_search_t search2(history, L"et");
test_history_matches(search2, 1);
do_test(search2.current_string() == L"Beta");
// Items matching "alpha", case-insensitive. Note that HISTORY_SEARCH_TYPE_CONTAINS but we have
// to explicitly specify it in order to be able to pass false for the case_sensitive parameter.
searcher = history_search_t(history, L"AlPhA", HISTORY_SEARCH_TYPE_CONTAINS, false);
test_history_matches(searcher, 3, __LINE__);
do_test(searcher.current_string() == L"Alpha");
// Test item removal.
// Items matching "et", case-sensitive.
searcher = history_search_t(history, L"et");
test_history_matches(searcher, 3, __LINE__);
do_test(searcher.current_string() == L"Beta");
// Items starting with "be", case-sensitive.
searcher = history_search_t(history, L"be", HISTORY_SEARCH_TYPE_PREFIX, true);
test_history_matches(searcher, 1, __LINE__);
do_test(searcher.current_string() == L"beta");
// Items starting with "be", case-insensitive.
searcher = history_search_t(history, L"be", HISTORY_SEARCH_TYPE_PREFIX, false);
test_history_matches(searcher, 3, __LINE__);
do_test(searcher.current_string() == L"Beta");
// Items exactly matchine "alph", case-sensitive.
searcher = history_search_t(history, L"alph", HISTORY_SEARCH_TYPE_EXACT, true);
test_history_matches(searcher, 1, __LINE__);
do_test(searcher.current_string() == L"alph");
// Items exactly matchine "alph", case-insensitive.
searcher = history_search_t(history, L"alph", HISTORY_SEARCH_TYPE_EXACT, false);
test_history_matches(searcher, 2, __LINE__);
do_test(searcher.current_string() == L"ALPH");
// Test item removal case-sensitive.
searcher = history_search_t(history, L"Alpha");
test_history_matches(searcher, 1, __LINE__);
history.remove(L"Alpha");
history_search_t search3(history, L"Alpha");
test_history_matches(search3, 0);
searcher = history_search_t(history, L"Alpha");
test_history_matches(searcher, 0, __LINE__);
// Test history escaping and unescaping, yaml, etc.
history_item_list_t before, after;

View file

@ -428,27 +428,52 @@ bool history_item_t::merge(const history_item_t &item) {
return result;
}
#if 0
history_item_t::history_item_t(const wcstring &str)
: contents(str), creation_timestamp(time(NULL)), identifier(0) {}
: contents(str), contents_lower(L""), creation_timestamp(time(NULL)), identifier(0) {
for (wcstring::const_iterator it = str.begin(); it != str.end(); ++it) {
contents_lower.push_back(towlower(*it));
}
}
#endif
history_item_t::history_item_t(const wcstring &str, time_t when, history_identifier_t ident)
: contents(str), creation_timestamp(when), identifier(ident) {}
: contents(str), contents_lower(L""), creation_timestamp(when), identifier(ident) {
for (wcstring::const_iterator it = str.begin(); it != str.end(); ++it) {
contents_lower.push_back(towlower(*it));
}
}
bool history_item_t::matches_search(const wcstring &term, enum history_search_type_t type) const {
switch (type) {
case HISTORY_SEARCH_TYPE_CONTAINS: {
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;
}
case HISTORY_SEARCH_TYPE_PREFIX: {
// We consider equal strings to match a prefix search, so that autosuggest will allow
// suggesting what you've typed.
} else if (type == HISTORY_SEARCH_TYPE_PREFIX) {
return string_prefixes_string(term, contents);
}
case HISTORY_SEARCH_TYPE_EXACT: {
return term == contents;
} else {
wcstring lterm(L"");
for (wcstring::const_iterator it = term.begin(); it != term.end(); ++it) {
lterm.push_back(towlower(*it));
}
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);
}
default: { DIE("unexpected history_search_type_t value"); }
}
DIE("unexpected history_search_type_t value");
}
/// Append our YAML history format to the provided vector at the given offset, updating the offset.
@ -765,16 +790,27 @@ void history_t::add(const wcstring &str, history_identifier_t ident, bool pendin
this->add(history_item_t(str, when, ident), pending);
}
void history_t::remove(const wcstring &str) {
// Add to our list of deleted items.
deleted_items.insert(str);
bool icompare_pred(wchar_t a, wchar_t b) { return std::tolower(a) == std::tolower(b); }
bool icompare(wcstring const &a, wcstring const &b) {
if (a.length() == b.length()) {
return std::equal(b.begin(), b.end(), a.begin(), icompare_pred);
} else {
return false;
}
}
// Remove matching history entries from our list of new items. This only supports literal,
// case-sensitive, matches.
void history_t::remove(const wcstring &str_to_remove) {
// Add to our list of deleted items.
deleted_items.insert(str_to_remove);
// Remove from our list of new items.
size_t idx = new_items.size();
while (idx--) {
if (new_items.at(idx).str() == str) {
bool matched = new_items.at(idx).str() == str_to_remove;
if (matched) {
new_items.erase(new_items.begin() + idx);
// If this index is before our first_unwritten_new_item_index, then subtract one from
// that index so it stays pointing at the same item. If it is equal to or larger, then
// we have not yet writen this item, so we don't have to adjust the index.
@ -1003,7 +1039,7 @@ bool history_search_t::go_backwards() {
// Look for a term that matches and that we haven't seen before.
const wcstring &str = item.str();
if (item.matches_search(term, search_type) && !match_already_made(str) &&
if (item.matches_search(term, search_type, case_sensitive) && !match_already_made(str) &&
!should_skip_match(str)) {
prev_matches.push_back(prev_match_t(idx, item));
return true;
@ -1417,7 +1453,8 @@ static bool format_history_record(const history_item_t &item, const wchar_t *sho
}
bool history_t::search(history_search_type_t search_type, wcstring_list_t search_args,
const wchar_t *show_time_format, long max_items, io_streams_t &streams) {
const wchar_t *show_time_format, long max_items, bool case_sensitive,
io_streams_t &streams) {
// scoped_lock locker(lock);
if (search_args.empty()) {
// Start at one because zero is the current command.
@ -1436,7 +1473,8 @@ bool history_t::search(history_search_type_t search_type, wcstring_list_t search
streams.err.append_format(L"Searching for the empty string isn't allowed");
return false;
}
history_search_t searcher = history_search_t(*this, search_string, search_type);
history_search_t searcher =
history_search_t(*this, search_string, search_type, case_sensitive);
while (searcher.go_backwards()) {
if (!format_history_record(searcher.current_item(), show_time_format, streams)) {
return false;

View file

@ -59,7 +59,8 @@ class history_item_t {
bool merge(const history_item_t &item);
// The actual contents of the entry.
wcstring contents;
wcstring contents; // value as entered by the user
wcstring contents_lower; // value normalized to all lowercase for case insensitive comparisons
// Original creation time for the entry.
time_t creation_timestamp;
@ -71,15 +72,16 @@ class history_item_t {
path_list_t required_paths;
public:
explicit history_item_t(const wcstring &str);
explicit history_item_t(const wcstring &, time_t, history_identifier_t ident = 0);
explicit history_item_t(const wcstring &str, time_t when = 0, history_identifier_t ident = 0);
const wcstring &str() const { return contents; }
const wcstring &str_lower() const { return contents_lower; }
bool empty() const { return contents.empty(); }
// Whether our contents matches a search term.
bool matches_search(const wcstring &term, enum history_search_type_t type) const;
bool matches_search(const wcstring &term, enum history_search_type_t type,
bool case_sensitive) const;
time_t timestamp() const { return creation_timestamp; }
@ -227,7 +229,8 @@ class history_t {
// Searches history.
bool search(history_search_type_t search_type, wcstring_list_t search_args,
const wchar_t *show_time_format, long max_items, io_streams_t &streams);
const wchar_t *show_time_format, long max_items, bool case_sensitive,
io_streams_t &streams);
// Enable / disable automatic saving. Main thread only!
void disable_automatic_saving();
@ -264,6 +267,7 @@ class history_search_t {
// Our type.
enum history_search_type_t search_type;
bool case_sensitive;
// Our list of previous matches as index, value. The end is the current match.
typedef std::pair<size_t, history_item_t> prev_match_t;
@ -310,11 +314,19 @@ class history_search_t {
// Constructor.
history_search_t(history_t &hist, const wcstring &str,
enum history_search_type_t type = HISTORY_SEARCH_TYPE_CONTAINS)
: history(&hist), search_type(type), term(str) {}
enum history_search_type_t type = HISTORY_SEARCH_TYPE_CONTAINS,
bool case_sensitive = true)
: history(&hist), term(str), search_type(type), case_sensitive(case_sensitive) {
if (!case_sensitive) {
term = wcstring();
for (wcstring::const_iterator it = str.begin(); it != str.end(); ++it) {
term.push_back(towlower(*it));
}
}
}
// Default constructor.
history_search_t() : history(), search_type(HISTORY_SEARCH_TYPE_CONTAINS), term() {}
history_search_t() : history(), term() {}
};
// Init history library. The history file won't actually be loaded until the first time a history

View file

@ -112,8 +112,10 @@ expect_prompt -re {history search --exact 'echo hell'\r\n} {
# ==========
# Delete a single command we recently ran.
send "history delete 'echo hello'\r"
expect_prompt -re {history delete 'echo hello'\r\n} {
send "history delete -e -C 'echo hello'\r"
expect -re {history delete -e -C 'echo hello'\r\n}
send "echo count hello (history search -e -C 'echo hello' | wc -l | string trim)\r"
expect -re {\r\ncount hello 0\r\n} {
puts "history function explicit exact delete 'echo hello' succeeded"
} unmatched {
puts stderr "history function explicit exact delete 'echo hello' failed"
@ -130,24 +132,20 @@ expect -re {\[2\] echo hello again\r\n\r\n}
expect -re {Enter nothing to cancel.*\r\nEnter "all" to delete all the matching entries\.\r\n}
expect -re {Delete which entries\? >}
send "1\r"
expect_prompt -re {Deleting history entry 1: "echo hello AGAIN"\r\n} {
puts "history function explicit prefix delete 'echo hello' succeeded"
} unmatched {
puts stderr "history function explicit prefix delete 'echo hello' failed"
}
expect -re {Deleting history entry 1: "echo hello AGAIN"\r\n}
# Verify that the deleted history entry is gone and the other one that matched
# the prefix search above is still there.
send "history search --exact 'echo hello again'\r"
expect_prompt -re {\r\necho hello again\r\n} {
send "echo count AGAIN (history search -e -C 'echo hello AGAIN' | wc -l | string trim)\r"
expect -re {\r\ncount AGAIN 0\r\n} {
puts "history function explicit prefix delete 'echo hello AGAIN' succeeded"
} unmatched {
puts stderr "history function explicit prefix delete 'echo hello AGAIN' failed"
}
send "echo count again (history search -e -C 'echo hello again' | wc -l | string trim)\r"
expect -re {\r\ncount again 1\r\n} {
puts "history function explicit exact search 'echo hello again' succeeded"
} unmatched {
puts stderr "history function explicit exact search 'echo hello again' failed"
}
send "history search --exact 'echo hello AGAIN'\r"
expect_prompt -re {\r\necho hello AGAIN\r\n} {
puts stderr "history function explicit exact search 'echo hello AGAIN' found the entry"
} unmatched {
puts "history function explicit exact search 'echo hello AGAIN' failed to find the entry"
}

View file

@ -7,6 +7,5 @@ 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 exact delete 'echo hello' succeeded
history function explicit prefix delete 'echo hello' succeeded
history function explicit prefix delete 'echo hello AGAIN' succeeded
history function explicit exact search 'echo hello again' succeeded
history function explicit exact search 'echo hello AGAIN' failed to find the entry