Skip to content
Merged
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 0.8.1 (TBD, 2018)

* Enhancements
* Added support for sub-menus.
* See [submenus.py](https://github.com/python-cmd2/cmd2/blob/master/examples/submenus.py) for an example of how to use it
* Added option for persistent readline history
* See [persistent_history.py](https://github.com/python-cmd2/cmd2/blob/master/examples/persistent_history.py) for an example
* See the [Searchable command history](http://cmd2.readthedocs.io/en/latest/freefeatures.html#searchable-command-history) section of the documentation for more info

## 0.8.0 (February 1, 2018)
* Bug Fixes
* Fixed unit tests on Python 3.7 due to changes in how re.escape() behaves in Python 3.7
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ when using cmd.

Main Features
-------------
- Searchable command history (`history` command and `<Ctrl>+r`)
- Searchable command history (`history` command and `<Ctrl>+r`) - optionally persistent
- Text file scripting of your application with `load` (`@`) and `_relative_load` (`@@`)
- Python scripting of your application with ``pyscript``
- Run shell commands with ``!``
Expand All @@ -30,6 +30,7 @@ Main Features
- Special-character command shortcuts (beyond cmd's `@` and `!`)
- Settable environment parameters
- Parsing commands with arguments using `argparse`, including support for sub-commands
- Sub-menu support via the ``AddSubmenu`` decorator
- Unicode character support (*Python 3 only*)
- Good tab-completion of commands, sub-commands, file system paths, and shell commands
- Python 2.7 and 3.4+ support
Expand Down
28 changes: 23 additions & 5 deletions cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@

Git repository on GitHub at https://github.com/python-cmd2/cmd2
"""
import argparse
import atexit
import cmd
import codecs
import collections
import datetime
import glob
import io
import optparse
import argparse
import os
import platform
import re
Expand Down Expand Up @@ -112,7 +113,7 @@
except ImportError:
pass

__version__ = '0.8.0'
__version__ = '0.8.1'

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

def _pop_readline_history(clear_history=True):
"""Returns a copy of readline's history and optionally clears it (default)"""
# noinspection PyArgumentList
history = [
readline.get_history_item(i)
for i in range(1, 1 + readline.get_current_history_length())
Expand Down Expand Up @@ -689,6 +691,7 @@ def enter_submenu(parent_cmd, line):
)
submenu.cmdloop()
if self.reformat_prompt is not None:
# noinspection PyUnboundLocalVariable
self.submenu.prompt = prompt
_push_readline_history(history)
finally:
Expand Down Expand Up @@ -761,12 +764,12 @@ class _Cmd(cmd_obj):
_Cmd.complete_help = _complete_submenu_help

# Create bindings in the parent command to the submenus commands.
setattr(_Cmd, 'do_' + self.command, enter_submenu)
setattr(_Cmd, 'do_' + self.command, enter_submenu)
setattr(_Cmd, 'complete_' + self.command, complete_submenu)

# Create additional bindings for aliases
for _alias in self.aliases:
setattr(_Cmd, 'do_' + _alias, enter_submenu)
setattr(_Cmd, 'do_' + _alias, enter_submenu)
setattr(_Cmd, 'complete_' + _alias, complete_submenu)
return _Cmd

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

def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False, transcript_files=None):
def __init__(self, completekey='tab', stdin=None, stdout=None, persistent_history_file='',
persistent_history_length=1000, use_ipython=False, transcript_files=None):
"""An easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.

:param completekey: str - (optional) readline name of a completion key, default to Tab
:param stdin: (optional) alternate input file object, if not specified, sys.stdin is used
:param stdout: (optional) alternate output file object, if not specified, sys.stdout is used
:param persistent_history_file: str - (optional) file path to load a persistent readline history from
:param persistent_history_length: int - (optional) max number of lines which will be written to the history file
:param use_ipython: (optional) should the "ipy" command be included for an embedded IPython shell
:param transcript_files: str - (optional) allows running transcript tests when allow_cli_args is False
"""
Expand All @@ -849,6 +855,17 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
except AttributeError:
pass

# If persistent readline history is enabled, then read history from file and register to write to file at exit
if persistent_history_file:
persistent_history_file = os.path.expanduser(persistent_history_file)
try:
readline.read_history_file(persistent_history_file)
# default history len is -1 (infinite), which may grow unruly
readline.set_history_length(persistent_history_length)
except FileNotFoundError:
pass
atexit.register(readline.write_history_file, persistent_history_file)

# Call super class constructor. Need to do it in this way for Python 2 and 3 compatibility
cmd.Cmd.__init__(self, completekey=completekey, stdin=stdin, stdout=stdout)

Expand Down Expand Up @@ -2901,6 +2918,7 @@ def namedtuple_with_two_defaults(typename, field_names, default_values=('', ''))
:return: namedtuple type
"""
T = collections.namedtuple(typename, field_names)
# noinspection PyUnresolvedReferences
T.__new__.__defaults__ = default_values
return T

Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
# The short X.Y version.
version = '0.8'
# The full version, including alpha/beta/rc tags.
release = '0.8.0'
release = '0.8.1'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
15 changes: 9 additions & 6 deletions docs/freefeatures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Script files
============

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

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

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

.. automethod:: cmd2.Cmd.do_edit

.. automethod:: cmd2.Cmd.do_history


Comments
========
Expand Down Expand Up @@ -250,17 +248,22 @@ Searchable command history
==========================

All cmd_-based applications have access to previous commands with
the up- and down- cursor keys.
the up- and down- arrow keys.

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

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

.. automethod:: cmd2.Cmd.__init__

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

.. automethod:: cmd2.Cmd.do_history

.. _`Readline Emacs editing mode`: http://readline.kablamo.org/emacs.html

Quitting the application
========================

Expand Down
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pyparsing
six
pyperclip
contextlib2
33 changes: 33 additions & 0 deletions examples/persistent_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env python
# coding=utf-8
"""This example demonstrates how to enable persistent readline history in your cmd2 application.

This will allow end users of your cmd2-based application to use the arrow keys and Ctrl+r in a manner which persists
across invocations of your cmd2 application. This can make it much easier for them to use your application.
"""
import cmd2


class Cmd2PersistentHistory(cmd2.Cmd):
"""Basic example of how to enable persistent readline history within your cmd2 app."""
def __init__(self, hist_file):
"""Configure the app to load persistent readline history from a file.

:param hist_file: file to load readline history from at start and write it to at end
"""
cmd2.Cmd.__init__(self, persistent_history_file=hist_file, persistent_history_length=500)
self.allow_cli_args = False
self.prompt = 'ph> '

# ... your class code here ...


if __name__ == '__main__':
import sys

history_file = '~/.persistent_history.cmd2'
if len(sys.argv) > 1:
history_file = sys.argv[1]

app = Cmd2PersistentHistory(hist_file=history_file)
app.cmdloop()
6 changes: 1 addition & 5 deletions examples/submenus.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
#!/usr/bin/env python
# coding=utf-8
"""
Create a CLI with a nested command structure as follows. The commands 'second' and 'third' navigate the CLI to the scope
of the submenu. Nesting of the submenus is done with the cmd2.AddSubmenu() decorator.

(Top Level)----second----->(2nd Level)----third----->(3rd Level)
| | |
---> say ---> say ---> say



"""
from __future__ import print_function
import sys
Expand Down Expand Up @@ -71,7 +69,6 @@ def complete_say(self, text, line, begidx, endidx):
return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]



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



if __name__ == '__main__':

root = TopLevel()
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys
from setuptools import setup

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

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

setup(
Expand Down
37 changes: 36 additions & 1 deletion tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import tempfile

import mock
import pexpect
import pytest
import six

Expand All @@ -25,7 +26,7 @@


def test_ver():
assert cmd2.__version__ == '0.8.0'
assert cmd2.__version__ == '0.8.1'


def test_empty_statement(base_app):
Expand Down Expand Up @@ -1525,3 +1526,37 @@ def test_poutput_none(base_app):
out = base_app.stdout.buffer
expected = ''
assert out == expected


@pytest.mark.skipif(sys.platform == 'win32' or sys.platform.startswith('lin'),
reason="pexpect doesn't have a spawn() function on Windows and readline doesn't work on TravisCI")
def test_persistent_history(request):
"""Will run on macOS to verify expected persistent history behavior."""
test_dir = os.path.dirname(request.module.__file__)
persistent_app = os.path.join(test_dir, '..', 'examples', 'persistent_history.py')

python = 'python3'
if six.PY2:
python = 'python2'

command = '{} {}'.format(python, persistent_app)

# Start an instance of the persistent history example and send it a few commands
child = pexpect.spawn(command)
prompt = 'ph> '
child.expect(prompt)
child.sendline('help')
child.expect(prompt)
child.sendline('help history')
child.expect(prompt)
child.sendline('quit')
child.close()

# Start a 2nd instance of the persistent history example and send it an up arrow to verify persistent history
up_arrow = '\x1b[A'
child2 = pexpect.spawn(command)
child2.expect(prompt)
child2.send(up_arrow)
child2.expect('quit')
assert child2.after == b'quit'
child2.close()
8 changes: 8 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ setenv =
deps =
codecov
mock
pexpect
pyparsing
pyperclip
pytest
Expand All @@ -28,6 +29,7 @@ commands =
deps =
codecov
mock
pexpect
pyparsing
pyperclip
pyreadline
Expand All @@ -43,6 +45,7 @@ commands =
[testenv:py34]
deps =
mock
pexpect
pyparsing
pyperclip
pytest
Expand All @@ -53,6 +56,7 @@ commands = py.test -v -n2
[testenv:py35]
deps =
mock
pexpect
pyparsing
pyperclip
pytest
Expand All @@ -63,6 +67,7 @@ commands = py.test -v -n2
[testenv:py35-win]
deps =
mock
pexpect
pyparsing
pyperclip
pyreadline
Expand All @@ -75,6 +80,7 @@ commands = py.test -v -n2
deps =
codecov
mock
pexpect
pyparsing
pyperclip
pytest
Expand All @@ -88,6 +94,7 @@ commands =
[testenv:py36-win]
deps =
mock
pexpect
pyparsing
pyperclip
pyreadline
Expand All @@ -99,6 +106,7 @@ commands = py.test -v -n2
[testenv:py37]
deps =
mock
pexpect
pyparsing
pyperclip
pytest
Expand Down