Repository: python-cmd2/cmd2

Issue:
Update history Sphinx documentation to help avoid confusion
The [history documentation](https://cmd2.readthedocs.io/en/latest/freefeatures.html?highlight=history#searchable-command-history) could use an extensive update/re-write to better explain:
1. That there are two logically different types of history within ``cmd2`` - **readline history** and the **history command** history
1. How and why the two differ
1. Much more info on the history command and how to use it

See #639 for more info on one case of user confusion.

Discussion:

Patch:
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 44b9cce8..e32b3180 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -3231,15 +3231,22 @@ class Cmd(cmd.Cmd):
             # If a character indicating a slice is present, retrieve
             # a slice of the history
             arg = args.arg
+            arg_is_int = False
+            try:
+                int(arg)
+                arg_is_int = True
+            except ValueError:
+                pass
+
             if '..' in arg or ':' in arg:
-                try:
-                    # Get a slice of history
-                    history = self.history.span(arg)
-                except IndexError:
-                    history = self.history.get(arg)
+                # Get a slice of history
+                history = self.history.span(arg)
+            elif arg_is_int:
+                history = [self.history.get(arg)]
+            elif arg.startswith(r'/') and arg.endswith(r'/'):
+                history = self.history.regex_search(arg)
             else:
-                # Get item(s) from history by index or string search
-                history = self.history.get(arg)
+                history = self.history.str_search(arg)
         else:
             # If no arg given, then retrieve the entire history
             cowardly_refuse_to_run = True
diff --git a/cmd2/history.py b/cmd2/history.py
index 45de3478..729cc6e3 100644
--- a/cmd2/history.py
+++ b/cmd2/history.py
@@ -5,7 +5,7 @@ History management classes
 
 import re
 
-from typing import List, Optional, Union
+from typing import List, Union
 
 from . import utils
 from .parsing import Statement
@@ -60,108 +60,167 @@ class HistoryItem(str):
 
 
 class History(list):
-    """ A list of HistoryItems that knows how to respond to user requests. """
+    """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
+
+    """
 
     # noinspection PyMethodMayBeStatic
-    def _zero_based_index(self, onebased: int) -> int:
+    def _zero_based_index(self, onebased: Union[int, str]) -> int:
         """Convert a one-based index to a zero-based index."""
-        result = onebased
+        result = int(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
+    def append(self, new: Statement) -> None:
+        """Append a HistoryItem to end of the History list
 
-    spanpattern = re.compile(r'^\s*(?P<start>-?\d+)?\s*(?P<separator>:|(\.{2,}))?\s*(?P<end>-?\d+)?\s*$')
+        :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 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.
+    def get(self, index: Union[int, str]) -> HistoryItem:
+        """Get item from the History list using 1-based indexing.
 
-        :param raw: string potentially containing a span of the forms a..b, a:b, a:, ..b
-        :return: slice from the History list
+        :param index: optional item to get (index as either integer or string)
+        :return: a single HistoryItem
         """
-        if raw.lower() in ('*', '-', 'all'):
-            raw = ':'
-        results = self.spanpattern.search(raw)
+        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]{1}\d*)?(?P<separator>:|(\.{2,}))?(?P<end>-?[1-9]{1}\d*)?\s*$')
+
+    def span(self, span: str) -> List[HistoryItem]:
+        """Return an index or slice of the History list,
+
+        :param raw: string containing an index or a slice
+        :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:
-            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()
+            # 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:
+            result = self[:end]
+        elif start is not None:
+            # there was no separator so it's either a posative or negative integer
+            result = [self[start]]
+        else:
+            # we just have a separator, return the whole list
+            result = self[:]
         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
+    def str_search(self, search: str) -> List[HistoryItem]:
+        """Find history items which contain a given string
 
-        :param new: command line to convert to HistoryItem and add to the end of the History list
+        :param search: the string to search for
+        :return: a list of history items, or an empty list if the string was not found
         """
-        new = HistoryItem(new)
-        list.append(self, new)
-        new.idx = len(self)
+        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)]
 
-    def get(self, getme: Optional[Union[int, str]]=None) -> List[HistoryItem]:
-        """Get an item or items from the History list using 1-based indexing.
+    def regex_search(self, regex: str) -> List[HistoryItem]:
+        """Find history items which match a given regular expression
 
-        :param getme: optional item(s) to get (either an integer index or string to search for)
-        :return: list of HistoryItems matching the retrieval criteria
+        :param regex: the regular expression to search for.
+        :return: a list of history items, or an empty list if the string was not found
         """
-        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)]
+        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) or finder.search(hi.expanded)
+        return [itm for itm in self if isin(itm)]
diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst
index bcb9c0e7..001a7599 100644
--- a/docs/freefeatures.rst
+++ b/docs/freefeatures.rst
@@ -29,9 +29,9 @@ Simply include one command per line, typed exactly as you would inside a ``cmd2`
 Comments
 ========
 
-Any command line input where the first non-whitespace character is a # will be treated as a comment.
-This means any # character appearing later in the command will be treated as a literal. The same
-applies to a # in the middle of a multiline command, even if it is the first character on a line.
+Any command line input where the first non-whitespace character is a `#` will be treated as a comment.
+This means any `#` character appearing later in the command will be treated as a literal. The same
+applies to a `#` in the middle of a multiline command, even if it is the first character on a line.
 
 Comments can be useful in :ref:`scripts`, but would be pointless within an interactive session.
 
@@ -253,9 +253,208 @@ the readline history.
 
 .. automethod:: cmd2.cmd2.Cmd.__init__
 
-``cmd2`` makes a third type of history access available with the **history** command:
+``cmd2`` makes a third type of history access available with the ``history``
+command. Each time the user enters a command, ``cmd2`` saves the input. The
+``history`` command lets you do interesting things with that saved input. The
+examples to follow all assume that you have entered the following commands::
+
+    (Cmd) alias create one !echo one
+    Alias 'one' created
+    (Cmd) alias create two !echo two
+    Alias 'two' created
+    (Cmd) alias create three !echo three
+    Alias 'three' created
+    (Cmd) alias create four !echo four
+    Alias 'four' created
 
-.. automethod:: cmd2.cmd2.Cmd.do_history
+In it's simplest form, the ``history`` command displays previously entered
+commands. With no additional arguments, it displays all previously entered
+commands::
+
+    (Cmd) history
+        1  alias create one !echo one
+        2  alias create two !echo two
+        3  alias create three !echo three
+        4  alias create four !echo four
+
+If you give a positive integer as an argument, then it only displays the
+specified command::
+
+    (Cmd) history 4
+        4  alias create four !echo four
+
+If you give a negative integer *N* as an argument, then it display the *Nth*
+last command. For example, if you give ``-1`` it will display the last command
+you entered. If you give ``-2`` it will display the next to last command you
+entered, and so forth::
+
+    (Cmd) history -2
+        3  alias create three !echo three
+
+You can use a similar mechanism to display a range of commands. Simply give two
+command numbers separated by ``..`` or ``:``, and you will see all commands
+between, and including, those two numbers::
+
+    (Cmd) history 1:3
+        1  alias create one !echo one
+        2  alias create two !echo two
+        3  alias create three !echo three
+
+If you omit the first number, it will start at the beginning. If you omit the
+last number, it will continue to the end::
+
+    (Cmd) history :2
+        1  alias create one !echo one
+        2  alias create two !echo two
+    (Cmd) history 2:
+        2  alias create two !echo two
+        3  alias create three !echo three
+        4  alias create four !echo four
+
+If you want to display the last three commands entered::
+
+    (Cmd) history -- -3:
+        2  alias create two !echo two
+        3  alias create three !echo three
+        4  alias create four !echo four
+
+Notice the double dashes. These are required because the history command uses
+``argparse`` to parse the command line arguments. As described in the `argparse
+documentation <https://docs.python.org/3/library/argparse.html>`_ , ``-3:`` is
+an option, not an argument:
+
+    If you have positional arguments that must begin with - and don’t look like
+    negative numbers, you can insert the pseudo-argument '--' which tells
+    parse_args() that everything after that is a positional argument:
+
+There is no zeroth command, so don't ask for it. If you are a python programmer,
+you've probably noticed this looks a lot like the slice syntax for lists and
+arrays. It is, with the exception that the first history command is 1, where the
+first element in a python array is 0.
+
+Besides selecting previous commands by number, you can also search for them. You
+can use a simple string search::
+
+    (Cmd) history two
+        2  alias create two !echo two
+
+Or a regular expression search by enclosing your regex in slashes::
+
+    (Cmd) history '/te\ +th/'
+        3  alias create three !echo three
+
+If your regular expression contains any characters that ``argparse`` finds
+interesting, like dash or plus, you also need to enclose your regular expression
+in quotation marks.
+
+This all sounds great, but doesn't it seem like a bit of overkill to have all
+these ways to select commands if all we can do is display them? Turns out,
+displaying history commands is just the beginning. The history command can
+perform many other actions:
+
+- running previously entered commands
+- saving previously entered commands to a text file
+- opening previously entered commands in your favorite text editor
+- running previously entered commands, saving the commands and their output to a text file
+- clearing the history of entered commands
+
+Each of these actions is invoked using a command line option. The ``-r`` or
+``--run`` option runs one or more previously entered commands. To run command
+number 1::
+
+    (Cmd) history --run 1
+
+To rerun the last two commands (there's that double dash again to make argparse
+stop looking for options)::
+
+    (Cmd) history -r -- -2:
+
+Say you want to re-run some previously entered commands, but you would really
+like to make a few changes to them before doing so. When you use the ``-e`` or
+``--edit`` option, ``history`` will write the selected commands out to a text
+file, and open that file with a text editor. You make whatever changes,
+additions, or deletions, you want. When you leave the text editor, all the
+commands in the file are executed. To edit and then re-run commands 2-4 you
+would::
+
+    (Cmd) history --edit 2:4
+
+If you want to save the commands to a text file, but not edit and re-run them,
+use the ``-o`` or ``--output-file`` option. This is a great way to create
+:ref:`scripts`, which can be loaded and executed using the ``load`` command. To
+save the first 5 commands entered in this session to a text file::
+
+    (Cmd) history :5 -o history.txt
+
+The ``history`` command can also save both the commands and their output to a
+text file. This is called a transcript. See :doc:`transcript` for more
+information on how transcripts work, and what you can use them for. To create a
+transcript use the ``-t`` or ``--transcription`` option::
+
+    (Cmd) history 2:3 --transcript transcript.txt
+
+The ``--transcript`` option implies ``--run``: the commands must be re-run in
+order to capture their output to the transcript file.
+
+The last action the history command can perform is to clear the command history
+using ``-c`` or ``--clear``::
+
+    (Cmd) history -c
+
+In addition to these five actions, the ``history`` command also has some options
+to control how the output is formatted. With no arguments, the ``history``
+command displays the command number before each command. This is great when
+displaying history to the screen because it gives you an easy reference to
+identify previously entered commands. However, when creating a script or a
+transcript, the command numbers would prevent the script from loading properly.
+The ``-s`` or ``--script`` option instructs the ``history`` command to suppress
+the line numbers. This option is automatically set by the ``--output-file``,
+``--transcript``, and ``--edit`` options. If you want to output the history
+commands with line numbers to a file, you can do it with output redirection::
+
+    (Cmd) history 1:4 > history.txt
+
+You might use ``-s`` or ``--script`` on it's own if you want to display history
+commands to the screen without line numbers, so you can copy them to the
+clipboard::
+
+    (Cmd) history -s 1:3
+
+``cmd2`` supports both aliases and macros, which allow you to substitute a
+short, more convenient input string with a longer replacement string. Say we
+create an alias like this, and then use it::
+
+    (Cmd) alias create ls shell ls -aF
+    Alias 'ls' created
+    (Cmd) ls -d h*
+    history.txt     htmlcov/
+
+By default, the ``history`` command shows exactly what we typed::
+
+    (Cmd) history
+        1  alias create ls shell ls -aF
+        2  ls -d h*
+
+There are two ways to modify that display so you can see what aliases and macros
+were expanded to. The first is to use ``-x`` or ``--expanded``. These options
+show the expanded command instead of the entered command::
+
+    (Cmd) history -x
+        1  alias create ls shell ls -aF
+        2  shell ls -aF -d h*
+
+If you want to see both the entered command and the expanded command, use the
+``-v`` or ``--verbose`` option::
+
+    (Cmd) history -v
+        1  alias create ls shell ls -aF
+        2  ls -d h*
+        2x shell ls -aF -d h*
+
+If the entered command had no expansion, it is displayed as usual. However, if
+there is some change as the result of expanding macros and aliases, then the
+entered command is displayed with the number, and the expanded command is
+displayed with the number followed by an ``x``.
 
 .. _`Readline Emacs editing mode`: http://readline.kablamo.org/emacs.html
 
@@ -279,7 +478,6 @@ with automatically included ``do_`` methods.
 ( ``!`` is a shortcut for ``shell``; thus ``!ls``
 is equivalent to ``shell ls``.)
 
-
 Transcript-based testing
 ========================
 


Test patch:
diff --git a/tests/test_history.py b/tests/test_history.py
index 06c57b4c..9046e877 100644
--- a/tests/test_history.py
+++ b/tests/test_history.py
@@ -63,26 +63,65 @@ def hist():
     return h
 
 def test_history_class_span(hist):
-    h = hist
-    assert h == ['first', 'second', 'third', 'fourth']
-    assert h.span('-2..') == ['third', 'fourth']
-    assert h.span('2..3') == ['second', 'third']    # Inclusive of end
-    assert h.span('3') == ['third']
-    assert h.span(':') == h
-    assert h.span('2..') == ['second', 'third', 'fourth']
-    assert h.span('-1') == ['fourth']
-    assert h.span('-2..-3') == ['third', 'second']
-    assert h.span('*') == h
+    for tryit in ['*', ':', '-', 'all', 'ALL']:
+        assert hist.span(tryit) == hist
+
+    assert hist.span('3') == ['third']
+    assert hist.span('-1') == ['fourth']
+
+    assert hist.span('2..') == ['second', 'third', 'fourth']
+    assert hist.span('2:') == ['second', 'third', 'fourth']
+
+    assert hist.span('-2..') == ['third', 'fourth']
+    assert hist.span('-2:') == ['third', 'fourth']
+
+    assert hist.span('1..3') == ['first', 'second', 'third']
+    assert hist.span('1:3') == ['first', 'second', 'third']
+    assert hist.span('2:-1') == ['second', 'third', 'fourth']
+    assert hist.span('-3:4') == ['second', 'third','fourth']
+    assert hist.span('-4:-2') == ['first', 'second', 'third']
+
+    assert hist.span(':-2') == ['first', 'second', 'third']
+    assert hist.span('..-2') == ['first', 'second', 'third']
+
+    value_errors = ['fred', 'fred:joe', 'a..b', '2 ..', '1 : 3', '1:0', '0:3']
+    for tryit in value_errors:
+        with pytest.raises(ValueError):
+            hist.span(tryit)
 
 def test_history_class_get(hist):
-    h = hist
-    assert h == ['first', 'second', 'third', 'fourth']
-    assert h.get('') == h
-    assert h.get('-2') == h[:-2]
-    assert h.get('5') == []
-    assert h.get('2-3') == ['second']           # Exclusive of end
-    assert h.get('ir') == ['first', 'third']    # Normal string search for all elements containing "ir"
-    assert h.get('/i.*d/') == ['third']         # Regex string search "i", then anything, then "d"
+    assert hist.get('1') == 'first'
+    assert hist.get(3) == 'third'
+    assert hist.get('-2') == hist[-2]
+    assert hist.get(-1) == 'fourth'
+
+    with pytest.raises(IndexError):
+        hist.get(0)
+    with pytest.raises(IndexError):
+        hist.get('0')
+
+    with pytest.raises(IndexError):
+        hist.get('5')
+    with pytest.raises(ValueError):
+        hist.get('2-3')
+    with pytest.raises(ValueError):
+        hist.get('1..2')
+    with pytest.raises(ValueError):
+        hist.get('3:4')
+    with pytest.raises(ValueError):
+        hist.get('fred')
+    with pytest.raises(ValueError):
+        hist.get('')
+    with pytest.raises(TypeError):
+        hist.get(None)
+
+def test_history_str_search(hist):
+    assert hist.str_search('ir') == ['first', 'third']
+    assert hist.str_search('rth') == ['fourth']
+
+def test_history_regex_search(hist):
+    assert hist.regex_search('/i.*d/') == ['third']
+    assert hist.regex_search('s[a-z]+ond') == ['second']
 
 def test_base_history(base_app):
     run_cmd(base_app, 'help')
@@ -200,11 +239,9 @@ def test_history_with_span_index_error(base_app):
     run_cmd(base_app, 'help')
     run_cmd(base_app, 'help history')
     run_cmd(base_app, '!ls -hal :')
-    out = run_cmd(base_app, 'history "hal :"')
-    expected = normalize("""
-    3  !ls -hal :
-""")
-    assert out == expected
+    with pytest.raises(ValueError):
+        _, err = run_cmd(base_app, 'history "hal :"')
+        assert "ValueError" in err
 
 def test_history_output_file(base_app):
     run_cmd(base_app, 'help')
@@ -358,8 +395,8 @@ def test_existing_history_file(hist_file, capsys):
         pass
 
     # Create a new cmd2 app
-    app = cmd2.Cmd(persistent_history_file=hist_file)
-    out, err = capsys.readouterr()
+    cmd2.Cmd(persistent_history_file=hist_file)
+    _, err = capsys.readouterr()
 
     # Make sure there were no errors
     assert err == ''
@@ -381,8 +418,8 @@ def test_new_history_file(hist_file, capsys):
         pass
 
     # Create a new cmd2 app
-    app = cmd2.Cmd(persistent_history_file=hist_file)
-    out, err = capsys.readouterr()
+    cmd2.Cmd(persistent_history_file=hist_file)
+    _, err = capsys.readouterr()
 
     # Make sure there were no errors
     assert err == ''
@@ -398,8 +435,8 @@ def test_bad_history_file_path(capsys, request):
     test_dir = os.path.dirname(request.module.__file__)
 
     # Create a new cmd2 app
-    app = cmd2.Cmd(persistent_history_file=test_dir)
-    out, err = capsys.readouterr()
+    cmd2.Cmd(persistent_history_file=test_dir)
+    _, err = capsys.readouterr()
 
     if sys.platform == 'win32':
         # pyreadline masks the read exception. Therefore the bad path error occurs when trying to write the file.
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index 6bfe187e..f93642b8 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -53,7 +53,7 @@ class CmdLineApp(cmd2.Cmd):
         if opts.shout:
             arg = arg.upper()
         repetitions = opts.repeat or 1
-        for i in range(min(repetitions, self.maxrepeats)):
+        for _ in range(min(repetitions, self.maxrepeats)):
             self.poutput(arg)
             # recommend using the poutput function instead of
             # self.stdout.write or "print", because Cmd allows the user
@@ -69,7 +69,7 @@ class CmdLineApp(cmd2.Cmd):
         """Mumbles what you tell me to."""
         repetitions = opts.repeat or 1
         #arg = arg.split()
-        for i in range(min(repetitions, self.maxrepeats)):
+        for _ in range(min(repetitions, self.maxrepeats)):
             output = []
             if random.random() < .33:
                 output.append(random.choice(self.MUMBLE_FIRST))