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 \subsection history-synopsis Synopsis
\fish{synopsis} \fish{synopsis}
history search [ --show-time ] [ --exact | --prefix | --contains ] [ --max=n ] [ "search string"... ] history search [ --show-time ] [ --case-sensitive ] [ --exact | --prefix | --contains ] [ --max=n ] [ "search string"... ]
history delete [ --show-time ] [ --exact | --prefix | --contains ] "search string"... history delete [ --show-time ] [ --case-sensitive ] [ --exact | --prefix | --contains ] "search string"...
history merge history merge
history save history save
history clear 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. - `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. - `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. 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. - `-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. - `-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 search_mode
set -l show_time set -l show_time
set -l max_count set -l max_count
set -l case_sensitive
# Check for a recognized subcommand as the first argument. # Check for a recognized subcommand as the first argument.
if set -q argv[1] if set -q argv[1]
@ -68,6 +69,8 @@ function history --description "display or manipulate interactive command histor
case --merge case --merge
__fish_set_hist_cmd merge __fish_set_hist_cmd merge
or return or return
case -C --case_sensitive
set case_sensitive --case-sensitive
case -h --help case -h --help
builtin history --help builtin history --help
return return
@ -121,14 +124,13 @@ function history --description "display or manipulate interactive command histor
test -z "$search_mode" test -z "$search_mode"
and set search_mode "--contains" and set search_mode "--contains"
echo "builtin history search $search_mode $show_time $max_count -- $argv" >>/tmp/x
if isatty stdout if isatty stdout
set -l pager less set -l pager less
set -q PAGER set -q PAGER
and set pager $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 else
builtin history search $search_mode $show_time $max_count -- $argv builtin history search $search_mode $show_time $max_count $case_sensitive -- $argv
end end
case delete # interactively delete history case delete # interactively delete history
@ -139,16 +141,16 @@ function history --description "display or manipulate interactive command histor
end end
test -z "$search_mode" test -z "$search_mode"
and set search_mode "--exact" and set search_mode "--contains"
if test $search_mode = "--exact" if test $search_mode = "--exact"
builtin history delete $search_mode $argv builtin history delete $search_mode $case_sensitive $argv
return return
end end
# TODO: Fix this so that requesting history entries with a timestamp works: # 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 $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] if set -q found_items[1]
set -l found_items_count (count $found_items) set -l found_items_count (count $found_items)
for i in (seq $found_items_count) for i in (seq $found_items_count)
@ -169,11 +171,8 @@ function history --description "display or manipulate interactive command histor
if test "$choice" = "all" if test "$choice" = "all"
printf "Deleting all matching entries!\n" 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 for item in $found_items
builtin history delete --exact -- $item builtin history delete --exact --case-sensitive -- $item
end end
builtin history save builtin history save
return return
@ -188,7 +187,7 @@ function history --description "display or manipulate interactive command histor
end end
printf "Deleting history entry %s: \"%s\"\n" $i $found_items[$i] 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 end
builtin history save builtin history save
end 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; long max_items = LONG_MAX;
bool history_search_type_defined = false; bool history_search_type_defined = false;
const wchar_t *show_time_format = NULL; 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 // 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). // 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'}, const struct woption long_options[] = {{L"prefix", no_argument, NULL, 'p'},
{L"contains", no_argument, NULL, 'c'}, {L"contains", no_argument, NULL, 'c'},
{L"help", no_argument, NULL, 'h'}, {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"with-time", optional_argument, NULL, 't'},
{L"exact", no_argument, NULL, 'e'}, {L"exact", no_argument, NULL, 'e'},
{L"max", required_argument, NULL, 'n'}, {L"max", required_argument, NULL, 'n'},
{L"case-sensitive", no_argument, 0, 'C'},
{L"delete", no_argument, NULL, 1}, {L"delete", no_argument, NULL, 1},
{L"search", no_argument, NULL, 2}, {L"search", no_argument, NULL, 2},
{L"save", no_argument, NULL, 3}, {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; break;
} }
case 'C': {
case_sensitive = true;
break;
}
case 'p': { case 'p': {
search_type = HISTORY_SEARCH_TYPE_PREFIX; search_type = HISTORY_SEARCH_TYPE_PREFIX;
history_search_type_defined = true; 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; int status = STATUS_BUILTIN_OK;
switch (hist_cmd) { switch (hist_cmd) {
case HIST_SEARCH: { 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; status = STATUS_BUILTIN_ERROR;
} }
break; break;
} }
case HIST_DELETE: { case HIST_DELETE: {
// TODO: Move this code to the history module and support the other search types. At // TODO: Move this code to the history module and support the other search types
// this time we expect the non-exact deletions to be handled only by the history // including case-insensitive matches. At this time we expect the non-exact deletions to
// function's interactive delete feature. // be handled only by the history function's interactive delete feature.
if (search_type != HISTORY_SEARCH_TYPE_EXACT) { if (search_type != HISTORY_SEARCH_TYPE_EXACT) {
streams.err.append_format(_(L"builtin history delete only supports --exact\n")); streams.err.append_format(_(L"builtin history delete only supports --exact\n"));
status = STATUS_BUILTIN_ERROR; status = STATUS_BUILTIN_ERROR;
break; 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) { for (wcstring_list_t::const_iterator iter = args.begin(); iter != args.end(); ++iter) {
wcstring delete_string = *iter; wcstring delete_string = *iter;
if (delete_string[0] == '"' && delete_string[delete_string.length() - 1] == '"') { 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); \ if (!(e)) err(L"Test failed on line %lu: %s", __LINE__, #e); \
} while (0) } 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) \ #define do_test1(e, msg) \
do { \ do { \
if (!(e)) err(L"Test failed on line %lu: %ls", __LINE__, (msg)); \ 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() { static void test_autosuggestion_ignores() {
say(L"Testing scenarios that should produce no autosuggestions"); 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. // 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__);
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"); 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; size_t i;
for (i = 0; i < matches; i++) { for (i = 0; i < matches; i++) {
do_test(search.go_backwards()); do_test(search.go_backwards());
wcstring item = search.current_string(); 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++) { 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) { 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) { void history_tests_t::test_history(void) {
history_search_t searcher;
say(L"Testing history"); say(L"Testing history");
history_t &history = history_t::history_with_name(L"test_history"); history_t &history = history_t::history_with_name(L"test_history");
history.clear(); history.clear();
history.add(L"Gamma"); history.add(L"Gamma");
history.add(L"beta");
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"Alpha");
history.add(L"alph");
history.add(L"ALPH");
history.add(L"ZZZ");
// All three items match "a". // Items matching "a", case-sensitive.
history_search_t search1(history, L"a"); searcher = history_search_t(history, L"a");
test_history_matches(search1, 3); test_history_matches(searcher, 6, __LINE__);
do_test(search1.current_string() == L"Alpha"); do_test(searcher.current_string() == L"alph");
// One item matches "et". // Items matching "alpha", case-insensitive. Note that HISTORY_SEARCH_TYPE_CONTAINS but we have
history_search_t search2(history, L"et"); // to explicitly specify it in order to be able to pass false for the case_sensitive parameter.
test_history_matches(search2, 1); searcher = history_search_t(history, L"AlPhA", HISTORY_SEARCH_TYPE_CONTAINS, false);
do_test(search2.current_string() == L"Beta"); 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.remove(L"Alpha");
history_search_t search3(history, L"Alpha"); searcher = history_search_t(history, L"Alpha");
test_history_matches(search3, 0); test_history_matches(searcher, 0, __LINE__);
// Test history escaping and unescaping, yaml, etc. // Test history escaping and unescaping, yaml, etc.
history_item_list_t before, after; history_item_list_t before, after;

View file

@ -428,27 +428,52 @@ bool history_item_t::merge(const history_item_t &item) {
return result; return result;
} }
#if 0
history_item_t::history_item_t(const wcstring &str) 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) 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 { bool history_item_t::matches_search(const wcstring &term, enum history_search_type_t type,
switch (type) { bool case_sensitive) const {
case HISTORY_SEARCH_TYPE_CONTAINS: { // 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; return contents.find(term) != wcstring::npos;
} } else if (type == HISTORY_SEARCH_TYPE_PREFIX) {
case HISTORY_SEARCH_TYPE_PREFIX: {
// We consider equal strings to match a prefix search, so that autosuggest will allow
// suggesting what you've typed.
return string_prefixes_string(term, contents); return string_prefixes_string(term, contents);
} }
case HISTORY_SEARCH_TYPE_EXACT: { } else {
return term == contents; 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. /// 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); this->add(history_item_t(str, when, ident), pending);
} }
void history_t::remove(const wcstring &str) { bool icompare_pred(wchar_t a, wchar_t b) { return std::tolower(a) == std::tolower(b); }
// Add to our list of deleted items.
deleted_items.insert(str); 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(); size_t idx = new_items.size();
while (idx--) { 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); new_items.erase(new_items.begin() + idx);
// If this index is before our first_unwritten_new_item_index, then subtract one from // 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 // 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. // 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. // Look for a term that matches and that we haven't seen before.
const wcstring &str = item.str(); 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)) { !should_skip_match(str)) {
prev_matches.push_back(prev_match_t(idx, item)); prev_matches.push_back(prev_match_t(idx, item));
return true; 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, 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); // scoped_lock locker(lock);
if (search_args.empty()) { if (search_args.empty()) {
// Start at one because zero is the current command. // 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"); streams.err.append_format(L"Searching for the empty string isn't allowed");
return false; 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()) { while (searcher.go_backwards()) {
if (!format_history_record(searcher.current_item(), show_time_format, streams)) { if (!format_history_record(searcher.current_item(), show_time_format, streams)) {
return false; return false;

View file

@ -59,7 +59,8 @@ class history_item_t {
bool merge(const history_item_t &item); bool merge(const history_item_t &item);
// The actual contents of the entry. // 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. // Original creation time for the entry.
time_t creation_timestamp; time_t creation_timestamp;
@ -71,15 +72,16 @@ class history_item_t {
path_list_t required_paths; path_list_t required_paths;
public: public:
explicit history_item_t(const wcstring &str); explicit history_item_t(const wcstring &str, time_t when = 0, history_identifier_t ident = 0);
explicit history_item_t(const wcstring &, time_t, history_identifier_t ident = 0);
const wcstring &str() const { return contents; } const wcstring &str() const { return contents; }
const wcstring &str_lower() const { return contents_lower; }
bool empty() const { return contents.empty(); } bool empty() const { return contents.empty(); }
// Whether our contents matches a search term. // 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; } time_t timestamp() const { return creation_timestamp; }
@ -227,7 +229,8 @@ class history_t {
// Searches history. // Searches history.
bool search(history_search_type_t search_type, wcstring_list_t search_args, 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! // Enable / disable automatic saving. Main thread only!
void disable_automatic_saving(); void disable_automatic_saving();
@ -264,6 +267,7 @@ class history_search_t {
// Our type. // Our type.
enum history_search_type_t search_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. // 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; typedef std::pair<size_t, history_item_t> prev_match_t;
@ -310,11 +314,19 @@ class history_search_t {
// Constructor. // Constructor.
history_search_t(history_t &hist, const wcstring &str, history_search_t(history_t &hist, const wcstring &str,
enum history_search_type_t type = HISTORY_SEARCH_TYPE_CONTAINS) enum history_search_type_t type = HISTORY_SEARCH_TYPE_CONTAINS,
: history(&hist), search_type(type), term(str) {} 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. // 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 // 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. # Delete a single command we recently ran.
send "history delete 'echo hello'\r" send "history delete -e -C 'echo hello'\r"
expect_prompt -re {history delete 'echo hello'\r\n} { 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" puts "history function explicit exact delete 'echo hello' succeeded"
} unmatched { } unmatched {
puts stderr "history function explicit exact delete 'echo hello' failed" 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 {Enter nothing to cancel.*\r\nEnter "all" to delete all the matching entries\.\r\n}
expect -re {Delete which entries\? >} expect -re {Delete which entries\? >}
send "1\r" send "1\r"
expect_prompt -re {Deleting history entry 1: "echo hello AGAIN"\r\n} { expect -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"
}
# Verify that the deleted history entry is gone and the other one that matched # Verify that the deleted history entry is gone and the other one that matched
# the prefix search above is still there. # the prefix search above is still there.
send "history search --exact 'echo hello again'\r" send "echo count AGAIN (history search -e -C 'echo hello AGAIN' | wc -l | string trim)\r"
expect_prompt -re {\r\necho hello again\r\n} { 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" puts "history function explicit exact search 'echo hello again' succeeded"
} unmatched { } unmatched {
puts stderr "history function explicit exact search 'echo hello again' failed" 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 hello' succeeded
history function explicit exact search 'echo hell' succeeded history function explicit exact search 'echo hell' succeeded
history function explicit exact delete 'echo hello' 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' succeeded
history function explicit exact search 'echo hello AGAIN' failed to find the entry