blob: 71c3de48bf5b436374743b4bf6c55b3a045d4244 [file] [log] [blame]
"""
``grizzled.history`` provides a command line history capability that
provides the same interface across different history implementations.
Currently, it supports three history implementations:
- `GNU Readline`_, which is built into versions of Python on the Mac
and Unix systems
- `pyreadline`_, which many people use on Windows systems
- A dummy fallback history implementation that does nothing, for when readline
isn't available.
The `History` class provides the interface and some common methods for
all history operations.
.. _pyreadline: http://ipython.scipy.org/dist/
.. _GNU Readline: http://cnswww.cns.cwru.edu/php/chet/readline/rluserman.html
To get the appropriate History implementation for the current platform,
simply call the ``get_history()`` factory method.
"""
from __future__ import with_statement
__docformat__ = "restructuredtext en"
# ---------------------------------------------------------------------------
# Imports
# ---------------------------------------------------------------------------
import re
import sys
import logging
import copy
from grizzled.decorators import abstract
from grizzled.exception import ExceptionWithMessage
# ---------------------------------------------------------------------------
# Exports
# ---------------------------------------------------------------------------
__all__ = ['get_history', 'History', 'DEFAULT_MAXLENGTH', 'HistoryError']
__docformat__ = 'restructuredtext'
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
DEFAULT_MAXLENGTH = 512
# ---------------------------------------------------------------------------
# Globals
# ---------------------------------------------------------------------------
log = logging.getLogger('history')
_have_readline = False
_have_pyreadline = False
try:
import readline
_have_readline = True
# Is it pyreadline? If so, it's not quite the same.
try:
_have_pyreadline = readline.rl.__module__.startswith('pyreadline.')
except AttributeError:
pass
except ImportError:
pass
# ---------------------------------------------------------------------------
# Functions
# ---------------------------------------------------------------------------
def get_history(verbose=True):
"""
Factory method to create an appropriate History object.
:Parameters:
verbose : bool
``True`` to display a message on standard output about what
history management mechanism is being used.
:rtype: ``History``
:return: the ``History`` object
"""
global _have_readline
global _have_pyreadline
result = None
if _have_pyreadline:
if verbose:
print 'Using pyreadline for history management.'
result = PyReadlineHistory()
elif _have_readline:
if verbose:
print 'Using readline for history management.'
result = ReadlineHistory()
else:
print 'WARNING: Readline unavailable. There will be no history.'
result = DummyHistory()
result.max_length = DEFAULT_MAXLENGTH
return result
# ---------------------------------------------------------------------------
# Classes
# ---------------------------------------------------------------------------
class HistoryError(ExceptionWithMessage):
"""
Thrown to indicate history errors, when another exception won't do.
"""
pass
class History(object):
"""
Base class for history implementations. All concrete history
implementations must extend this class.
"""
def __init__(self):
self.set_max_length(DEFAULT_MAXLENGTH)
def show(self, out=sys.stdout):
"""
Dump the history to a file-like object (defaulting to standard output).
:Parameters:
out : file
Where to dump the history.
"""
for i in range(1, self.total + 1):
print >> out, '%4d: %s' % (i, self.get_item(i))
def get_last_matching_item(self, command_name):
"""
Get the most recently entered item that matches ``command_name``
at the beginning.
:Parameters:
command_name : str
The string to match against the commands in the history
:rtype: str
:return: the matching string, or ``None``
"""
result = None
for i in range(self.get_total(), 0, -1):
s = self.get_item(i)
tokens = s.split(None, 1)
if len(command_name) <= len(s):
if s[0:len(command_name)] == command_name:
result = s
break
return result
def get_last_item(self):
"""
Get the most recent item in the history.
:rtype: str
:return: The most recent command, or ``None``
"""
return self.get_item(self.get_total() - 1)
def get_item(self, index):
"""
Get an item from the history.
:Parameters:
index : int
0-based index of the item to get. The larger the index
value, the more recent the entry
:rtype: str
:return: the item at that index
:raise IndexError: Index out of range
"""
return None
def set_completer_delims(self, s):
"""
Set the completer delimiters--the characters that delimit tokens
that are eligible for completion.
:Parameters:
s : str
The delimiters
"""
pass
def get_completer_delims(self):
"""
Get the completer delimiters--the characters that delimit tokens
that are eligible for completion.
:rtype: str
:return: the delimiters
"""
return ''
@property
def total(self):
"""
The total number number of commands in the history. Identical to
calling ``get_total()``.
"""
return self.get_total()
def get_total(self):
"""
Get the total number number of commands in the history. Identical to
the ``total`` property.
:rtype: int
:return: the number of commands in the history
"""
return 0
def __set_max_length(self, n):
return self.set_max_length(n)
def __get_max_length(self):
return self.get_max_length()
maxLength = property(__get_max_length, __set_max_length,
doc="The maximum length of the history")
@abstract
def get_max_length(self):
"""
Get the maximum length of the history. This isn't the maximum number
of entries in the in-memory history buffer; instead, it's the maximum
number of entries that will be saved to the history file. Subclasses
*must* provide an implementation of this method.
:rtype: int
:return: the maximum saved size of the history
"""
pass
@abstract
def set_max_length(self, n):
"""
Set the maximum length of the history. This isn't the maximum number
of entries in the in-memory history buffer; instead, it's the maximum
number of entries that will be saved to the history file. Subclasses
*must* provide an implementation of this method.
:Parameters:
n : int
the maximum saved size of the history
"""
pass
@abstract
def add_item(self, line):
"""
Add (append) a line to the history buffer. Subclasses *must* provide
an implementation of this method.
:Parameters:
line : str
the command to append to the history
"""
pass
@abstract
def remove_item(self, i):
"""
Remove a line from the history buffer. Subclasses *must* provide an
implementation of this method.
:Parameters:
i : int
the 0-based index of the item to be removed
"""
pass
@abstract
def clear_history(self):
"""
Clear the history buffer. Subclasses *must* provide an
implementation of this method.
"""
pass
def get_history_list(self):
"""
Get a copy of the history buffer.
:rtype: list
:return: a list of commands from the history
"""
result = []
for i in range(1, self.total + 1):
result += [self.get_item(i)]
return result
def remove_matches(self, regexp_string):
"""
Remove all history items that match a regular expression.
:Parameters:
regexp_string : str
the uncompiled regular expression to match
:raise HistoryError: bad regular expression
"""
try:
pat = re.compile(regexp_string)
except:
raise HistoryError(str(sys.exc_info[1]))
buf = []
for i in range(1, self.total + 1):
s = self.get_item(i)
if not pat.match(s):
buf += [s]
self.replace_history(buf)
def cut_back_to(self, index):
"""
Cut the history back to the specified index, removing all entries
more recent than that index.
:Parameters:
index : int
the index of the command that should become the last command
in the history
:raise IndexError: index out of range
"""
if (index > 0) and (index <= self.total):
buf = []
for i in range(1, index):
buf += [self.get_item(i)]
self.replace_history(buf)
def replace_history(self, commands):
"""
Replace the entire contents of the history with another set of values
:Parameters:
commands : list
List of strings to put in the history after clearing it of any
existing entries
"""
self.clear_history()
for command in commands:
self.add_item(command, force=True)
def save_history_file(self, path):
"""
Save the history to a file. The file is overwritten with the contents
of the history buffer.
:Parameters:
path : str
Path to the history file to receive the output.
:raise IOError: Unable to open file
"""
log.debug('Writing history file "%s"' % path)
with open(path, 'w') as f:
for i in range(1, self.total + 1):
f.write(self.get_item(i) + '\n')
def load_history_file(self, path):
"""
Load the history buffer with the contents of a file, completely
replacing the in-memory history with the file's contents.
:Parameters:
path : str
Path to the history file to read
:raise IOError: Unable to open file
"""
log.debug('Loading history file "%s"' % path)
with open(path, 'r') as f:
buf = []
for line in f:
buf += [line.strip()]
max = self.get_max_length()
if len(buf) > max:
buf = buf[max]
self.replace_history(buf)
class ReadlineHistory(History):
def __init__(self):
global _have_readline
assert(_have_readline)
History.__init__(self)
def get_item(self, index):
return readline.get_history_item(index)
def get_total(self):
return readline.get_current_history_length()
def set_completer_delims(self, s):
readline.set_completer_delims(s)
def get_completer_delims(self,):
return readline.get_completer_delims()
def remove_item(self, index):
# readline.remove_history_item() doesn't seem to work. Do it the
# hard way.
#try:
# readline.remove_history_item(i)
#except ValueError:
# pass
buf = []
for i in range(1, self.total + 1):
if i != index:
buf += self.get_item(i)
self.clear_history()
for s in buf:
readline.add_history(s)
def clear_history(self):
try:
readline.clear_history()
except AttributeError:
len = self.get_max_length()
readline.set_history_length(0)
readline.set_history_length(len)
def get_max_length(self):
return readline.get_history_length()
def set_max_length(self, n):
readline.set_history_length(n)
def add_item(self, line, force=False):
readline.add_history(line)
class PyReadlineHistory(ReadlineHistory):
def __init__(self):
global _have_pyreadline
assert(_have_pyreadline)
ReadlineHistory.__init__(self)
def get_item(self, index):
return self.__get_buf()[index - 1].get_line_text()
def get_total(self):
return len(self.__get_buf())
def set_completer_delims(self, s):
readline.set_completer_delims(s)
def get_completer_delims(self):
return readline.get_completer_delims()
def remove_item(self, index):
buf = copy.deepcopy(self.__get_buf())
self.clear_history()
for s in buf:
readline.add_history(s)
def clear_history(self):
readline.clear_history()
def get_max_length(self):
return readline.get_history_length()
def set_max_length(self, n):
readline.set_history_length(n)
def add_item(self, line, force=False):
# Kludge. pyreadline is a pain in the ass.
from pyreadline import lineobj
from pyreadline.unicode_helper import ensure_unicode
line = ensure_unicode(line.rstrip())
readline.add_history(lineobj.ReadLineTextBuffer(line))
def __get_buf(self):
return readline.rl._history.history
class DummyHistory(History):
def __init__(self):
History.__init__(self)
def remove_item(self, i):
pass
def get_item(self, index):
return None
def get_history_list(self):
return []
def get_total(self):
return 0
def get_max_length(self):
return 0
def set_max_length(self, n):
pass
def clear_history(self):
pass
def add_item(self, line, force=False):
pass
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
if __name__ == "__main__":
h = getHistory()