mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-25 04:13:08 +00:00
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:
parent
4fbc476b19
commit
b53f42970c
10 changed files with 398 additions and 246 deletions
|
@ -2,12 +2,11 @@
|
|||
|
||||
\subsection history-synopsis 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 ( -s | --save )
|
||||
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 )
|
||||
\endfish
|
||||
|
||||
|
@ -15,23 +14,25 @@ history ( -h | --help )
|
|||
|
||||
`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:
|
||||
|
||||
- `--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.
|
||||
|
||||
- `--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.
|
||||
- `-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.
|
||||
|
||||
\subsection history-examples Example
|
||||
|
||||
|
@ -43,5 +44,11 @@ history --search --contains "foo"
|
|||
# Outputs a list of all previous commands containing the string "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
|
||||
|
|
|
@ -2,140 +2,124 @@
|
|||
# Wrap the builtin history command to provide additional functionality.
|
||||
#
|
||||
function history --shadow-builtin --description "display or manipulate interactive command history"
|
||||
# no args or just -t? use pager if interactive.
|
||||
set -l cmd search
|
||||
set -l prefix_args ""
|
||||
set -l contains_args ""
|
||||
set -l search_mode none
|
||||
set -l time_args
|
||||
set -l cmd
|
||||
set -l search_mode --contains
|
||||
set -l with_time
|
||||
|
||||
for i in (seq (count $argv))
|
||||
if set -q argv[$i]
|
||||
switch $argv[$i]
|
||||
case -d --delete
|
||||
set cmd delete
|
||||
case -v --save
|
||||
set cmd save
|
||||
case -l --clear
|
||||
set cmd clear
|
||||
case -s --search
|
||||
set cmd search
|
||||
case -m --merge
|
||||
set cmd merge
|
||||
case -h --help
|
||||
set cmd help
|
||||
case -t --with-time
|
||||
set time_args "-t"
|
||||
case -p --prefix
|
||||
set search_mode prefix
|
||||
set prefix_args $argv[(math $i + 1)]
|
||||
case -c --contains
|
||||
set search_mode contains
|
||||
set contains_args $argv[(math $i + 1)]
|
||||
case --
|
||||
set -e argv[1..$i]
|
||||
break
|
||||
case "-*" "--*"
|
||||
printf ( _ "%s: invalid option -- %s\n" ) history $argv[$i] >&2
|
||||
return 1
|
||||
end
|
||||
# The "set cmd $cmd xyz" lines are to make it easy to detect if the user specifies more than one
|
||||
# subcommand.
|
||||
while set -q argv[1]
|
||||
switch $argv[1]
|
||||
case -d --delete
|
||||
set cmd $cmd delete
|
||||
case -v --save
|
||||
set cmd $cmd save
|
||||
case -l --clear
|
||||
set cmd $cmd clear
|
||||
case -s --search
|
||||
set cmd $cmd search
|
||||
case -m --merge
|
||||
set cmd $cmd merge
|
||||
case -h --help
|
||||
set cmd $cmd help
|
||||
case -t --with-time
|
||||
set with_time -t
|
||||
case -p --prefix
|
||||
set search_mode --prefix
|
||||
case -c --contains
|
||||
set search_mode --contains
|
||||
case --
|
||||
set -e argv[1]
|
||||
break
|
||||
case '*'
|
||||
break
|
||||
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
|
||||
|
||||
switch $cmd
|
||||
case search
|
||||
if set -q argv[1]
|
||||
or test -n $time_args
|
||||
and contains $search_mode none
|
||||
if isatty stdout
|
||||
set -l pager less
|
||||
set -q PAGER
|
||||
and set pager $PAGER
|
||||
builtin history $time_args | eval $pager
|
||||
builtin history --search $search_mode $with_time -- $argv | eval $pager
|
||||
else
|
||||
builtin history $time_args $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
|
||||
builtin history --search $search_mode $with_time -- $argv
|
||||
end
|
||||
|
||||
set found_items_count (count $found_items)
|
||||
if test $found_items_count -gt 0
|
||||
echo "[0] cancel"
|
||||
echo "[1] all"
|
||||
echo
|
||||
case delete # Interactively delete history
|
||||
# TODO: Fix this to deal with history entries that have multiple lines.
|
||||
if not set -q argv[1]
|
||||
printf "You have to specify at least one search term to find entries to delete" >&2
|
||||
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)
|
||||
printf "[%s] %s \n" (math $i + 1) $found_items[$i]
|
||||
printf "[%s] %s\n" $i $found_items[$i]
|
||||
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
|
||||
set choice (string split " " -- $choice)
|
||||
echo ''
|
||||
|
||||
for i in $choice
|
||||
|
||||
# Skip empty input, for example, if the user just hits return
|
||||
if test -z $i
|
||||
continue
|
||||
end
|
||||
|
||||
# Following two validations could be embedded with "and" but I find the syntax
|
||||
# kind of weird.
|
||||
if not string match -qr '^[0-9]+$' $i
|
||||
printf "Invalid input: %s\n" $i
|
||||
continue
|
||||
end
|
||||
|
||||
if test $i -gt (math $found_items_count + 1)
|
||||
printf "Invalid input : %s\n" $i
|
||||
continue
|
||||
end
|
||||
|
||||
if test $i = "0"
|
||||
printf "Cancel\n"
|
||||
return
|
||||
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
|
||||
if test -z "$choice"
|
||||
printf "Cancelling the delete!\n"
|
||||
return
|
||||
end
|
||||
|
||||
if test "$choice" = "all"
|
||||
printf "Deleting all matching entries!\n"
|
||||
builtin history --delete $search_mode -- $argv
|
||||
builtin history --save
|
||||
return
|
||||
end
|
||||
|
||||
for i in (string split " " -- $choice)
|
||||
if test -z "$i"
|
||||
or not string match -qr '^[1-9][0-9]*$' -- $i
|
||||
or test $i -gt $found_items_count
|
||||
printf "Ignoring invalid history entry ID \"%s\"\n" $i
|
||||
continue
|
||||
end
|
||||
|
||||
printf "Deleting history entry %s: \"%s\"\n" $i $found_items[$i]
|
||||
builtin history --delete "$found_items[$i]"
|
||||
end
|
||||
# Save changes after deleting item(s).
|
||||
builtin history --save
|
||||
end
|
||||
|
||||
case save
|
||||
# Save changes to history file.
|
||||
builtin history $argv
|
||||
builtin history --save -- $argv
|
||||
|
||||
case merge
|
||||
builtin history --merge
|
||||
builtin history --merge -- $argv
|
||||
|
||||
case help
|
||||
builtin history --help
|
||||
|
||||
case clear
|
||||
# Erase the entire history.
|
||||
echo "Are you sure you want to clear history ? (y/n)"
|
||||
read ch
|
||||
if test $ch = "y"
|
||||
builtin history $argv
|
||||
echo "History cleared!"
|
||||
read --local --prompt "echo 'Are you sure you want to clear history? (y/n) '" choice
|
||||
if test "$choice" = "y"
|
||||
or test "$choice" = "yes"
|
||||
builtin history --clear -- $argv
|
||||
and echo "History cleared!"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
246
src/builtin.cpp
246
src/builtin.cpp
|
@ -2814,88 +2814,113 @@ static int builtin_return(parser_t &parser, io_streams_t &streams, wchar_t **arg
|
|||
return status;
|
||||
}
|
||||
|
||||
// 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,
|
||||
output_stream_t *const out) {
|
||||
if (with_time) {
|
||||
const time_t seconds = item.timestamp();
|
||||
struct tm timestamp;
|
||||
if (!localtime_r(&seconds, ×tamp)) {
|
||||
return false;
|
||||
}
|
||||
char timestamp_string[32];
|
||||
if (strftime(timestamp_string, 32, "%c ", ×tamp) != 31) {
|
||||
out->append(str2wcstring(timestamp_string));
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
enum hist_cmd_t { HIST_NOOP, HIST_SEARCH, HIST_DELETE, HIST_CLEAR, HIST_MERGE, HIST_SAVE };
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// History of commands executed by user.
|
||||
static int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
||||
int argc = builtin_count_args(argv);
|
||||
#define CHECK_FOR_UNEXPECTED_HIST_ARGS() \
|
||||
if (args.size() != 0) { \
|
||||
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;
|
||||
bool delete_item = false;
|
||||
bool search_prefix = false;
|
||||
bool save_history = false;
|
||||
bool clear_history = false;
|
||||
bool merge_history = false;
|
||||
/// Manipulate history of interactive commands executed by the user.
|
||||
static int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
||||
wchar_t *cmd = argv[0];
|
||||
;
|
||||
int argc = builtin_count_args(argv);
|
||||
hist_cmd_t hist_cmd = HIST_NOOP;
|
||||
history_search_type_t search_type = HISTORY_SEARCH_TYPE_CONTAINS;
|
||||
bool with_time = false;
|
||||
|
||||
static const struct woption long_options[] = {
|
||||
{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"save", no_argument, 0, 'v'}, {L"clear", no_argument, 0, 'l'},
|
||||
{L"merge", no_argument, 0, 'm'}, {L"help", no_argument, 0, 'h'},
|
||||
{L"with-time", no_argument, 0, 't'}, {0, 0, 0, 0}};
|
||||
{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"save", no_argument, 0, 'v'}, {L"clear", no_argument, 0, 'l'},
|
||||
{L"merge", no_argument, 0, 'm'}, {L"help", no_argument, 0, 'h'},
|
||||
{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();
|
||||
|
||||
// Use the default history if we have none (which happens if invoked non-interactively, e.g.
|
||||
// from webconfig.py.
|
||||
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) {
|
||||
case 'p': {
|
||||
search_history = true;
|
||||
search_prefix = true;
|
||||
break;
|
||||
}
|
||||
case 'd': {
|
||||
delete_item = true;
|
||||
break;
|
||||
}
|
||||
case 's': {
|
||||
search_history = true;
|
||||
break;
|
||||
}
|
||||
case 'c': {
|
||||
search_history = true;
|
||||
break;
|
||||
}
|
||||
case 'v': {
|
||||
save_history = true;
|
||||
break;
|
||||
}
|
||||
case 'l': {
|
||||
clear_history = true;
|
||||
if (!set_hist_cmd(cmd, &hist_cmd, HIST_SEARCH, streams)) {
|
||||
return STATUS_BUILTIN_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
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;
|
||||
}
|
||||
case 't': {
|
||||
|
@ -2903,85 +2928,66 @@ static int builtin_history(parser_t &parser, io_streams_t &streams, wchar_t **ar
|
|||
break;
|
||||
}
|
||||
case 'h': {
|
||||
builtin_print_help(parser, streams, argv[0], streams.out);
|
||||
builtin_print_help(parser, streams, cmd, streams.out);
|
||||
return STATUS_BUILTIN_OK;
|
||||
break;
|
||||
}
|
||||
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;
|
||||
break;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
if (merge_history) {
|
||||
history->incorporate_external_changes();
|
||||
return STATUS_BUILTIN_OK;
|
||||
}
|
||||
|
||||
else if (search_history) {
|
||||
int res = STATUS_BUILTIN_ERROR;
|
||||
for (wcstring_list_t::const_iterator iter = args.begin(); iter != args.end(); ++iter) {
|
||||
const wcstring &search_string = *iter;
|
||||
if (search_string.empty()) {
|
||||
streams.err.append_format(BUILTIN_ERR_COMBO2, argv[0],
|
||||
L"Use --search with either --contains or --prefix");
|
||||
return res;
|
||||
if (hist_cmd == HIST_NOOP) hist_cmd = HIST_SEARCH;
|
||||
|
||||
int status = STATUS_BUILTIN_OK;
|
||||
switch (hist_cmd) {
|
||||
case HIST_SEARCH: {
|
||||
if (!history->search(search_type, args, with_time, streams)) {
|
||||
status = STATUS_BUILTIN_ERROR;
|
||||
}
|
||||
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, 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;
|
||||
history->remove(delete_string);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
else if (delete_item) {
|
||||
for (wcstring_list_t::const_iterator iter = args.begin(); iter != args.end(); ++iter) {
|
||||
wcstring delete_string = *iter;
|
||||
if (delete_string[0] == '"' && delete_string[delete_string.length() - 1] == '"')
|
||||
delete_string = delete_string.substr(1, delete_string.length() - 2);
|
||||
|
||||
history->remove(delete_string);
|
||||
case HIST_CLEAR: {
|
||||
CHECK_FOR_UNEXPECTED_HIST_ARGS();
|
||||
history->clear();
|
||||
history->save();
|
||||
break;
|
||||
}
|
||||
return STATUS_BUILTIN_OK;
|
||||
}
|
||||
|
||||
else if (save_history) {
|
||||
history->save();
|
||||
return STATUS_BUILTIN_OK;
|
||||
}
|
||||
|
||||
else if (clear_history) {
|
||||
history->clear();
|
||||
history->save();
|
||||
return STATUS_BUILTIN_OK;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
case HIST_MERGE: {
|
||||
CHECK_FOR_UNEXPECTED_HIST_ARGS();
|
||||
history->incorporate_external_changes();
|
||||
break;
|
||||
}
|
||||
case HIST_SAVE: {
|
||||
CHECK_FOR_UNEXPECTED_HIST_ARGS();
|
||||
history->save();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
DIE("Unhandled history command");
|
||||
break;
|
||||
}
|
||||
return STATUS_BUILTIN_OK;
|
||||
}
|
||||
|
||||
return STATUS_BUILTIN_ERROR;
|
||||
return status;
|
||||
}
|
||||
|
||||
#if 0
|
||||
|
|
|
@ -33,6 +33,9 @@ enum { COMMAND_NOT_BUILTIN, BUILTIN_REGULAR, BUILTIN_FUNCTION };
|
|||
// Error message for unknown switch.
|
||||
#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.
|
||||
#define BUILTIN_ERR_VARCHAR \
|
||||
_(L"%ls: Invalid character '%lc' in variable name. Only alphanumerical characters and " \
|
||||
|
|
|
@ -218,6 +218,14 @@ extern bool has_working_tty_timestamps;
|
|||
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.
|
||||
#define DIE_MEM() \
|
||||
{ \
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
#include "env.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "history.h"
|
||||
#include "io.h"
|
||||
#include "iothread.h"
|
||||
#include "lru.h"
|
||||
#include "parse_constants.h"
|
||||
|
@ -1396,6 +1397,51 @@ void history_t::save(void) {
|
|||
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, ×tamp)) return false;
|
||||
char timestamp_string[22];
|
||||
if (strftime(timestamp_string, 22, "%Y-%m-%d %H:%M:%S ", ×tamp) != 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() {
|
||||
scoped_lock locker(lock); //!OCLINT(side-effect)
|
||||
disable_automatic_save_counter++;
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
#include "common.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
struct io_streams_t;
|
||||
|
||||
// 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.
|
||||
|
@ -193,7 +195,7 @@ class history_t {
|
|||
|
||||
public:
|
||||
explicit history_t(const wcstring &); // constructor
|
||||
~history_t(); // desctructor
|
||||
~history_t(); // destructor
|
||||
|
||||
// Returns history with the given name, creating it if necessary.
|
||||
static history_t &history_with_name(const wcstring &name);
|
||||
|
@ -221,6 +223,10 @@ class history_t {
|
|||
// Saves history.
|
||||
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!
|
||||
void disable_automatic_saving();
|
||||
void enable_automatic_saving();
|
||||
|
@ -250,6 +256,7 @@ class history_t {
|
|||
};
|
||||
|
||||
class history_search_t {
|
||||
private:
|
||||
// The history in which we are searching.
|
||||
history_t *history;
|
||||
|
||||
|
|
85
tests/history.expect
Normal file
85
tests/history.expect
Normal 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
0
tests/history.expect.err
Normal file
6
tests/history.expect.out
Normal file
6
tests/history.expect.out
Normal 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
|
Loading…
Reference in a new issue