summaryrefslogtreecommitdiff
path: root/cmd2/history.py
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2019-03-04 22:48:33 -0500
committerTodd Leonhardt <todd.leonhardt@gmail.com>2019-03-04 22:48:33 -0500
commitd1a970bc5853aa6c1c52db923fb9b4d12ada7cf2 (patch)
tree0f664834a86d6543287cf89ace609f3c0b3ae535 /cmd2/history.py
parentf765be2ce360cf104999f1440f1b13e75cbd957c (diff)
parentdddf5d03c6aa9d7a4f6e953fe1529fa3d7743e39 (diff)
downloadcmd2-git-d1a970bc5853aa6c1c52db923fb9b4d12ada7cf2.tar.gz
Merged master into this branch and resolved conflicts in CHANGELOG
Diffstat (limited to 'cmd2/history.py')
-rw-r--r--cmd2/history.py167
1 files changed, 167 insertions, 0 deletions
diff --git a/cmd2/history.py b/cmd2/history.py
new file mode 100644
index 00000000..45de3478
--- /dev/null
+++ b/cmd2/history.py
@@ -0,0 +1,167 @@
+# coding=utf-8
+"""
+History management classes
+"""
+
+import re
+
+from typing import List, Optional, Union
+
+from . import utils
+from .parsing import Statement
+
+
+class HistoryItem(str):
+ """Class used to represent one command in the History list"""
+ listformat = ' {:>4} {}\n'
+ ex_listformat = ' {:>4}x {}\n'
+
+ def __new__(cls, statement: Statement):
+ """Create a new instance of HistoryItem
+
+ 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
+
+ @property
+ def expanded(self) -> str:
+ """Return the command as run which includes shortcuts and aliases resolved plus any changes made in hooks"""
+ return self.statement.expanded_command_line
+
+ def pr(self, script=False, expanded=False, verbose=False) -> str:
+ """Represent a HistoryItem in a pretty fashion suitable for printing.
+
+ If you pass verbose=True, script and expanded will be ignored
+
+ :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())
+ else:
+ if script:
+ # display without entry numbers
+ if expanded or self.statement.multiline_command:
+ ret_str = self.expanded.rstrip()
+ else:
+ ret_str = str(self)
+ else:
+ # display a numbered list
+ if expanded or self.statement.multiline_command:
+ ret_str = self.listformat.format(self.idx, self.expanded.rstrip())
+ else:
+ ret_str = self.listformat.format(self.idx, str(self).rstrip())
+ return ret_str
+
+
+class History(list):
+ """ A list of HistoryItems that knows how to respond to user requests. """
+
+ # noinspection PyMethodMayBeStatic
+ def _zero_based_index(self, onebased: int) -> int:
+ """Convert a one-based index to a zero-based index."""
+ result = onebased
+ if result > 0:
+ result -= 1
+ return result
+
+ def _to_index(self, raw: str) -> Optional[int]:
+ if raw:
+ result = self._zero_based_index(int(raw))
+ else:
+ result = None
+ return result
+
+ spanpattern = re.compile(r'^\s*(?P<start>-?\d+)?\s*(?P<separator>:|(\.{2,}))?\s*(?P<end>-?\d+)?\s*$')
+
+ def span(self, raw: str) -> List[HistoryItem]:
+ """Parses the input string search for a span pattern and if if found, returns a slice from the History list.
+
+ :param raw: string potentially containing a span of the forms a..b, a:b, a:, ..b
+ :return: slice from the History list
+ """
+ if raw.lower() in ('*', '-', 'all'):
+ raw = ':'
+ results = self.spanpattern.search(raw)
+ if not results:
+ raise IndexError
+ if not results.group('separator'):
+ return [self[self._to_index(results.group('start'))]]
+ start = self._to_index(results.group('start')) or 0 # Ensure start is not None
+ end = self._to_index(results.group('end'))
+ reverse = False
+ if end is not None:
+ if end < start:
+ (start, end) = (end, start)
+ reverse = True
+ end += 1
+ result = self[start:end]
+ if reverse:
+ result.reverse()
+ return result
+
+ rangePattern = re.compile(r'^\s*(?P<start>[\d]+)?\s*-\s*(?P<end>[\d]+)?\s*$')
+
+ def append(self, new: Statement) -> None:
+ """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)
+
+ def get(self, getme: Optional[Union[int, str]]=None) -> List[HistoryItem]:
+ """Get an item or items from the History list using 1-based indexing.
+
+ :param getme: optional item(s) to get (either an integer index or string to search for)
+ :return: list of HistoryItems matching the retrieval criteria
+ """
+ if not getme:
+ return self
+ try:
+ getme = int(getme)
+ if getme < 0:
+ return self[:(-1 * getme)]
+ else:
+ return [self[getme - 1]]
+ except IndexError:
+ return []
+ except ValueError:
+ range_result = self.rangePattern.search(getme)
+ if range_result:
+ start = range_result.group('start') or None
+ end = range_result.group('start') or None
+ if start:
+ start = int(start) - 1
+ if end:
+ end = int(end)
+ return self[start:end]
+
+ getme = getme.strip()
+
+ if getme.startswith(r'/') and getme.endswith(r'/'):
+ finder = re.compile(getme[1:-1], re.DOTALL | re.MULTILINE | re.IGNORECASE)
+
+ def isin(hi):
+ """Listcomp filter function for doing a regular expression search of History.
+
+ :param hi: HistoryItem
+ :return: bool - True if search matches
+ """
+ return finder.search(hi) or finder.search(hi.expanded)
+ else:
+ def isin(hi):
+ """Listcomp filter function for doing a case-insensitive string search of History.
+
+ :param hi: HistoryItem
+ :return: bool - True if search matches
+ """
+ srch = utils.norm_fold(getme)
+ return srch in utils.norm_fold(hi) or srch in utils.norm_fold(hi.expanded)
+ return [itm for itm in self if isin(itm)]