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
\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

View File

@ -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

View File

@ -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, &timestamp)) {
return false;
}
char timestamp_string[32];
if (strftime(timestamp_string, 32, "%c ", &timestamp) != 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

View File

@ -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 " \

View File

@ -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() \
{ \

View File

@ -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, &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() {
scoped_lock locker(lock); //!OCLINT(side-effect)
disable_automatic_save_counter++;

View File

@ -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
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