55
66import re
77from typing import (
8- List ,
8+ Callable ,
9+ Dict ,
10+ Optional ,
911 Union ,
1012)
1113
@@ -27,7 +29,6 @@ class HistoryItem:
2729 _ex_listformat = ' {:>4}x {}'
2830
2931 statement = attr .ib (default = None , validator = attr .validators .instance_of (Statement ))
30- idx = attr .ib (default = None , validator = attr .validators .instance_of (int ))
3132
3233 def __str__ (self ):
3334 """A convenient human readable representation of the history item"""
@@ -50,20 +51,24 @@ def expanded(self) -> str:
5051 """
5152 return self .statement .expanded_command_line
5253
53- def pr (self , script = False , expanded = False , verbose = False ) -> str :
54+ def pr (self , idx : int , script : bool = False , expanded : bool = False , verbose : bool = False ) -> str :
5455 """Represent this item in a pretty fashion suitable for printing.
5556
5657 If you pass verbose=True, script and expanded will be ignored
5758
59+ :param idx: The 1-based index of this item in the history list
60+ :param script: True if formatting for a script (No item numbers)
61+ :param expanded: True if expanded command line should be printed
62+ :param verbose: True if expanded and raw should both appear when they are different
5863 :return: pretty print string version of a HistoryItem
5964 """
6065 if verbose :
6166 raw = self .raw .rstrip ()
6267 expanded = self .expanded
6368
64- ret_str = self ._listformat .format (self . idx , raw )
69+ ret_str = self ._listformat .format (idx , raw )
6570 if raw != expanded :
66- ret_str += '\n ' + self ._ex_listformat .format (self . idx , expanded )
71+ ret_str += '\n ' + self ._ex_listformat .format (idx , expanded )
6772 else :
6873 if expanded :
6974 ret_str = self .expanded
@@ -80,7 +85,7 @@ def pr(self, script=False, expanded=False, verbose=False) -> str:
8085
8186 # Display a numbered list if not writing to a script
8287 if not script :
83- ret_str = self ._listformat .format (self . idx , ret_str )
88+ ret_str = self ._listformat .format (idx , ret_str )
8489
8590 return ret_str
8691
@@ -121,21 +126,20 @@ def append(self, new: Statement) -> None:
121126 :param new: Statement object which will be composed into a HistoryItem
122127 and added to the end of the list
123128 """
124- history_item = HistoryItem (new , len ( self ) + 1 )
129+ history_item = HistoryItem (new )
125130 super ().append (history_item )
126131
127132 def clear (self ) -> None :
128133 """Remove all items from the History list."""
129134 super ().clear ()
130135 self .start_session ()
131136
132- def get (self , index : Union [ int , str ] ) -> HistoryItem :
137+ def get (self , index : int ) -> HistoryItem :
133138 """Get item from the History list using 1-based indexing.
134139
135- :param index: optional item to get (index as either integer or string)
140+ :param index: optional item to get
136141 :return: a single :class:`~cmd2.history.HistoryItem`
137142 """
138- index = int (index )
139143 if index == 0 :
140144 raise IndexError ('The first command in history is command 1.' )
141145 elif index < 0 :
@@ -155,8 +159,7 @@ def get(self, index: Union[int, str]) -> HistoryItem:
155159 # This regex will match 1, -1, 10, -10, but not 0 or -0.
156160 #
157161 # (?P<separator>:|(\.{2,}))? create a capture group named 'separator' which matches either
158- # a colon or two periods. This group is optional so we can
159- # match a string like '3'
162+ # a colon or two periods.
160163 #
161164 # (?P<end>-?[1-9]{1}\d*)? create a capture group named 'end' which matches an
162165 # optional minus sign, followed by exactly one non-zero
@@ -168,19 +171,18 @@ def get(self, index: Union[int, str]) -> HistoryItem:
168171 # \s*$ match any whitespace at the end of the input. This is here so
169172 # you don't have to trim the input
170173 #
171- spanpattern = re .compile (r'^\s*(?P<start>-?[1-9]\d*)?(?P<separator>:|(\.{2,}))? (?P<end>-?[1-9]\d*)?\s*$' )
174+ spanpattern = re .compile (r'^\s*(?P<start>-?[1-9]\d*)?(?P<separator>:|(\.{2,}))(?P<end>-?[1-9]\d*)?\s*$' )
172175
173- def span (self , span : str , include_persisted : bool = False ) -> List [ HistoryItem ]:
174- """Return an index or slice of the History list,
176+ def span (self , span : str , include_persisted : bool = False ) -> Dict [ int , HistoryItem ]:
177+ """Return a slice of the History list
175178
176179 :param span: string containing an index or a slice
177180 :param include_persisted: if True, then retrieve full results including from persisted history
178- :return: a list of HistoryItems
181+ :return: a dictionary of history items keyed by their 1-based index in ascending order,
182+ or an empty dictionary if no results were found
179183
180184 This method can accommodate input in any of these forms:
181185
182- a
183- -a
184186 a..b or a:b
185187 a.. or a:
186188 ..a or :a
@@ -197,89 +199,67 @@ def span(self, span: str, include_persisted: bool = False) -> List[HistoryItem]:
197199 - off by one errors
198200
199201 """
200- if span .lower () in ('*' , '-' , 'all' ):
201- span = ':'
202202 results = self .spanpattern .search (span )
203203 if not results :
204204 # our regex doesn't match the input, bail out
205205 raise ValueError ('History indices must be positive or negative integers, and may not be zero.' )
206206
207- sep = results .group ('separator' )
208207 start = results .group ('start' )
209208 if start :
210- start = self ._zero_based_index (start )
209+ start = min (self ._zero_based_index (start ), len (self ) - 1 )
210+ if start < 0 :
211+ start = max (0 , len (self ) + start )
212+ else :
213+ start = 0 if include_persisted else self .session_start_index
214+
211215 end = results .group ('end' )
212216 if end :
213- end = int (end )
214- # modify end so it's inclusive of the last element
215- if end == - 1 :
216- # -1 as the end means include the last command in the array, which in pythonic
217- # terms means to not provide an ending index. If you put -1 as the ending index
218- # python excludes the last item in the list.
219- end = None
220- elif end < - 1 :
221- # if the ending is smaller than -1, make it one larger so it includes
222- # the element (python native indices exclude the last referenced element)
223- end += 1
224-
225- if start is not None and end is not None :
226- # we have both start and end, return a slice of history
227- result = self [start :end ]
228- elif start is not None and sep is not None :
229- # take a slice of the array
230- result = self [start :]
231- elif end is not None and sep is not None :
232- if include_persisted :
233- result = self [:end ]
234- else :
235- result = self [self .session_start_index : end ]
236- elif start is not None :
237- # there was no separator so it's either a positive or negative integer
238- result = [self [start ]]
217+ end = min (int (end ), len (self ))
218+ if end < 0 :
219+ end = max (0 , len (self ) + end + 1 )
239220 else :
240- # we just have a separator, return the whole list
241- if include_persisted :
242- result = self [:]
243- else :
244- result = self [self .session_start_index :]
245- return result
221+ end = len (self )
246222
247- def str_search (self , search : str , include_persisted : bool = False ) -> List [HistoryItem ]:
223+ return self ._build_result_dictionary (start , end )
224+
225+ def str_search (self , search : str , include_persisted : bool = False ) -> Dict [int , HistoryItem ]:
248226 """Find history items which contain a given string
249227
250228 :param search: the string to search for
251229 :param include_persisted: if True, then search full history including persisted history
252- :return: a list of history items, or an empty list if the string was not found
230+ :return: a dictionary of history items keyed by their 1-based index in ascending order,
231+ or an empty dictionary if the string was not found
253232 """
254233
255- def isin (history_item ) :
234+ def isin (history_item : HistoryItem ) -> bool :
256235 """filter function for string search of history"""
257236 sloppy = utils .norm_fold (search )
258237 inraw = sloppy in utils .norm_fold (history_item .raw )
259238 inexpanded = sloppy in utils .norm_fold (history_item .expanded )
260239 return inraw or inexpanded
261240
262- search_list = self if include_persisted else self [ self .session_start_index :]
263- return [ item for item in search_list if isin ( item )]
241+ start = 0 if include_persisted else self .session_start_index
242+ return self . _build_result_dictionary ( start , len ( self ), isin )
264243
265- def regex_search (self , regex : str , include_persisted : bool = False ) -> List [ HistoryItem ]:
244+ def regex_search (self , regex : str , include_persisted : bool = False ) -> Dict [ int , HistoryItem ]:
266245 """Find history items which match a given regular expression
267246
268247 :param regex: the regular expression to search for.
269248 :param include_persisted: if True, then search full history including persisted history
270- :return: a list of history items, or an empty list if the string was not found
249+ :return: a dictionary of history items keyed by their 1-based index in ascending order,
250+ or an empty dictionary if the regex was not matched
271251 """
272252 regex = regex .strip ()
273253 if regex .startswith (r'/' ) and regex .endswith (r'/' ):
274254 regex = regex [1 :- 1 ]
275255 finder = re .compile (regex , re .DOTALL | re .MULTILINE )
276256
277- def isin (hi ) :
257+ def isin (hi : HistoryItem ) -> bool :
278258 """filter function for doing a regular expression search of history"""
279- return finder .search (hi .raw ) or finder .search (hi .expanded )
259+ return bool ( finder .search (hi .raw ) or finder .search (hi .expanded ) )
280260
281- search_list = self if include_persisted else self [ self .session_start_index :]
282- return [ itm for itm in search_list if isin ( itm )]
261+ start = 0 if include_persisted else self .session_start_index
262+ return self . _build_result_dictionary ( start , len ( self ), isin )
283263
284264 def truncate (self , max_length : int ) -> None :
285265 """Truncate the length of the history, dropping the oldest items if necessary
@@ -294,3 +274,17 @@ def truncate(self, max_length: int) -> None:
294274 elif len (self ) > max_length :
295275 last_element = len (self ) - max_length
296276 del self [0 :last_element ]
277+
278+ def _build_result_dictionary (
279+ self , start : int , end : int , filter_func : Optional [Callable [[HistoryItem ], bool ]] = None
280+ ) -> Dict [int , HistoryItem ]:
281+ """
282+ Build history search results
283+ :param start: start index to search from
284+ :param end: end index to stop searching (exclusive)
285+ """
286+ results : Dict [int , HistoryItem ] = dict ()
287+ for index in range (start , end ):
288+ if filter_func is None or filter_func (self [index ]):
289+ results [index + 1 ] = self [index ]
290+ return results
0 commit comments