Skip to content

Commit 4a765e1

Browse files
authored
Merge pull request #270 from python-cmd2/persistent_history
Added optional persistent readline history feature
2 parents 4895d5d + f3c6b1b commit 4a765e1

11 files changed

Lines changed: 125 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 0.8.1 (TBD, 2018)
2+
3+
* Enhancements
4+
* Added support for sub-menus.
5+
* See [submenus.py](https://github.com/python-cmd2/cmd2/blob/master/examples/submenus.py) for an example of how to use it
6+
* Added option for persistent readline history
7+
* See [persistent_history.py](https://github.com/python-cmd2/cmd2/blob/master/examples/persistent_history.py) for an example
8+
* See the [Searchable command history](http://cmd2.readthedocs.io/en/latest/freefeatures.html#searchable-command-history) section of the documentation for more info
9+
110
## 0.8.0 (February 1, 2018)
211
* Bug Fixes
312
* Fixed unit tests on Python 3.7 due to changes in how re.escape() behaves in Python 3.7

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ when using cmd.
1818

1919
Main Features
2020
-------------
21-
- Searchable command history (`history` command and `<Ctrl>+r`)
21+
- Searchable command history (`history` command and `<Ctrl>+r`) - optionally persistent
2222
- Text file scripting of your application with `load` (`@`) and `_relative_load` (`@@`)
2323
- Python scripting of your application with ``pyscript``
2424
- Run shell commands with ``!``
@@ -30,6 +30,7 @@ Main Features
3030
- Special-character command shortcuts (beyond cmd's `@` and `!`)
3131
- Settable environment parameters
3232
- Parsing commands with arguments using `argparse`, including support for sub-commands
33+
- Sub-menu support via the ``AddSubmenu`` decorator
3334
- Unicode character support (*Python 3 only*)
3435
- Good tab-completion of commands, sub-commands, file system paths, and shell commands
3536
- Python 2.7 and 3.4+ support

cmd2.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@
2424
2525
Git repository on GitHub at https://github.com/python-cmd2/cmd2
2626
"""
27+
import argparse
28+
import atexit
2729
import cmd
2830
import codecs
2931
import collections
3032
import datetime
3133
import glob
3234
import io
3335
import optparse
34-
import argparse
3536
import os
3637
import platform
3738
import re
@@ -112,7 +113,7 @@
112113
except ImportError:
113114
pass
114115

115-
__version__ = '0.8.0'
116+
__version__ = '0.8.1'
116117

117118
# Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past
118119
pyparsing.ParserElement.enablePackrat()
@@ -549,6 +550,7 @@ def strip_ansi(text):
549550

550551
def _pop_readline_history(clear_history=True):
551552
"""Returns a copy of readline's history and optionally clears it (default)"""
553+
# noinspection PyArgumentList
552554
history = [
553555
readline.get_history_item(i)
554556
for i in range(1, 1 + readline.get_current_history_length())
@@ -689,6 +691,7 @@ def enter_submenu(parent_cmd, line):
689691
)
690692
submenu.cmdloop()
691693
if self.reformat_prompt is not None:
694+
# noinspection PyUnboundLocalVariable
692695
self.submenu.prompt = prompt
693696
_push_readline_history(history)
694697
finally:
@@ -761,12 +764,12 @@ class _Cmd(cmd_obj):
761764
_Cmd.complete_help = _complete_submenu_help
762765

763766
# Create bindings in the parent command to the submenus commands.
764-
setattr(_Cmd, 'do_' + self.command, enter_submenu)
767+
setattr(_Cmd, 'do_' + self.command, enter_submenu)
765768
setattr(_Cmd, 'complete_' + self.command, complete_submenu)
766769

767770
# Create additional bindings for aliases
768771
for _alias in self.aliases:
769-
setattr(_Cmd, 'do_' + _alias, enter_submenu)
772+
setattr(_Cmd, 'do_' + _alias, enter_submenu)
770773
setattr(_Cmd, 'complete_' + _alias, complete_submenu)
771774
return _Cmd
772775

@@ -833,12 +836,15 @@ class Cmd(cmd.Cmd):
833836
'quiet': "Don't print nonessential feedback",
834837
'timing': 'Report execution times'}
835838

836-
def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False, transcript_files=None):
839+
def __init__(self, completekey='tab', stdin=None, stdout=None, persistent_history_file='',
840+
persistent_history_length=1000, use_ipython=False, transcript_files=None):
837841
"""An easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.
838842
839843
:param completekey: str - (optional) readline name of a completion key, default to Tab
840844
:param stdin: (optional) alternate input file object, if not specified, sys.stdin is used
841845
:param stdout: (optional) alternate output file object, if not specified, sys.stdout is used
846+
:param persistent_history_file: str - (optional) file path to load a persistent readline history from
847+
:param persistent_history_length: int - (optional) max number of lines which will be written to the history file
842848
:param use_ipython: (optional) should the "ipy" command be included for an embedded IPython shell
843849
:param transcript_files: str - (optional) allows running transcript tests when allow_cli_args is False
844850
"""
@@ -849,6 +855,17 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
849855
except AttributeError:
850856
pass
851857

858+
# If persistent readline history is enabled, then read history from file and register to write to file at exit
859+
if persistent_history_file:
860+
persistent_history_file = os.path.expanduser(persistent_history_file)
861+
try:
862+
readline.read_history_file(persistent_history_file)
863+
# default history len is -1 (infinite), which may grow unruly
864+
readline.set_history_length(persistent_history_length)
865+
except FileNotFoundError:
866+
pass
867+
atexit.register(readline.write_history_file, persistent_history_file)
868+
852869
# Call super class constructor. Need to do it in this way for Python 2 and 3 compatibility
853870
cmd.Cmd.__init__(self, completekey=completekey, stdin=stdin, stdout=stdout)
854871

@@ -2901,6 +2918,7 @@ def namedtuple_with_two_defaults(typename, field_names, default_values=('', ''))
29012918
:return: namedtuple type
29022919
"""
29032920
T = collections.namedtuple(typename, field_names)
2921+
# noinspection PyUnresolvedReferences
29042922
T.__new__.__defaults__ = default_values
29052923
return T
29062924

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
# The short X.Y version.
6363
version = '0.8'
6464
# The full version, including alpha/beta/rc tags.
65-
release = '0.8.0'
65+
release = '0.8.1'
6666

6767
# The language for content autogenerated by Sphinx. Refer to documentation
6868
# for a list of supported languages.

docs/freefeatures.rst

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Script files
1313
============
1414

1515
Text files can serve as scripts for your ``cmd2``-based
16-
application, with the ``load``, ``_relative_load``, ``edit`` and ``history`` commands.
16+
application, with the ``load``, ``_relative_load``, and ``edit`` commands.
1717

1818
Both ASCII and UTF-8 encoded unicode text files are supported.
1919

@@ -25,8 +25,6 @@ Simply include one command per line, typed exactly as you would inside a ``cmd2`
2525

2626
.. automethod:: cmd2.Cmd.do_edit
2727

28-
.. automethod:: cmd2.Cmd.do_history
29-
3028

3129
Comments
3230
========
@@ -250,17 +248,22 @@ Searchable command history
250248
==========================
251249

252250
All cmd_-based applications have access to previous commands with
253-
the up- and down- cursor keys.
251+
the up- and down- arrow keys.
254252

255253
All cmd_-based applications on systems with the ``readline`` module
256-
also provide `bash-like history list editing`_.
254+
also provide `Readline Emacs editing mode`_. With this you can, for example, use **Ctrl-r** to search backward through
255+
the readline history.
257256

258-
.. _`bash-like history list editing`: http://www.talug.org/events/20030709/cmdline_history.html
257+
``cmd2`` adds the option of making this readline history persistent via optional arguments to ``cmd2.Cmd.__init__()``:
258+
259+
.. automethod:: cmd2.Cmd.__init__
259260

260261
``cmd2`` makes a third type of history access available with the **history** command:
261262

262263
.. automethod:: cmd2.Cmd.do_history
263264

265+
.. _`Readline Emacs editing mode`: http://readline.kablamo.org/emacs.html
266+
264267
Quitting the application
265268
========================
266269

docs/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pyparsing
22
six
33
pyperclip
4+
contextlib2

examples/persistent_history.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python
2+
# coding=utf-8
3+
"""This example demonstrates how to enable persistent readline history in your cmd2 application.
4+
5+
This will allow end users of your cmd2-based application to use the arrow keys and Ctrl+r in a manner which persists
6+
across invocations of your cmd2 application. This can make it much easier for them to use your application.
7+
"""
8+
import cmd2
9+
10+
11+
class Cmd2PersistentHistory(cmd2.Cmd):
12+
"""Basic example of how to enable persistent readline history within your cmd2 app."""
13+
def __init__(self, hist_file):
14+
"""Configure the app to load persistent readline history from a file.
15+
16+
:param hist_file: file to load readline history from at start and write it to at end
17+
"""
18+
cmd2.Cmd.__init__(self, persistent_history_file=hist_file, persistent_history_length=500)
19+
self.allow_cli_args = False
20+
self.prompt = 'ph> '
21+
22+
# ... your class code here ...
23+
24+
25+
if __name__ == '__main__':
26+
import sys
27+
28+
history_file = '~/.persistent_history.cmd2'
29+
if len(sys.argv) > 1:
30+
history_file = sys.argv[1]
31+
32+
app = Cmd2PersistentHistory(hist_file=history_file)
33+
app.cmdloop()

examples/submenus.py

100644100755
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
#!/usr/bin/env python
2+
# coding=utf-8
23
"""
34
Create a CLI with a nested command structure as follows. The commands 'second' and 'third' navigate the CLI to the scope
45
of the submenu. Nesting of the submenus is done with the cmd2.AddSubmenu() decorator.
56
67
(Top Level)----second----->(2nd Level)----third----->(3rd Level)
78
| | |
89
---> say ---> say ---> say
9-
10-
11-
1210
"""
1311
from __future__ import print_function
1412
import sys
@@ -71,7 +69,6 @@ def complete_say(self, text, line, begidx, endidx):
7169
return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]
7270

7371

74-
7572
@cmd2.AddSubmenu(SecondLevel(),
7673
command='second',
7774
aliases=('second_alias',),
@@ -105,7 +102,6 @@ def complete_say(self, text, line, begidx, endidx):
105102
return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]
106103

107104

108-
109105
if __name__ == '__main__':
110106

111107
root = TopLevel()

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import sys
77
from setuptools import setup
88

9-
VERSION = '0.8.0'
9+
VERSION = '0.8.1'
1010
DESCRIPTION = "cmd2 - a tool for building interactive command line applications in Python"
1111
LONG_DESCRIPTION = """cmd2 is a tool for building interactive command line applications in Python. Its goal is to make
1212
it quick and easy for developers to build feature-rich and user-friendly interactive command line applications. It
@@ -77,7 +77,7 @@
7777
INSTALL_REQUIRES += ['subprocess32']
7878

7979
# unittest.mock was added in Python 3.3. mock is a backport of unittest.mock to all versions of Python
80-
TESTS_REQUIRE = ['mock', 'pytest']
80+
TESTS_REQUIRE = ['mock', 'pytest', 'pexpect']
8181
DOCS_REQUIRE = ['sphinx', 'sphinx_rtd_theme', 'pyparsing', 'pyperclip', 'six']
8282

8383
setup(

tests/test_cmd2.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import tempfile
1212

1313
import mock
14+
import pexpect
1415
import pytest
1516
import six
1617

@@ -25,7 +26,7 @@
2526

2627

2728
def test_ver():
28-
assert cmd2.__version__ == '0.8.0'
29+
assert cmd2.__version__ == '0.8.1'
2930

3031

3132
def test_empty_statement(base_app):
@@ -1525,3 +1526,37 @@ def test_poutput_none(base_app):
15251526
out = base_app.stdout.buffer
15261527
expected = ''
15271528
assert out == expected
1529+
1530+
1531+
@pytest.mark.skipif(sys.platform == 'win32' or sys.platform.startswith('lin'),
1532+
reason="pexpect doesn't have a spawn() function on Windows and readline doesn't work on TravisCI")
1533+
def test_persistent_history(request):
1534+
"""Will run on macOS to verify expected persistent history behavior."""
1535+
test_dir = os.path.dirname(request.module.__file__)
1536+
persistent_app = os.path.join(test_dir, '..', 'examples', 'persistent_history.py')
1537+
1538+
python = 'python3'
1539+
if six.PY2:
1540+
python = 'python2'
1541+
1542+
command = '{} {}'.format(python, persistent_app)
1543+
1544+
# Start an instance of the persistent history example and send it a few commands
1545+
child = pexpect.spawn(command)
1546+
prompt = 'ph> '
1547+
child.expect(prompt)
1548+
child.sendline('help')
1549+
child.expect(prompt)
1550+
child.sendline('help history')
1551+
child.expect(prompt)
1552+
child.sendline('quit')
1553+
child.close()
1554+
1555+
# Start a 2nd instance of the persistent history example and send it an up arrow to verify persistent history
1556+
up_arrow = '\x1b[A'
1557+
child2 = pexpect.spawn(command)
1558+
child2.expect(prompt)
1559+
child2.send(up_arrow)
1560+
child2.expect('quit')
1561+
assert child2.after == b'quit'
1562+
child2.close()

0 commit comments

Comments
 (0)