correct handling of history args

This fixes several problems with how the builtin `history` command handles
arguments. It now complains and refuses to do anything if the user specifies
incompatible actions (e.g., `--search` and `--clear`). It also fixes a
regression introduced by previous changes with regard to invocations that
don't explicitly specify `--search` or a search term.

Enhances the history man page to clarify the behavior of various options.

This change is already far larger than I like so unit tests will be added
in a separate commit.

Fixes #3224.

Note: This fixes only a couple problems with the interactive `history
--delete` command in the `history` function. The main problem will be
dealt with via issue #31.
This commit is contained in:
Kurtis Rader 2016-07-13 22:33:50 -07:00
parent 4fbc476b19
commit b53f42970c
10 changed files with 398 additions and 246 deletions

View file

@ -2,12 +2,11 @@
\subsection history-synopsis Synopsis \subsection history-synopsis Synopsis
\fish{synopsis} \fish{synopsis}
history ( -s | --search ) [ -t | --with-time ] [ -p | --prefix | -c | --contains ] [ "search string"... ]
history ( -d | --delete ) [ -t | --with-time ] [ -p | --prefix | -c | --contains ] "search string"...
history ( -m | --merge ) history ( -m | --merge )
history ( -s | --save ) history ( -s | --save )
history ( -l | --clear ) history ( -l | --clear )
history ( -s | --search ) [ -t | --with-time ] [ -p "prefix string" | --prefix "prefix string" | -c "search string | --contains "search string" ]
history ( -d | --delete ) [ -t | --with-time ] [ -p "prefix string" | --prefix "prefix string" | -c "search string | --contains "search string" ]
history ( -t | --with-time )
history ( -h | --help ) history ( -h | --help )
\endfish \endfish
@ -15,23 +14,25 @@ history ( -h | --help )
`history` is used to list, search and delete the history of commands used. `history` is used to list, search and delete the history of commands used.
The following commands are available:
- `-s` or `--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. 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.
- `-d` or `--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.
- `-m` or `--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.
- `-v` or `--save` saves all changes in the history file. The shell automatically saves the history file; this option is provided for internal use.
- `-l` or `--clear` clears the history file. A prompt is displayed before the history is erased asking you to confirm you really want to clear all history.
The following options are available: The following options are available:
- `--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. - `-p` or `--prefix` searches or deletes items in the history that begin with the specified text string.
- `--save` saves all changes in the history file. The shell automatically saves the history file; this option is provided for internal use. - `-c` or `--contains` searches or deletes items in the history that contain the specified text string. This is the default.
- `--clear` clears the history file. A prompt is displayed before the history is erased. - `-t` or `--with-time` prefixes the output of each displayed history entry with the time it was recorded in the format "%Y-%m-%d %H:%M:%S" in your local timezone.
- `--search` returns history items in keeping with the `--prefix` or `--contains` options. Without either, `--contains` will be assumed.
- `--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.
- `--prefix` searches or deletes items in the history that begin with the specified text string.
- `--contains` searches or deletes items in the history that contain the specified text string.
- `--with-time` prefixes the output of each displayed history entry with the time it was recorded in the format "%Y-%m-%d %H:%M:%S" in your local timezone.
\subsection history-examples Example \subsection history-examples Example
@ -43,5 +44,11 @@ history --search --contains "foo"
# Outputs a list of all previous commands containing the string "foo". # Outputs a list of all previous commands containing the string "foo".
history --delete --prefix "foo" history --delete --prefix "foo"
# Interactively deletes the record of previous commands which start with "foo". # Interactively deletes commands which start with "foo" from the history.
# You can select more than one entry by entering their IDs seperated by a space.
\subsection history-notes Notes
If you specify both `--prefix` and `--contains` the last flag seen is used.
\endfish \endfish

View file

@ -2,140 +2,124 @@
# Wrap the builtin history command to provide additional functionality. # Wrap the builtin history command to provide additional functionality.
# #
function history --shadow-builtin --description "display or manipulate interactive command history" function history --shadow-builtin --description "display or manipulate interactive command history"
# no args or just -t? use pager if interactive. set -l cmd
set -l cmd search set -l search_mode --contains
set -l prefix_args "" set -l with_time
set -l contains_args ""
set -l search_mode none
set -l time_args
for i in (seq (count $argv)) # The "set cmd $cmd xyz" lines are to make it easy to detect if the user specifies more than one
if set -q argv[$i] # subcommand.
switch $argv[$i] while set -q argv[1]
case -d --delete switch $argv[1]
set cmd delete case -d --delete
case -v --save set cmd $cmd delete
set cmd save case -v --save
case -l --clear set cmd $cmd save
set cmd clear case -l --clear
case -s --search set cmd $cmd clear
set cmd search case -s --search
case -m --merge set cmd $cmd search
set cmd merge case -m --merge
case -h --help set cmd $cmd merge
set cmd help case -h --help
case -t --with-time set cmd $cmd help
set time_args "-t" case -t --with-time
case -p --prefix set with_time -t
set search_mode prefix case -p --prefix
set prefix_args $argv[(math $i + 1)] set search_mode --prefix
case -c --contains case -c --contains
set search_mode contains set search_mode --contains
set contains_args $argv[(math $i + 1)] case --
case -- set -e argv[1]
set -e argv[1..$i] break
break case '*'
case "-*" "--*" break
printf ( _ "%s: invalid option -- %s\n" ) history $argv[$i] >&2
return 1
end
end end
set -e argv[1]
end
if not set -q cmd[1]
set cmd search # default to "search" if the user didn't explicitly specify a command
else if set -q cmd[2]
printf (_ "You cannot specify multiple commands: %s\n") "$cmd"
return 1
end end
switch $cmd switch $cmd
case search case search
if set -q argv[1] if isatty stdout
or test -n $time_args
and contains $search_mode none
set -l pager less set -l pager less
set -q PAGER set -q PAGER
and set pager $PAGER and set pager $PAGER
builtin history $time_args | eval $pager builtin history --search $search_mode $with_time -- $argv | eval $pager
else else
builtin history $time_args $argv builtin history --search $search_mode $with_time -- $argv
end
return
case delete
# Interactively delete history
set -l found_items ""
switch $search_mode
case prefix:
set found_items (builtin history --search --prefix $prefix_args)
case contains
set found_items (builtin history --search --contains $contains_args)
case none
builtin history $argv
# Save changes after deleting item.
builtin history --save
return 0
end end
set found_items_count (count $found_items) case delete # Interactively delete history
if test $found_items_count -gt 0 # TODO: Fix this to deal with history entries that have multiple lines.
echo "[0] cancel" if not set -q argv[1]
echo "[1] all" printf "You have to specify at least one search term to find entries to delete" >&2
echo return 1
end
# TODO: Fix this so that requesting history entries with a timestamp works:
# set -l found_items (builtin history --search $search_mode $with_time -- $argv)
set -l found_items (builtin history --search $search_mode -- $argv)
if set -q found_items[1]
set -l found_items_count (count $found_items)
for i in (seq $found_items_count) for i in (seq $found_items_count)
printf "[%s] %s \n" (math $i + 1) $found_items[$i] printf "[%s] %s\n" $i $found_items[$i]
end end
echo ""
echo "Enter nothing to cancel the delete, or"
echo "Enter one or more of the entry IDs separated by a space, or"
echo "Enter \"all\" to delete all the matching entries."
echo ""
read --local --prompt "echo 'Delete which entries? > '" choice read --local --prompt "echo 'Delete which entries? > '" choice
set choice (string split " " -- $choice) echo ''
for i in $choice if test -z "$choice"
printf "Cancelling the delete!\n"
# Skip empty input, for example, if the user just hits return return
if test -z $i end
continue
end if test "$choice" = "all"
printf "Deleting all matching entries!\n"
# Following two validations could be embedded with "and" but I find the syntax builtin history --delete $search_mode -- $argv
# kind of weird. builtin history --save
if not string match -qr '^[0-9]+$' $i return
printf "Invalid input: %s\n" $i end
continue
end for i in (string split " " -- $choice)
if test -z "$i"
if test $i -gt (math $found_items_count + 1) or not string match -qr '^[1-9][0-9]*$' -- $i
printf "Invalid input : %s\n" $i or test $i -gt $found_items_count
continue printf "Ignoring invalid history entry ID \"%s\"\n" $i
end continue
end
if test $i = "0"
printf "Cancel\n" printf "Deleting history entry %s: \"%s\"\n" $i $found_items[$i]
return builtin history --delete "$found_items[$i]"
else
if test $i = "1"
for item in $found_items
builtin history --delete $item
end
printf "Deleted all!\n"
else
builtin history --delete $found_items[(math $i - 1)]
end
end
end end
# Save changes after deleting item(s).
builtin history --save builtin history --save
end end
case save case save
# Save changes to history file. builtin history --save -- $argv
builtin history $argv
case merge case merge
builtin history --merge builtin history --merge -- $argv
case help case help
builtin history --help builtin history --help
case clear case clear
# Erase the entire history. # Erase the entire history.
echo "Are you sure you want to clear history ? (y/n)" read --local --prompt "echo 'Are you sure you want to clear history? (y/n) '" choice
read ch if test "$choice" = "y"
if test $ch = "y" or test "$choice" = "yes"
builtin history $argv builtin history --clear -- $argv
echo "History cleared!" and echo "History cleared!"
end end
end end
end end

View file

@ -2814,88 +2814,113 @@ static int builtin_return(parser_t &parser, io_streams_t &streams, wchar_t **arg
return status; return status;
} }
// Formats a single history record, including a trailing newline. Returns true enum hist_cmd_t { HIST_NOOP, HIST_SEARCH, HIST_DELETE, HIST_CLEAR, HIST_MERGE, HIST_SAVE };
// if bytes were written to the output stream and false otherwise.
static bool format_history_record(const history_item_t &item, const bool with_time,
output_stream_t *const out) {
if (with_time) {
const time_t seconds = item.timestamp();
struct tm timestamp;
if (!localtime_r(&seconds, &timestamp)) {
return false;
}
char timestamp_string[32];
if (strftime(timestamp_string, 32, "%c ", &timestamp) != 31) {
out->append(str2wcstring(timestamp_string));
}
else {
return false;
}
static const wcstring hist_cmd_to_string(hist_cmd_t hist_cmd) {
switch (hist_cmd) {
case HIST_NOOP:
return L"no-op";
case HIST_SEARCH:
return L"search";
case HIST_DELETE:
return L"delete";
case HIST_CLEAR:
return L"clear";
case HIST_MERGE:
return L"merge";
case HIST_SAVE:
return L"save";
default:
DIE("Unhandled history command");
} }
out->append(item.str()); }
out->append(L"\n");
/// 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_NOOP) {
wchar_t err_text[1024];
swprintf(err_text, sizeof(err_text) / sizeof(wchar_t),
_(L"You cannot do both '%ls' and '%ls' in the same '%ls' invocation\n"),
hist_cmd_to_string(*hist_cmd).c_str(), hist_cmd_to_string(sub_cmd).c_str(), cmd);
streams.err.append_format(BUILTIN_ERR_COMBO2, cmd, err_text);
return false;
}
*hist_cmd = sub_cmd;
return true; return true;
} }
/// History of commands executed by user. #define CHECK_FOR_UNEXPECTED_HIST_ARGS() \
static int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **argv) { if (args.size() != 0) { \
int argc = builtin_count_args(argv); streams.err.append_format(BUILTIN_ERR_ARG_COUNT, cmd, \
hist_cmd_to_string(hist_cmd).c_str(), 0, args.size()); \
status = STATUS_BUILTIN_ERROR; \
break; \
}
bool search_history = false; /// Manipulate history of interactive commands executed by the user.
bool delete_item = false; static int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
bool search_prefix = false; wchar_t *cmd = argv[0];
bool save_history = false; ;
bool clear_history = false; int argc = builtin_count_args(argv);
bool merge_history = false; hist_cmd_t hist_cmd = HIST_NOOP;
history_search_type_t search_type = HISTORY_SEARCH_TYPE_CONTAINS;
bool with_time = false; bool with_time = false;
static const struct woption long_options[] = { static const struct woption long_options[] = {
{L"delete", no_argument, 0, 'd'}, {L"search", no_argument, 0, 's'}, {L"delete", no_argument, 0, 'd'}, {L"search", no_argument, 0, 's'},
{L"prefix", no_argument, 0, 'p'}, {L"contains", no_argument, 0, 'c'}, {L"prefix", no_argument, 0, 'p'}, {L"contains", no_argument, 0, 'c'},
{L"save", no_argument, 0, 'v'}, {L"clear", no_argument, 0, 'l'}, {L"save", no_argument, 0, 'v'}, {L"clear", no_argument, 0, 'l'},
{L"merge", no_argument, 0, 'm'}, {L"help", no_argument, 0, 'h'}, {L"merge", no_argument, 0, 'm'}, {L"help", no_argument, 0, 'h'},
{L"with-time", no_argument, 0, 't'}, {0, 0, 0, 0}}; {L"with-time", no_argument, 0, 't'}, {0, 0, 0, 0}};
int opt = 0;
int opt_index = 0;
wgetopter_t w;
history_t *history = reader_get_history(); history_t *history = reader_get_history();
// Use the default history if we have none (which happens if invoked non-interactively, e.g. // Use the default history if we have none (which happens if invoked non-interactively, e.g.
// from webconfig.py. // from webconfig.py.
if (!history) history = &history_t::history_with_name(L"fish"); if (!history) history = &history_t::history_with_name(L"fish");
while ((opt = w.wgetopt_long(argc, argv, L"dspcvlmht", long_options, &opt_index)) != EOF) { int opt = 0;
int opt_index = 0;
wgetopter_t w;
while ((opt = w.wgetopt_long(argc, argv, L"+dspcvlmht", long_options, &opt_index)) != EOF) {
switch (opt) { switch (opt) {
case 'p': {
search_history = true;
search_prefix = true;
break;
}
case 'd': {
delete_item = true;
break;
}
case 's': { case 's': {
search_history = true; if (!set_hist_cmd(cmd, &hist_cmd, HIST_SEARCH, streams)) {
break; return STATUS_BUILTIN_ERROR;
} }
case 'c': {
search_history = true;
break;
}
case 'v': {
save_history = true;
break;
}
case 'l': {
clear_history = true;
break; break;
} }
case 'm': { case 'm': {
merge_history = true; if (!set_hist_cmd(cmd, &hist_cmd, HIST_MERGE, streams)) {
return STATUS_BUILTIN_ERROR;
}
break;
}
case 'v': {
if (!set_hist_cmd(cmd, &hist_cmd, HIST_SAVE, streams)) {
return STATUS_BUILTIN_ERROR;
}
break;
}
case 'd': {
if (!set_hist_cmd(cmd, &hist_cmd, HIST_DELETE, streams)) {
return STATUS_BUILTIN_ERROR;
}
break;
}
case 'l': {
if (!set_hist_cmd(cmd, &hist_cmd, HIST_CLEAR, streams)) {
return STATUS_BUILTIN_ERROR;
}
break;
}
case 'p': {
search_type = HISTORY_SEARCH_TYPE_PREFIX;
break;
}
case 'c': {
search_type = HISTORY_SEARCH_TYPE_CONTAINS;
break; break;
} }
case 't': { case 't': {
@ -2903,85 +2928,66 @@ static int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **ar
break; break;
} }
case 'h': { case 'h': {
builtin_print_help(parser, streams, argv[0], streams.out); builtin_print_help(parser, streams, cmd, streams.out);
return STATUS_BUILTIN_OK; return STATUS_BUILTIN_OK;
break;
} }
case '?': { case '?': {
streams.err.append_format(BUILTIN_ERR_UNKNOWN, argv[0], argv[w.woptind - 1]); streams.err.append_format(BUILTIN_ERR_UNKNOWN, cmd, argv[w.woptind - 1]);
return STATUS_BUILTIN_ERROR; return STATUS_BUILTIN_ERROR;
break;
} }
default: { default: {
streams.err.append_format(BUILTIN_ERR_UNKNOWN, argv[0], argv[w.woptind - 1]); streams.err.append_format(BUILTIN_ERR_UNKNOWN, cmd, argv[w.woptind - 1]);
return STATUS_BUILTIN_ERROR; return STATUS_BUILTIN_ERROR;
} }
} }
} }
// Everything after is an argument. // Everything after the flags is an argument for a subcommand (e.g., a search term).
const wcstring_list_t args(argv + w.woptind, argv + argc); const wcstring_list_t args(argv + w.woptind, argv + argc);
if (merge_history) {
history->incorporate_external_changes();
return STATUS_BUILTIN_OK;
}
else if (search_history) { if (hist_cmd == HIST_NOOP) hist_cmd = HIST_SEARCH;
int res = STATUS_BUILTIN_ERROR;
for (wcstring_list_t::const_iterator iter = args.begin(); iter != args.end(); ++iter) { int status = STATUS_BUILTIN_OK;
const wcstring &search_string = *iter; switch (hist_cmd) {
if (search_string.empty()) { case HIST_SEARCH: {
streams.err.append_format(BUILTIN_ERR_COMBO2, argv[0], if (!history->search(search_type, args, with_time, streams)) {
L"Use --search with either --contains or --prefix"); status = STATUS_BUILTIN_ERROR;
return res;
} }
break;
}
case HIST_DELETE: {
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_search_t searcher = history_search_t( history->remove(delete_string);
*history, search_string,
search_prefix ? HISTORY_SEARCH_TYPE_PREFIX : HISTORY_SEARCH_TYPE_CONTAINS);
while (searcher.go_backwards()) {
if (!format_history_record(searcher.current_item(), with_time, &streams.out)) {
return STATUS_BUILTIN_ERROR;
}
res = STATUS_BUILTIN_OK;
} }
break;
} }
return res; case HIST_CLEAR: {
} CHECK_FOR_UNEXPECTED_HIST_ARGS();
history->clear();
else if (delete_item) { history->save();
for (wcstring_list_t::const_iterator iter = args.begin(); iter != args.end(); ++iter) { break;
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);
} }
return STATUS_BUILTIN_OK; case HIST_MERGE: {
} CHECK_FOR_UNEXPECTED_HIST_ARGS();
history->incorporate_external_changes();
else if (save_history) { break;
history->save(); }
return STATUS_BUILTIN_OK; case HIST_SAVE: {
} CHECK_FOR_UNEXPECTED_HIST_ARGS();
history->save();
else if (clear_history) { break;
history->clear(); }
history->save(); default: {
return STATUS_BUILTIN_OK; DIE("Unhandled history command");
} break;
else if (argc - w.woptind == 0) {
for (int i = 1; !history->item_at_index(i).empty(); ++i) {
if (!format_history_record(history->item_at_index(i), with_time, &streams.out)) {
return STATUS_BUILTIN_ERROR;
}
} }
return STATUS_BUILTIN_OK;
} }
return STATUS_BUILTIN_ERROR; return status;
} }
#if 0 #if 0

View file

@ -33,6 +33,9 @@ enum { COMMAND_NOT_BUILTIN, BUILTIN_REGULAR, BUILTIN_FUNCTION };
// Error message for unknown switch. // Error message for unknown switch.
#define BUILTIN_ERR_UNKNOWN _(L"%ls: Unknown option '%ls'\n") #define BUILTIN_ERR_UNKNOWN _(L"%ls: Unknown option '%ls'\n")
// Error message for unexpected args.
#define BUILTIN_ERR_ARG_COUNT _(L"%ls: %ls command expected %d args, got %d\n")
// Error message for invalid character in variable name. // Error message for invalid character in variable name.
#define BUILTIN_ERR_VARCHAR \ #define BUILTIN_ERR_VARCHAR \
_(L"%ls: Invalid character '%lc' in variable name. Only alphanumerical characters and " \ _(L"%ls: Invalid character '%lc' in variable name. Only alphanumerical characters and " \

View file

@ -218,6 +218,14 @@ extern bool has_working_tty_timestamps;
exit_without_destructors(1); \ exit_without_destructors(1); \
} }
/// Exit program at once after emitting an error message.
#define DIE(msg) \
{ \
fprintf(stderr, "fish: %s on line %ld of file %s, shutting down fish\n", msg, \
(long)__LINE__, __FILE__); \
FATAL_EXIT(); \
}
/// Exit program at once, leaving an error message about running out of memory. /// Exit program at once, leaving an error message about running out of memory.
#define DIE_MEM() \ #define DIE_MEM() \
{ \ { \

View file

@ -23,6 +23,7 @@
#include "env.h" #include "env.h"
#include "fallback.h" // IWYU pragma: keep #include "fallback.h" // IWYU pragma: keep
#include "history.h" #include "history.h"
#include "io.h"
#include "iothread.h" #include "iothread.h"
#include "lru.h" #include "lru.h"
#include "parse_constants.h" #include "parse_constants.h"
@ -1396,6 +1397,51 @@ void history_t::save(void) {
this->save_internal(false); this->save_internal(false);
} }
// Formats a single history record, including a trailing newline. Returns true
// if bytes were written to the output stream and false otherwise.
static bool format_history_record(const history_item_t &item, const bool with_time,
io_streams_t &streams) {
if (with_time) {
const time_t seconds = item.timestamp();
struct tm timestamp;
if (!localtime_r(&seconds, &timestamp)) return false;
char timestamp_string[22];
if (strftime(timestamp_string, 22, "%Y-%m-%d %H:%M:%S ", &timestamp) != 21) return false;
streams.out.append(str2wcstring(timestamp_string));
}
streams.out.append(item.str());
streams.out.append(L"\n");
return true;
}
bool history_t::search(history_search_type_t search_type, wcstring_list_t search_args, bool with_time, io_streams_t &streams) {
// scoped_lock locker(lock); //!OCLINT(side-effect)
if (search_args.empty()) {
// Start at one because zero is the current command.
for (int i = 1; !this->item_at_index(i).empty(); ++i) {
if (!format_history_record(this->item_at_index(i), with_time, streams)) return false;
}
return true;
}
for (wcstring_list_t::const_iterator iter = search_args.begin(); iter != search_args.end();
++iter) {
const wcstring &search_string = *iter;
if (search_string.empty()) {
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);
while (searcher.go_backwards()) {
if (!format_history_record(searcher.current_item(), with_time, streams)) {
return false;
}
}
}
return true;
}
void history_t::disable_automatic_saving() { void history_t::disable_automatic_saving() {
scoped_lock locker(lock); //!OCLINT(side-effect) scoped_lock locker(lock); //!OCLINT(side-effect)
disable_automatic_save_counter++; disable_automatic_save_counter++;

View file

@ -17,6 +17,8 @@
#include "common.h" #include "common.h"
#include "wutil.h" // IWYU pragma: keep #include "wutil.h" // IWYU pragma: keep
struct io_streams_t;
// Fish supports multiple shells writing to history at once. Here is its strategy: // Fish supports multiple shells writing to history at once. Here is its strategy:
// //
// 1. All history files are append-only. Data, once written, is never modified. // 1. All history files are append-only. Data, once written, is never modified.
@ -193,7 +195,7 @@ class history_t {
public: public:
explicit history_t(const wcstring &); // constructor explicit history_t(const wcstring &); // constructor
~history_t(); // desctructor ~history_t(); // destructor
// Returns history with the given name, creating it if necessary. // Returns history with the given name, creating it if necessary.
static history_t &history_with_name(const wcstring &name); static history_t &history_with_name(const wcstring &name);
@ -221,6 +223,10 @@ class history_t {
// Saves history. // Saves history.
void save(); void save();
// Searches history.
bool search(history_search_type_t search_type, wcstring_list_t search_args,
bool with_time, 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();
void enable_automatic_saving(); void enable_automatic_saving();
@ -250,6 +256,7 @@ class history_t {
}; };
class history_search_t { class history_search_t {
private:
// The history in which we are searching. // The history in which we are searching.
history_t *history; history_t *history;

85
tests/history.expect Normal file
View file

@ -0,0 +1,85 @@
# vim: set filetype=expect:
#
# This is a very fragile test. Sorry about that. But interactively entering
# commands and verifying they are recorded correctly in the interactive
# history and that history can be manipulated is inherently difficult.
#
# This is meant to verify just a few of the most basic behaviors of the
# interactive history to hopefully keep regressions from happening. It is not
# meant to be a comprehensive test of the history subsystem. Those types of
# tests belong in the src/fish_tests.cpp module.
#
# The history function might pipe output through the user's pager. We don't
# want something like `less` to complicate matters so force the use of `cat`.
set ::env(PAGER) cat
spawn $fish
expect_prompt
# ==========
# Start by ensuring we're not affected by earlier tests. Clear the history.
send "builtin history --clear\r"
expect_prompt
# ==========
# The following tests verify the behavior of the history builtin.
# ==========
# ==========
# List our history which should be empty after just clearing it.
send "echo start1; builtin history; echo end1\r"
expect_prompt -re {start1\r\nend1\r\n} {
puts "empty history detected as expected"
} unmatched {
puts stderr "empty history not detected as expected"
}
# ==========
# Our history should now contain the previous command and nothing else.
send "echo start2; builtin history; echo end2\r"
expect_prompt -re {start2\r\necho start1; builtin history; echo end1\r\nend2\r\n} {
puts "first history command detected as expected"
} unmatched {
puts stderr "first history command not detected as expected"
}
# ==========
# Verify asking for two different actions produces an error.
send "builtin history --search --merge\r"
expect_prompt -re {\r\nYou cannot do both 'search' and 'merge' in the same 'history' invocation\r\n} {
puts "invalid attempt at multiple history commands detected"
} unmatched {
puts stderr "invalid attempt at multiple history commands not detected"
}
# ==========
# The following tests verify the behavior of the history function.
# ==========
# ==========
# Verify explicit searching for the first two commands in the previous tests
# returns the expected results.
send "history --search echo start\r"
expect_prompt -re {\r\necho start1.*\r\necho start2} {
puts "history function explicit search succeeded"
} unmatched {
puts stderr "history function explicit search failed"
}
# ==========
# Verify searching is the implicit action.
send "history echo start\r"
expect_prompt -re {\r\necho start1.*\r\necho start2} {
puts "history function implicit search succeeded"
} unmatched {
puts stderr "history function implicit search failed"
}
# ==========
# Verify implicit searching with a request for timestamps includes the timestamps.
send "history -t echo start\r"
expect_prompt -re {\r\n\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d echo start1; builtin history;.*\r\n\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d echo start2; builtin history} {
puts "history function implicit search with timestamps succeeded"
} unmatched {
puts stderr "history function implicit search with timestamps failed"
}

0
tests/history.expect.err Normal file
View file

6
tests/history.expect.out Normal file
View file

@ -0,0 +1,6 @@
empty history detected as expected
first history command detected as expected
invalid attempt at multiple history commands detected
history function explicit search succeeded
history function implicit search succeeded
history function implicit search with timestamps succeeded