text: # coding=utf-8
  """
  History management classes
  """
  
  import re
  
  from typing import List, Union
  
  import attr
  
  from . import utils
  from .parsing import Statement
  
  
  @attr.s(frozen=True)
  class HistoryItem():
      """Class used to represent one command in the History list"""
      _listformat = ' {:>4}  {}'
      _ex_listformat = ' {:>4}x {}'
  
      statement = attr.ib(default=None, validator=attr.validators.instance_of(Statement))
      idx = attr.ib(default=None, validator=attr.validators.instance_of(int))
  
      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:
          """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:
              raw = self.raw.rstrip()
              expanded = self.expanded.rstrip()
  
              ret_str = self._listformat.format(self.idx, raw)
              if raw != expanded:
                  ret_str += '\n' + self._ex_listformat.format(self.idx, expanded)
          else:
              if expanded:
                  ret_str = self.expanded.rstrip()
              else:
                  ret_str = self.raw.rstrip()
  
                  # In non-verbose mode, display raw multiline commands on 1 line
                  if self.statement.multiline_command:
                      # This is an approximation and not meant to be a perfect piecing together of lines.
                      # All newlines will be converted to spaces, including the ones in quoted strings that
                      # are considered literals. Also if the final line starts with a terminator, then the
                      # terminator will have an extra space before it in the 1 line version.
                      ret_str = ret_str.replace('\n', ' ')
  
              # Display a numbered list if not writing to a script
              if not script:
                  ret_str = self._listformat.format(self.idx, ret_str)
  
          return ret_str
  
  
  class History(list):
      """A list of HistoryItems that knows how to respond to user requests.
  
      Here are some key methods:
  
      select() - parse user input and return a list of relevant history items
      str_search() - return a list of history items which contain the given string
      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:
          """Convert a one-based index to a zero-based index."""
          result = int(onebased)
          if result > 0:
              result -= 1
          return result
  
      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
          """
          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.
  
          :param index: optional item to get (index as either integer or string)
          :return: a single HistoryItem
          """
          index = int(index)
          if index == 0:
              raise IndexError('The first command in history is command 1.')
          elif index < 0:
              return self[index]
          else:
              return self[index - 1]
  
      # This regular expression parses input for the span() method. There are five parts:
      #
      #    ^\s*                          matches any whitespace at the beginning of the
      #                                  input. This is here so you don't have to trim the input
      #
      #    (?P<start>-?[1-9]{1}\d*)?     create a capture group named 'start' which matches an
      #                                  optional minus sign, followed by exactly one non-zero
      #                                  digit, and as many other digits as you want. This group
      #                                  is optional so that we can match an input string like '..2'.
      #                                  This regex will match 1, -1, 10, -10, but not 0 or -0.
      #
      #    (?P<separator>:|(\.{2,}))?    create a capture group named 'separator' which matches either
      #                                  a colon or two periods. This group is optional so we can
      #                                  match a string like '3'
      #
      #    (?P<end>-?[1-9]{1}\d*)?       create a capture group named 'end' which matches an
      #                                  optional minus sign, followed by exactly one non-zero
      #                                  digit, and as many other digits as you want. This group is
      #                                  optional so that we can match an input string like ':'
      #                                  or '5:'. This regex will match 1, -1, 10, -10, but not
      #                                  0 or -0.
      #
      #    \s*$                          match any whitespace at the end of the input. This is here so
      #                                  you don't have to trim the input
      #
      spanpattern = re.compile(r'^\s*(?P<start>-?[1-9]\d*)?(?P<separator>:|(\.{2,}))?(?P<end>-?[1-9]\d*)?\s*$')
  
      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: 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:
  
              a
              -a
              a..b or a:b
              a.. or a:
              ..a or :a
              -a.. or -a:
              ..-a or :-a
  
          Different from native python indexing and slicing of arrays, this method
          uses 1-based array numbering. Users who are not programmers can't grok
          0 based numbering. Programmers can usually grok either. Which reminds me,
          there are only two hard problems in programming:
  
          - naming
          - cache invalidation
          - off by one errors
  
          """
          if span.lower() in ('*', '-', 'all'):
              span = ':'
          results = self.spanpattern.search(span)
          if not results:
              # our regex doesn't match the input, bail out
              raise ValueError('History indices must be positive or negative integers, and may not be zero.')
  
          sep = results.group('separator')
          start = results.group('start')
          if start:
              start = self._zero_based_index(start)
          end = results.group('end')
          if end:
              end = int(end)
              # modify end so it's inclusive of the last element
              if end == -1:
                  # -1 as the end means include the last command in the array, which in pythonic
                  # terms means to not provide an ending index. If you put -1 as the ending index
                  # python excludes the last item in the list.
                  end = None
              elif end < -1:
                  # if the ending is smaller than -1, make it one larger so it includes
                  # the element (python native indices exclude the last referenced element)
                  end += 1
  
          if start is not None and end is not None:
              # we have both start and end, return a slice of history
              result = self[start:end]
          elif start is not None and sep is not None:
              # take a slice of the array
              result = self[start:]
          elif end is not None and sep is not None:
              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 positive or negative integer
              result = [self[start]]
          else:
              # we just have a separator, return the whole list
              if include_persisted:
                  result = self[:]
              else:
                  result = self[self.session_start_index:]
          return result
  
      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: 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)
              inraw = sloppy in utils.norm_fold(history_item.raw)
              inexpanded = sloppy in utils.norm_fold(history_item.expanded)
              return inraw or inexpanded
  
          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: 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()
          if regex.startswith(r'/') and regex.endswith(r'/'):
              regex = regex[1:-1]
          finder = re.compile(regex, re.DOTALL | re.MULTILINE)
  
          def isin(hi):
              """filter function for doing a regular expression search of history"""
              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]
  