mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-27 05:13:10 +00:00
Adding history builtin
This commit is contained in:
parent
b877181e17
commit
ec34f2527a
6 changed files with 316 additions and 11 deletions
125
builtin.cpp
125
builtin.cpp
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
1). Create a function in builtin.c with the following signature:
|
1). Create a function in builtin.c with the following signature:
|
||||||
|
|
||||||
<tt>static int builtin_NAME( wchar_t ** args )</tt>
|
<tt>static int builtin_NAME( parser_t &parser, wchar_t ** args )</tt>
|
||||||
|
|
||||||
where NAME is the name of the builtin, and args is a zero-terminated list of arguments.
|
where NAME is the name of the builtin, and args is a zero-terminated list of arguments.
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
#include "parser_keywords.h"
|
#include "parser_keywords.h"
|
||||||
#include "expand.h"
|
#include "expand.h"
|
||||||
#include "path.h"
|
#include "path.h"
|
||||||
|
#include "history.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The default prompt for the read command
|
The default prompt for the read command
|
||||||
|
@ -3590,6 +3590,126 @@ static int builtin_case( parser_t &parser, wchar_t **argv )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
History of commands executed by user
|
||||||
|
*/
|
||||||
|
static int builtin_history( parser_t &parser, wchar_t **argv )
|
||||||
|
{
|
||||||
|
int argc = builtin_count_args(argv);
|
||||||
|
|
||||||
|
bool search_history = false;
|
||||||
|
bool delete_item = false;
|
||||||
|
bool search_prefix = false;
|
||||||
|
bool save_history = false;
|
||||||
|
bool clear_history = false;
|
||||||
|
|
||||||
|
wcstring delete_string;
|
||||||
|
wcstring search_string;
|
||||||
|
|
||||||
|
static const struct woption long_options[] =
|
||||||
|
{
|
||||||
|
{ L"prefix", required_argument, 0, 'p' },
|
||||||
|
{ L"delete", required_argument, 0, 'd' },
|
||||||
|
{ L"search", no_argument, 0, 's' },
|
||||||
|
{ L"contains", required_argument, 0, 'c' },
|
||||||
|
{ L"save", no_argument, 0, 'v' },
|
||||||
|
{ L"clear", no_argument, 0, 'l' },
|
||||||
|
{ 0, 0, 0, 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
int opt = 0;
|
||||||
|
int opt_index = 0;
|
||||||
|
woptind = 0;
|
||||||
|
history_t *history = reader_get_history();
|
||||||
|
|
||||||
|
while((opt = wgetopt_long( argc, argv, L"pdsc", long_options, &opt_index )) != -1)
|
||||||
|
{
|
||||||
|
switch(opt)
|
||||||
|
{
|
||||||
|
case 'p':
|
||||||
|
search_prefix = true;
|
||||||
|
search_string = woptarg;
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
delete_item = true;
|
||||||
|
delete_string = woptarg;
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
search_history = true;
|
||||||
|
break;
|
||||||
|
case 'c':
|
||||||
|
search_string = woptarg;
|
||||||
|
break;
|
||||||
|
case 'v':
|
||||||
|
save_history = true;
|
||||||
|
break;
|
||||||
|
case 'l':
|
||||||
|
clear_history = true;
|
||||||
|
break;
|
||||||
|
case '?':
|
||||||
|
append_format(stderr_buffer, BUILTIN_ERR_UNKNOWN, argv[0], argv[woptind-1]);
|
||||||
|
return STATUS_BUILTIN_ERROR;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
append_format(stderr_buffer, BUILTIN_ERR_UNKNOWN, argv[0], argv[woptind-1]);
|
||||||
|
return STATUS_BUILTIN_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argc == 1)
|
||||||
|
{
|
||||||
|
wcstring full_history;
|
||||||
|
history->get_string_representation(full_history, wcstring(L"\n"));
|
||||||
|
stdout_buffer.append(full_history);
|
||||||
|
return STATUS_BUILTIN_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search_history)
|
||||||
|
{
|
||||||
|
int res = STATUS_BUILTIN_ERROR;
|
||||||
|
|
||||||
|
if (search_string.empty())
|
||||||
|
{
|
||||||
|
append_format(stderr_buffer, BUILTIN_ERR_COMBO2, argv[0], L"Use --search with either --contains or --prefix");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
history_search_t searcher = history_search_t(*history, search_string, search_prefix?HISTORY_SEARCH_TYPE_PREFIX:HISTORY_SEARCH_TYPE_CONTAINS);
|
||||||
|
while (searcher.go_backwards())
|
||||||
|
{
|
||||||
|
stdout_buffer.append(searcher.current_string());
|
||||||
|
stdout_buffer.append(L"\n");
|
||||||
|
res = STATUS_BUILTIN_OK;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delete_item)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (save_history)
|
||||||
|
{
|
||||||
|
history->save();
|
||||||
|
return STATUS_BUILTIN_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clear_history)
|
||||||
|
{
|
||||||
|
history->clear();
|
||||||
|
history->save();
|
||||||
|
return STATUS_BUILTIN_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return STATUS_BUILTIN_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
END OF BUILTIN COMMANDS
|
END OF BUILTIN COMMANDS
|
||||||
Below are functions for handling the builtin commands.
|
Below are functions for handling the builtin commands.
|
||||||
|
@ -3629,6 +3749,7 @@ static const builtin_data_t builtin_datas[]=
|
||||||
{ L"for", &builtin_for, N_( L"Perform a set of commands multiple times" ) },
|
{ L"for", &builtin_for, N_( L"Perform a set of commands multiple times" ) },
|
||||||
{ L"function", &builtin_function, N_( L"Define a new function" ) },
|
{ L"function", &builtin_function, N_( L"Define a new function" ) },
|
||||||
{ L"functions", &builtin_functions, N_( L"List or remove functions" ) },
|
{ L"functions", &builtin_functions, N_( L"List or remove functions" ) },
|
||||||
|
{ L"history", &builtin_history, N_( L"History of commands executed by user" ) },
|
||||||
{ L"if", &builtin_generic, N_( L"Evaluate block if condition is true" ) },
|
{ L"if", &builtin_generic, N_( L"Evaluate block if condition is true" ) },
|
||||||
{ L"jobs", &builtin_jobs, N_( L"Print currently running jobs" ) },
|
{ L"jobs", &builtin_jobs, N_( L"Print currently running jobs" ) },
|
||||||
{ L"not", &builtin_generic, N_( L"Negate exit status of job" ) },
|
{ L"not", &builtin_generic, N_( L"Negate exit status of job" ) },
|
||||||
|
|
36
doc_src/history.txt
Normal file
36
doc_src/history.txt
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
\section history history - Show and manipulate user's command history
|
||||||
|
|
||||||
|
\subsection history-synopsis Synopsis
|
||||||
|
<pre>
|
||||||
|
history (--save | --clear)
|
||||||
|
history (--search | --delete ) (--prefix "prefix string" | --search "search string")
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
\subsection history-description Description
|
||||||
|
|
||||||
|
history is used to list, search and delete user's command history.
|
||||||
|
|
||||||
|
\subsection history-examples Example
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
history --save
|
||||||
|
Save all changes in history file.
|
||||||
|
|
||||||
|
history --clear
|
||||||
|
Delete all history items.
|
||||||
|
|
||||||
|
history --search --contains "foo"
|
||||||
|
Searches commands containing "foo" string.
|
||||||
|
|
||||||
|
history --search --prefix "foo"
|
||||||
|
Searches for commands with prefix "foo".
|
||||||
|
|
||||||
|
history --delete --contains "foo"
|
||||||
|
Interactively delete commands containing string "foo".
|
||||||
|
|
||||||
|
history --delete --prefix "foo"
|
||||||
|
Interactively delete commands with prefix "foo".
|
||||||
|
|
||||||
|
history --delete "foo"
|
||||||
|
Delete command "foo" from history.
|
||||||
|
<pre>
|
|
@ -864,6 +864,11 @@ void history_tests_t::test_history(void) {
|
||||||
test_history_matches(search2, 1);
|
test_history_matches(search2, 1);
|
||||||
assert(search2.current_string() == L"Beta");
|
assert(search2.current_string() == L"Beta");
|
||||||
|
|
||||||
|
/* Test item removal */
|
||||||
|
history.remove(L"Alpha");
|
||||||
|
history_search_t search3(history, L"Alpha");
|
||||||
|
test_history_matches(search3, 0);
|
||||||
|
|
||||||
/* Test history escaping and unescaping, yaml, etc. */
|
/* Test history escaping and unescaping, yaml, etc. */
|
||||||
std::vector<history_item_t> before, after;
|
std::vector<history_item_t> before, after;
|
||||||
history.clear();
|
history.clear();
|
||||||
|
|
42
history.cpp
42
history.cpp
|
@ -393,6 +393,23 @@ void history_t::add(const wcstring &str, const path_list_t &valid_paths)
|
||||||
this->add(history_item_t(str, time(NULL), valid_paths));
|
this->add(history_item_t(str, time(NULL), valid_paths));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void history_t::remove(const wcstring &str)
|
||||||
|
{
|
||||||
|
history_item_t item_to_delete(str);
|
||||||
|
deleted_items.push_back(item_to_delete);
|
||||||
|
|
||||||
|
for (std::vector<history_item_t>::iterator iter = new_items.begin(); iter != new_items.end(); ++iter)
|
||||||
|
{
|
||||||
|
if (iter->match_contents(item_to_delete))
|
||||||
|
{
|
||||||
|
new_items.erase(iter);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
void history_t::get_string_representation(wcstring &result, const wcstring &separator)
|
void history_t::get_string_representation(wcstring &result, const wcstring &separator)
|
||||||
{
|
{
|
||||||
scoped_lock locker(lock);
|
scoped_lock locker(lock);
|
||||||
|
@ -400,10 +417,10 @@ void history_t::get_string_representation(wcstring &result, const wcstring &sepa
|
||||||
bool first = true;
|
bool first = true;
|
||||||
|
|
||||||
/* Append new items */
|
/* Append new items */
|
||||||
for (size_t i=0; i < new_items.size(); i++) {
|
for (std::vector<history_item_t>::const_reverse_iterator iter=new_items.rbegin(); iter < new_items.rend(); ++iter) {
|
||||||
if (! first)
|
if (! first)
|
||||||
result.append(separator);
|
result.append(separator);
|
||||||
result.append(new_items.at(i).str());
|
result.append(iter->str());
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -795,7 +812,7 @@ void history_t::save_internal()
|
||||||
ASSERT_IS_LOCKED(lock);
|
ASSERT_IS_LOCKED(lock);
|
||||||
|
|
||||||
/* Nothing to do if there's no new items */
|
/* Nothing to do if there's no new items */
|
||||||
if (new_items.empty())
|
if (new_items.empty() && deleted_items.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
/* Compact our new items so we don't have duplicates */
|
/* Compact our new items so we don't have duplicates */
|
||||||
|
@ -825,9 +842,11 @@ void history_t::save_internal()
|
||||||
|
|
||||||
/* Try decoding an old item */
|
/* Try decoding an old item */
|
||||||
const history_item_t old_item = history_t::decode_item(local_mmap_start + offset, local_mmap_size - offset);
|
const history_item_t old_item = history_t::decode_item(local_mmap_start + offset, local_mmap_size - offset);
|
||||||
if (old_item.empty())
|
if (old_item.empty() || is_deleted(old_item))
|
||||||
|
{
|
||||||
|
// debug(0, L"Item is deleted : %s\n", old_item.str().c_str());
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
/* The old item may actually be more recent than our new item, if it came from another session. Insert all new items at the given index with an earlier timestamp. */
|
/* The old item may actually be more recent than our new item, if it came from another session. Insert all new items at the given index with an earlier timestamp. */
|
||||||
for (; new_item_iter != new_items.end(); ++new_item_iter) {
|
for (; new_item_iter != new_items.end(); ++new_item_iter) {
|
||||||
if (new_item_iter->timestamp() < old_item.timestamp()) {
|
if (new_item_iter->timestamp() < old_item.timestamp()) {
|
||||||
|
@ -846,7 +865,8 @@ void history_t::save_internal()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Insert any remaining new items */
|
/* Insert any remaining new items */
|
||||||
for (; new_item_iter != new_items.end(); ++new_item_iter) {
|
for (; new_item_iter != new_items.end(); ++new_item_iter)
|
||||||
|
{
|
||||||
lru.add_item(*new_item_iter);
|
lru.add_item(*new_item_iter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -903,6 +923,7 @@ void history_t::save(void) {
|
||||||
void history_t::clear(void) {
|
void history_t::clear(void) {
|
||||||
scoped_lock locker(lock);
|
scoped_lock locker(lock);
|
||||||
new_items.clear();
|
new_items.clear();
|
||||||
|
deleted_items.clear();
|
||||||
unsaved_item_count = 0;
|
unsaved_item_count = 0;
|
||||||
old_item_offsets.clear();
|
old_item_offsets.clear();
|
||||||
wcstring filename = history_filename(name, L"");
|
wcstring filename = history_filename(name, L"");
|
||||||
|
@ -1020,3 +1041,12 @@ void history_t::add_with_file_detection(const wcstring &str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool history_t::is_deleted(const history_item_t &item) const
|
||||||
|
{
|
||||||
|
for (std::vector<history_item_t>::const_iterator iter = deleted_items.begin(); iter != deleted_items.end(); ++iter)
|
||||||
|
{
|
||||||
|
if (iter->match_contents(item)) { return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
12
history.h
12
history.h
|
@ -65,6 +65,10 @@ class history_item_t {
|
||||||
required_paths == other.required_paths;
|
required_paths == other.required_paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool match_contents(const history_item_t &other) const {
|
||||||
|
return contents == other.contents;
|
||||||
|
}
|
||||||
|
|
||||||
/* Functions for testing only */
|
/* Functions for testing only */
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -97,6 +101,9 @@ private:
|
||||||
/** New items. */
|
/** New items. */
|
||||||
std::vector<history_item_t> new_items;
|
std::vector<history_item_t> new_items;
|
||||||
|
|
||||||
|
/** Deleted items. */
|
||||||
|
std::vector<history_item_t> deleted_items;
|
||||||
|
|
||||||
/** How many items we've added without saving */
|
/** How many items we've added without saving */
|
||||||
size_t unsaved_item_count;
|
size_t unsaved_item_count;
|
||||||
|
|
||||||
|
@ -138,6 +145,9 @@ public:
|
||||||
/** Add a new history item to the end */
|
/** Add a new history item to the end */
|
||||||
void add(const wcstring &str, const path_list_t &valid_paths = path_list_t());
|
void add(const wcstring &str, const path_list_t &valid_paths = path_list_t());
|
||||||
|
|
||||||
|
/** Remove a history item */
|
||||||
|
void remove(const wcstring &str);
|
||||||
|
|
||||||
/** Add a new history item to the end */
|
/** Add a new history item to the end */
|
||||||
void add_with_file_detection(const wcstring &str);
|
void add_with_file_detection(const wcstring &str);
|
||||||
|
|
||||||
|
@ -152,6 +162,8 @@ public:
|
||||||
|
|
||||||
/** Return the specified history at the specified index. 0 is the index of the current commandline. (So the most recent item is at index 1.) */
|
/** Return the specified history at the specified index. 0 is the index of the current commandline. (So the most recent item is at index 1.) */
|
||||||
history_item_t item_at_index(size_t idx);
|
history_item_t item_at_index(size_t idx);
|
||||||
|
|
||||||
|
bool is_deleted(const history_item_t &item) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
class history_search_t {
|
class history_search_t {
|
||||||
|
|
101
share/functions/history.fish
Normal file
101
share/functions/history.fish
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
#
|
||||||
|
#Deletes an item from history
|
||||||
|
#
|
||||||
|
|
||||||
|
function history --description "Deletes an item from history"
|
||||||
|
|
||||||
|
set -l argc (count $argv)
|
||||||
|
set -l prefix_args ""
|
||||||
|
set -l contains_args ""
|
||||||
|
|
||||||
|
set -l delete 0
|
||||||
|
set -l clear 0
|
||||||
|
|
||||||
|
set -l search_mode none
|
||||||
|
|
||||||
|
if test $argc -gt 0
|
||||||
|
for i in (seq $argc)
|
||||||
|
switch $argv[$i]
|
||||||
|
case --delete
|
||||||
|
set delete 1
|
||||||
|
case --prefix
|
||||||
|
set search_mode prefix
|
||||||
|
set prefix_args $argv[(math $i + 1)]
|
||||||
|
case --contains
|
||||||
|
set search_mode contains
|
||||||
|
set contains_args $argv[(math $i + 1)]
|
||||||
|
case --clear
|
||||||
|
set clear 1
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if test $delete = 1
|
||||||
|
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
|
||||||
|
return 0
|
||||||
|
end;
|
||||||
|
|
||||||
|
set found_items_count (count $found_items)
|
||||||
|
if test $found_items_count -gt 0
|
||||||
|
printf "Delete which entries ?\n"
|
||||||
|
printf "[0] cancel\n"
|
||||||
|
printf "[1] all\n\n"
|
||||||
|
|
||||||
|
for i in (seq $found_items_count)
|
||||||
|
printf "[%s] %s \n" (math $i + 1) $found_items[$i]
|
||||||
|
end;
|
||||||
|
|
||||||
|
read choice
|
||||||
|
set choice (echo $choice | tr " " "\n")
|
||||||
|
|
||||||
|
for i in $choice
|
||||||
|
echo $i | grep -q "^[0-9]*\$"
|
||||||
|
|
||||||
|
#Following two validations could be embedded with "and" but I find the syntax kind of weird.
|
||||||
|
if test $status -ne 0
|
||||||
|
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"
|
||||||
|
return
|
||||||
|
else
|
||||||
|
builtin history --delete $found_items[(math $i - 1)]
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
else
|
||||||
|
if test $clear = 1
|
||||||
|
echo "Are you sure you want to clear history ? (y/n)"
|
||||||
|
read ch
|
||||||
|
if test $ch = "y"
|
||||||
|
builtin history $argv
|
||||||
|
echo "History cleared!"
|
||||||
|
end;
|
||||||
|
else
|
||||||
|
builtin history $argv
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end;
|
Loading…
Reference in a new issue