mirror of
https://github.com/fish-shell/fish-shell
synced 2025-01-12 21:18:53 +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
|
\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
|
||||||
|
|
|
@ -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
|
||||||
|
|
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;
|
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, ×tamp)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
char timestamp_string[32];
|
|
||||||
if (strftime(timestamp_string, 32, "%c ", ×tamp) != 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
|
||||||
|
|
|
@ -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 " \
|
||||||
|
|
|
@ -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() \
|
||||||
{ \
|
{ \
|
||||||
|
|
|
@ -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, ×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() {
|
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++;
|
||||||
|
|
|
@ -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
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