summaryrefslogtreecommitdiff
path: root/cmd2/history.py
diff options
context:
space:
mode:
Diffstat (limited to 'cmd2/history.py')
-rw-r--r--cmd2/history.py109
1 files changed, 76 insertions, 33 deletions
diff --git a/cmd2/history.py b/cmd2/history.py
index 7cc36bfc..bbeb9199 100644
--- a/cmd2/history.py
+++ b/cmd2/history.py
@@ -7,25 +7,29 @@ import re
from typing import List, Union
+import attr
+
from . import utils
from .parsing import Statement
-class HistoryItem(str):
+@attr.s(frozen=True)
+class HistoryItem():
"""Class used to represent one command in the History list"""
- listformat = ' {:>4} {}\n'
- ex_listformat = ' {:>4}x {}\n'
+ _listformat = ' {:>4} {}\n'
+ _ex_listformat = ' {:>4}x {}\n'
- def __new__(cls, statement: Statement):
- """Create a new instance of HistoryItem
+ statement = attr.ib(default=None, validator=attr.validators.instance_of(Statement))
+ idx = attr.ib(default=None, validator=attr.validators.instance_of(int))
- We must override __new__ because we are subclassing `str` which is
- immutable and takes a different number of arguments as Statement.
- """
- hi = super().__new__(cls, statement.raw)
- hi.statement = statement
- hi.idx = None
- return hi
+ def __str__(self):
+ """A convenient human readable representation of the history item"""
+ return self.statement.raw
+
+ @property
+ def raw(self) -> str:
+ """Return the raw input from the user for this item"""
+ return self.statement.raw
@property
def expanded(self) -> str:
@@ -40,22 +44,22 @@ class HistoryItem(str):
:return: pretty print string version of a HistoryItem
"""
if verbose:
- ret_str = self.listformat.format(self.idx, str(self).rstrip())
- if self != self.expanded:
- ret_str += self.ex_listformat.format(self.idx, self.expanded.rstrip())
+ ret_str = self._listformat.format(self.idx, self.raw)
+ if self.raw != self.expanded.rstrip():
+ ret_str += self._ex_listformat.format(self.idx, self.expanded)
else:
if script:
# display without entry numbers
if expanded or self.statement.multiline_command:
ret_str = self.expanded.rstrip()
else:
- ret_str = str(self)
+ ret_str = self.raw.rstrip()
else:
# display a numbered list
if expanded or self.statement.multiline_command:
- ret_str = self.listformat.format(self.idx, self.expanded.rstrip())
+ ret_str = self._listformat.format(self.idx, self.expanded.rstrip())
else:
- ret_str = self.listformat.format(self.idx, str(self).rstrip())
+ ret_str = self._listformat.format(self.idx, self.raw.rstrip())
return ret_str
@@ -69,8 +73,14 @@ class History(list):
regex_search() - return a list of history items which match a given regex
get() - return a single element of the list, using 1 based indexing
span() - given a 1-based slice, return the appropriate list of history items
-
"""
+ def __init__(self, seq=()) -> None:
+ super().__init__(seq)
+ self.session_start_index = 0
+
+ def start_session(self) -> None:
+ """Start a new session, thereby setting the next index as the first index in the new session."""
+ self.session_start_index = len(self)
# noinspection PyMethodMayBeStatic
def _zero_based_index(self, onebased: Union[int, str]) -> int:
@@ -81,13 +91,17 @@ class History(list):
return result
def append(self, new: Statement) -> None:
- """Append a HistoryItem to end of the History list
+ """Append a HistoryItem to end of the History list.
:param new: command line to convert to HistoryItem and add to the end of the History list
"""
- new = HistoryItem(new)
- list.append(self, new)
- new.idx = len(self)
+ history_item = HistoryItem(new, len(self) + 1)
+ super().append(history_item)
+
+ def clear(self) -> None:
+ """Remove all items from the History list."""
+ super().clear()
+ self.start_session()
def get(self, index: Union[int, str]) -> HistoryItem:
"""Get item from the History list using 1-based indexing.
@@ -130,10 +144,11 @@ class History(list):
#
spanpattern = re.compile(r'^\s*(?P<start>-?[1-9]\d*)?(?P<separator>:|(\.{2,}))?(?P<end>-?[1-9]\d*)?\s*$')
- def span(self, span: str) -> List[HistoryItem]:
+ def span(self, span: str, include_persisted: bool = False) -> List[HistoryItem]:
"""Return an index or slice of the History list,
:param span: string containing an index or a slice
+ :param include_persisted: (optional) if True, then retrieve full results including from persisted history
:return: a list of HistoryItems
This method can accommodate input in any of these forms:
@@ -188,31 +203,43 @@ class History(list):
# take a slice of the array
result = self[start:]
elif end is not None and sep is not None:
- result = self[:end]
+ if include_persisted:
+ result = self[:end]
+ else:
+ result = self[self.session_start_index:end]
elif start is not None:
- # there was no separator so it's either a posative or negative integer
+ # there was no separator so it's either a positive or negative integer
result = [self[start]]
else:
# we just have a separator, return the whole list
- result = self[:]
+ if include_persisted:
+ result = self[:]
+ else:
+ result = self[self.session_start_index:]
return result
- def str_search(self, search: str) -> List[HistoryItem]:
+ def str_search(self, search: str, include_persisted: bool = False) -> List[HistoryItem]:
"""Find history items which contain a given string
:param search: the string to search for
+ :param include_persisted: (optional) if True, then search full history including persisted history
:return: a list of history items, or an empty list if the string was not found
"""
def isin(history_item):
"""filter function for string search of history"""
sloppy = utils.norm_fold(search)
- return sloppy in utils.norm_fold(history_item) or sloppy in utils.norm_fold(history_item.expanded)
- return [item for item in self if isin(item)]
+ inraw = sloppy in utils.norm_fold(history_item.raw)
+ inexpanded = sloppy in utils.norm_fold(history_item.expanded)
+ return inraw or inexpanded
- def regex_search(self, regex: str) -> List[HistoryItem]:
+ search_list = self if include_persisted else self[self.session_start_index:]
+ return [item for item in search_list if isin(item)]
+
+ def regex_search(self, regex: str, include_persisted: bool = False) -> List[HistoryItem]:
"""Find history items which match a given regular expression
:param regex: the regular expression to search for.
+ :param include_persisted: (optional) if True, then search full history including persisted history
:return: a list of history items, or an empty list if the string was not found
"""
regex = regex.strip()
@@ -222,5 +249,21 @@ class History(list):
def isin(hi):
"""filter function for doing a regular expression search of history"""
- return finder.search(hi) or finder.search(hi.expanded)
- return [itm for itm in self if isin(itm)]
+ return finder.search(hi.raw) or finder.search(hi.expanded)
+
+ search_list = self if include_persisted else self[self.session_start_index:]
+ return [itm for itm in search_list if isin(itm)]
+
+ def truncate(self, max_length: int) -> None:
+ """Truncate the length of the history, dropping the oldest items if necessary
+
+ :param max_length: the maximum length of the history, if negative, all history
+ items will be deleted
+ :return: nothing
+ """
+ if max_length <= 0:
+ # remove all history
+ del self[:]
+ elif len(self) > max_length:
+ last_element = len(self) - max_length
+ del self[0:last_element]