| 1 |
# -*- coding: utf-8 -*- |
|---|
| 2 |
# |
|---|
| 3 |
# Copyright (C) 2006-2007 Alec Thomas <alec@swapoff.org> |
|---|
| 4 |
# |
|---|
| 5 |
# This software is licensed as described in the file COPYING, which |
|---|
| 6 |
# you should have received as part of this distribution. |
|---|
| 7 |
# |
|---|
| 8 |
|
|---|
| 9 |
"""CLY and readline, together at last. |
|---|
| 10 |
|
|---|
| 11 |
This module uses readline's line editing and tab completion along with CLY's |
|---|
| 12 |
grammar parser to provide an interactive command line environment. |
|---|
| 13 |
|
|---|
| 14 |
It includes support for application specific history files, dynamic prompt, |
|---|
| 15 |
customisable completion key, interactive help and more. |
|---|
| 16 |
|
|---|
| 17 |
*Users can press ? at any time to view contextual help.* |
|---|
| 18 |
""" |
|---|
| 19 |
|
|---|
| 20 |
import os |
|---|
| 21 |
import sys |
|---|
| 22 |
import types |
|---|
| 23 |
import cly.console as console |
|---|
| 24 |
from cly.exceptions import Error, ParseError |
|---|
| 25 |
from cly.builder import Grammar |
|---|
| 26 |
from cly.parser import Parser |
|---|
| 27 |
|
|---|
| 28 |
try: |
|---|
| 29 |
import readline |
|---|
| 30 |
except ImportError: |
|---|
| 31 |
readline = None |
|---|
| 32 |
try: |
|---|
| 33 |
from cly import _rlext |
|---|
| 34 |
except ImportError: |
|---|
| 35 |
_rlext = None |
|---|
| 36 |
try: |
|---|
| 37 |
import pyreadline |
|---|
| 38 |
except ImportError: |
|---|
| 39 |
pyreadline = None |
|---|
| 40 |
|
|---|
| 41 |
|
|---|
| 42 |
__all__ = ['Interact', 'interact', 'brief_exceptions', 'verbose_exceptions', |
|---|
| 43 |
'debug_exceptions'] |
|---|
| 44 |
__docformat__ = 'restructuredtext en' |
|---|
| 45 |
|
|---|
| 46 |
|
|---|
| 47 |
# Interact.loop exception modifiers |
|---|
| 48 |
def brief_exceptions(interact, context, completing, e): |
|---|
| 49 |
"""Display the string summary for exceptions.""" |
|---|
| 50 |
if not completing: |
|---|
| 51 |
console.cerror(str(e)) |
|---|
| 52 |
|
|---|
| 53 |
def verbose_exceptions(interact, context, completing, e): |
|---|
| 54 |
if not completing: |
|---|
| 55 |
interact.dump_traceback(e) |
|---|
| 56 |
|
|---|
| 57 |
def debug_exceptions(interact, context, completing, e): |
|---|
| 58 |
interact.dump_traceback(e) |
|---|
| 59 |
|
|---|
| 60 |
|
|---|
| 61 |
class InputDriver(object): |
|---|
| 62 |
"""Abstraction for line input.""" |
|---|
| 63 |
|
|---|
| 64 |
def __init__(self, parser, prompt, history_file, history_length): |
|---|
| 65 |
self.parser = parser |
|---|
| 66 |
self.prompt = prompt |
|---|
| 67 |
self.history_file = history_file |
|---|
| 68 |
self.history_length = history_length |
|---|
| 69 |
|
|---|
| 70 |
def enter(self): |
|---|
| 71 |
"""Enter the input context.""" |
|---|
| 72 |
|
|---|
| 73 |
def input(self): |
|---|
| 74 |
"""Input one line from user and return it.""" |
|---|
| 75 |
raise NotImplementedError |
|---|
| 76 |
|
|---|
| 77 |
def leave(self): |
|---|
| 78 |
"""Exit the input context.""" |
|---|
| 79 |
|
|---|
| 80 |
@staticmethod |
|---|
| 81 |
def usable(): |
|---|
| 82 |
"""Called to determine whether this driver is usable.""" |
|---|
| 83 |
raise NotImplementedError |
|---|
| 84 |
|
|---|
| 85 |
|
|---|
| 86 |
class DumbInput(InputDriver): |
|---|
| 87 |
"""The horror.""" |
|---|
| 88 |
|
|---|
| 89 |
def input(self): |
|---|
| 90 |
return raw_input(self.prompt) |
|---|
| 91 |
|
|---|
| 92 |
@staticmethod |
|---|
| 93 |
def usable(): |
|---|
| 94 |
print >> sys.stderr, \ |
|---|
| 95 |
'WARNING: Most line editing features are unavailable.' |
|---|
| 96 |
return True |
|---|
| 97 |
|
|---|
| 98 |
|
|---|
| 99 |
class ReadlineDriver(InputDriver): |
|---|
| 100 |
"""Base class for readline variants.""" |
|---|
| 101 |
|
|---|
| 102 |
def __init__(self, *args, **kwargs): |
|---|
| 103 |
super(ReadlineDriver, self).__init__(*args, **kwargs) |
|---|
| 104 |
self._cli_inject_text = '' |
|---|
| 105 |
self._completion_candidates = [] |
|---|
| 106 |
|
|---|
| 107 |
def enter(self): |
|---|
| 108 |
try: |
|---|
| 109 |
readline.set_history_length(self.history_length) |
|---|
| 110 |
readline.read_history_file(self.history_file) |
|---|
| 111 |
except: |
|---|
| 112 |
pass |
|---|
| 113 |
|
|---|
| 114 |
readline.parse_and_bind('tab: complete') |
|---|
| 115 |
readline.set_completer_delims(' \t') |
|---|
| 116 |
readline.set_completer(self._completion) |
|---|
| 117 |
readline.set_startup_hook(self._redraw_input) |
|---|
| 118 |
|
|---|
| 119 |
self._bind_help() |
|---|
| 120 |
|
|---|
| 121 |
def input(self): |
|---|
| 122 |
return raw_input(self.prompt) |
|---|
| 123 |
|
|---|
| 124 |
def leave(self): |
|---|
| 125 |
try: |
|---|
| 126 |
readline.write_history_file(self.history_file) |
|---|
| 127 |
except: |
|---|
| 128 |
pass |
|---|
| 129 |
|
|---|
| 130 |
@staticmethod |
|---|
| 131 |
def usable(): |
|---|
| 132 |
if readline: |
|---|
| 133 |
print >> sys.stderr, \ |
|---|
| 134 |
'WARNING: neither pyreadline nor CLY\'s built-in readline ' \ |
|---|
| 135 |
'extensions found,\n contextual help is not ' \ |
|---|
| 136 |
'available.' |
|---|
| 137 |
return readline |
|---|
| 138 |
|
|---|
| 139 |
def _bind_help(self): |
|---|
| 140 |
pass |
|---|
| 141 |
|
|---|
| 142 |
def _force_redisplay(self): |
|---|
| 143 |
raise NotImplementedError |
|---|
| 144 |
|
|---|
| 145 |
def _get_cursor(self): |
|---|
| 146 |
raise NotImplementedError |
|---|
| 147 |
|
|---|
| 148 |
def _set_cursor(self, cursor): |
|---|
| 149 |
raise NotImplementedError |
|---|
| 150 |
|
|---|
| 151 |
cursor = property(lambda s: s._get_cursor(), lambda s, c: s._set_cursor(c)) |
|---|
| 152 |
|
|---|
| 153 |
# Internal methods |
|---|
| 154 |
def _completion(self, text, state): |
|---|
| 155 |
line = readline.get_line_buffer()[0:readline.get_begidx()] |
|---|
| 156 |
ctx = None |
|---|
| 157 |
try: |
|---|
| 158 |
result = self.parser.parse(line) |
|---|
| 159 |
if not state: |
|---|
| 160 |
try: |
|---|
| 161 |
self._completion_candidates = list(result.candidates(text)) |
|---|
| 162 |
except Exception, e: |
|---|
| 163 |
Interact.dump_traceback(e) |
|---|
| 164 |
self._force_redisplay() |
|---|
| 165 |
raise |
|---|
| 166 |
if self._completion_candidates: |
|---|
| 167 |
return self._completion_candidates.pop() |
|---|
| 168 |
return None |
|---|
| 169 |
except cly.Error: |
|---|
| 170 |
return None |
|---|
| 171 |
|
|---|
| 172 |
def _redraw_input(self): |
|---|
| 173 |
readline.insert_text(self._cli_inject_text) |
|---|
| 174 |
self._cli_inject_text = '' |
|---|
| 175 |
|
|---|
| 176 |
def _show_help(self, key, count): |
|---|
| 177 |
try: |
|---|
| 178 |
command = readline.get_line_buffer()[:self.cursor] |
|---|
| 179 |
context = self.parser.parse(command) |
|---|
| 180 |
if context.remaining.strip(): |
|---|
| 181 |
print |
|---|
| 182 |
candidates = [help[1] for help in context.help()] |
|---|
| 183 |
text = '%s^ invalid token (candidates are %s)' % \ |
|---|
| 184 |
(' ' * (context.cursor + len(self.prompt)), |
|---|
| 185 |
', '.join(candidates)) |
|---|
| 186 |
console.cerror(text) |
|---|
| 187 |
self._force_redisplay() |
|---|
| 188 |
return |
|---|
| 189 |
help = context.help() |
|---|
| 190 |
print |
|---|
| 191 |
console.cprint('\n'.join(help.format())) |
|---|
| 192 |
self._force_redisplay() |
|---|
| 193 |
return 0 |
|---|
| 194 |
except Exception, e: |
|---|
| 195 |
Interact.dump_traceback(e) |
|---|
| 196 |
self._force_redisplay() |
|---|
| 197 |
return 0 |
|---|
| 198 |
|
|---|
| 199 |
|
|---|
| 200 |
class PyReadlineDriver(ReadlineDriver): |
|---|
| 201 |
"""The IPython pure-Python pyreadline implementation.""" |
|---|
| 202 |
|
|---|
| 203 |
@staticmethod |
|---|
| 204 |
def usable(): |
|---|
| 205 |
return pyreadline |
|---|
| 206 |
|
|---|
| 207 |
def _bind_help(self): |
|---|
| 208 |
def _show_help_proxy(_, __): |
|---|
| 209 |
self._show_help(None, None) |
|---|
| 210 |
|
|---|
| 211 |
pyreadline.rl.mode.cly_help = types.MethodType( |
|---|
| 212 |
_show_help_proxy, pyreadline.rl.mode, pyreadline.Readline |
|---|
| 213 |
) |
|---|
| 214 |
pyreadline.parse_and_bind('?: cly-help') |
|---|
| 215 |
pyreadline.parse_and_bind('Shift-?: cly-help') |
|---|
| 216 |
pyreadline.parse_and_bind('F1: cly-help') |
|---|
| 217 |
|
|---|
| 218 |
def _force_redisplay(self): |
|---|
| 219 |
pyreadline.rl._print_prompt() |
|---|
| 220 |
|
|---|
| 221 |
def _get_cursor(self): |
|---|
| 222 |
return pyreadline.rl.l_buffer.point |
|---|
| 223 |
|
|---|
| 224 |
def _set_cursor(self, cursor): |
|---|
| 225 |
pyreadline.rl.l_buffer.point = cursor |
|---|
| 226 |
|
|---|
| 227 |
|
|---|
| 228 |
class ExtendedReadlineDriver(ReadlineDriver): |
|---|
| 229 |
"""Use CLY's built-in readline extensions.""" |
|---|
| 230 |
|
|---|
| 231 |
@staticmethod |
|---|
| 232 |
def usable(): |
|---|
| 233 |
return _rlext |
|---|
| 234 |
|
|---|
| 235 |
def _bind_help(self): |
|---|
| 236 |
_rlext.bind_key(ord('?'), self._show_help) |
|---|
| 237 |
|
|---|
| 238 |
def _force_redisplay(self): |
|---|
| 239 |
_rlext.force_redisplay() |
|---|
| 240 |
|
|---|
| 241 |
def _get_cursor(self): |
|---|
| 242 |
return _rlext.cursor() |
|---|
| 243 |
|
|---|
| 244 |
def _set_cursor(self, cursor): |
|---|
| 245 |
_rlext.cursor(cursor) |
|---|
| 246 |
|
|---|
| 247 |
|
|---|
| 248 |
class Interact(object): |
|---|
| 249 |
"""CLY interaction through readline. Due to readline limitations, only one |
|---|
| 250 |
Interact object can be active within an application. |
|---|
| 251 |
|
|---|
| 252 |
Arguments: |
|---|
| 253 |
|
|---|
| 254 |
:grammar_or_parser: The :class:`~cly.parser.Parser` or |
|---|
| 255 |
:class:`~cly.builder.Grammar` to use for |
|---|
| 256 |
interaction. |
|---|
| 257 |
:application: The application name. Used to construct the history file |
|---|
| 258 |
name and prompt, if not provided. |
|---|
| 259 |
:prompt: The prompt. |
|---|
| 260 |
:data: A user-specified object to pass to the parser. The parser builds |
|---|
| 261 |
each parse :class:`~cly.parser.Context` with this object, which |
|---|
| 262 |
in turn will deliver this object on to terminal nodes that have |
|---|
| 263 |
set ``with_context=True``. |
|---|
| 264 |
:with_context: Force current parser :class:`~cly.parser.Context` to be |
|---|
| 265 |
passed to all action nodes, unless they explicitly set |
|---|
| 266 |
the member variable ``with_context=False``. |
|---|
| 267 |
:history_file: Defaults to ``~/.<application>_history``. |
|---|
| 268 |
:history_length: Lines of history to keep. |
|---|
| 269 |
:exceptions: See :meth:`loop`. |
|---|
| 270 |
""" |
|---|
| 271 |
|
|---|
| 272 |
# Available input drivers |
|---|
| 273 |
INPUT_DRIVERS = [ExtendedReadlineDriver, PyReadlineDriver, ReadlineDriver, |
|---|
| 274 |
DumbInput] |
|---|
| 275 |
|
|---|
| 276 |
|
|---|
| 277 |
def __init__(self, grammar_or_parser, application='cly', prompt=None, |
|---|
| 278 |
data=None, history_file=None, history_length=500, |
|---|
| 279 |
exceptions=None): |
|---|
| 280 |
if prompt is None: |
|---|
| 281 |
prompt = application + '> ' |
|---|
| 282 |
if history_file is None: |
|---|
| 283 |
history_file = os.path.expanduser('~/.%s_history' % application) |
|---|
| 284 |
if isinstance(grammar_or_parser, Grammar): |
|---|
| 285 |
parser = Parser(grammar_or_parser, data=data) |
|---|
| 286 |
else: |
|---|
| 287 |
parser = grammar_or_parser |
|---|
| 288 |
assert not data, '"data" ignored because a Parser was passed' |
|---|
| 289 |
|
|---|
| 290 |
self.parser = parser |
|---|
| 291 |
self.exceptions = exceptions or (lambda *a, **kw: True) |
|---|
| 292 |
|
|---|
| 293 |
self.input_driver = self.best_input_driver( |
|---|
| 294 |
parser, prompt, history_file, history_length |
|---|
| 295 |
) |
|---|
| 296 |
|
|---|
| 297 |
def once(self): |
|---|
| 298 |
"""Input one command from the user and return the result of the |
|---|
| 299 |
executed command. |
|---|
| 300 |
""" |
|---|
| 301 |
while True: |
|---|
| 302 |
command = '' |
|---|
| 303 |
try: |
|---|
| 304 |
self.input_driver.enter() |
|---|
| 305 |
try: |
|---|
| 306 |
command = self.input_driver.input() |
|---|
| 307 |
except KeyboardInterrupt: |
|---|
| 308 |
print |
|---|
| 309 |
continue |
|---|
| 310 |
except EOFError: |
|---|
| 311 |
print |
|---|
| 312 |
return None |
|---|
| 313 |
finally: |
|---|
| 314 |
self.input_driver.leave() |
|---|
| 315 |
|
|---|
| 316 |
try: |
|---|
| 317 |
context = self.parser.parse(command) |
|---|
| 318 |
context.execute() |
|---|
| 319 |
except ParseError, e: |
|---|
| 320 |
self.print_error(context, e) |
|---|
| 321 |
return context |
|---|
| 322 |
|
|---|
| 323 |
def loop(self, exceptions=None, every=None): |
|---|
| 324 |
"""Repeatedly read and execute commands from the user. |
|---|
| 325 |
|
|---|
| 326 |
Arguments: |
|---|
| 327 |
:exceptions: A callback used to handle exceptions. It has the |
|---|
| 328 |
signature: |
|---|
| 329 |
|
|---|
| 330 |
exceptions(interact, context, completing, e) => bool |
|---|
| 331 |
|
|---|
| 332 |
context may be None and completing is True if |
|---|
| 333 |
exception was thrown from a completion function. |
|---|
| 334 |
|
|---|
| 335 |
If True is returned the exception will be re-raised. |
|---|
| 336 |
:each: Called with the Interact object before each line is |
|---|
| 337 |
displayed. |
|---|
| 338 |
""" |
|---|
| 339 |
exceptions = exceptions or self.exceptions |
|---|
| 340 |
while True: |
|---|
| 341 |
try: |
|---|
| 342 |
if every: |
|---|
| 343 |
every(self) |
|---|
| 344 |
if not self.once(): |
|---|
| 345 |
break |
|---|
| 346 |
except Exception, e: |
|---|
| 347 |
if exceptions(self, None, False, e): |
|---|
| 348 |
raise |
|---|
| 349 |
|
|---|
| 350 |
def print_error(self, context, e): |
|---|
| 351 |
"""Called by `once()` to print a ParseError.""" |
|---|
| 352 |
candidates = [help[1] for help in context.help()] |
|---|
| 353 |
if len(candidates) > 1: |
|---|
| 354 |
message = '%s (candidates are %s)' |
|---|
| 355 |
else: |
|---|
| 356 |
message = '%s (expected %s)' |
|---|
| 357 |
message = message % (str(e), ', '.join(candidates)) |
|---|
| 358 |
self.error_at_cursor(context, message) |
|---|
| 359 |
|
|---|
| 360 |
def error_at_cursor(self, context, text): |
|---|
| 361 |
"""Attempt to intelligently print an error at the current cursor |
|---|
| 362 |
offset.""" |
|---|
| 363 |
text = str(text) |
|---|
| 364 |
term_width = console.termwidth() |
|---|
| 365 |
indent = ' ' * (context.cursor % term_width |
|---|
| 366 |
+ len(self.prompt)) |
|---|
| 367 |
if len(indent + text) > term_width: |
|---|
| 368 |
console.cerror(indent + '^') |
|---|
| 369 |
console.cerror(text) |
|---|
| 370 |
else: |
|---|
| 371 |
console.cerror(indent + '^ ' + text) |
|---|
| 372 |
|
|---|
| 373 |
@classmethod |
|---|
| 374 |
def dump_traceback(cls, exception): |
|---|
| 375 |
import traceback |
|---|
| 376 |
from StringIO import StringIO |
|---|
| 377 |
out = StringIO() |
|---|
| 378 |
traceback.print_exc(file=out) |
|---|
| 379 |
print >>sys.stderr, str(exception) |
|---|
| 380 |
print >>sys.stderr, out.getvalue() |
|---|
| 381 |
|
|---|
| 382 |
def _get_prompt(self): |
|---|
| 383 |
return self.input_driver.prompt |
|---|
| 384 |
|
|---|
| 385 |
def _set_prompt(self, prompt): |
|---|
| 386 |
self.input_driver.prompt = prompt |
|---|
| 387 |
|
|---|
| 388 |
prompt = property(_get_prompt, _set_prompt, doc='Prompt. Can be set.') |
|---|
| 389 |
|
|---|
| 390 |
|
|---|
| 391 |
@classmethod |
|---|
| 392 |
def best_input_driver(cls, *args, **kwargs): |
|---|
| 393 |
"""Select the "best" available input driver.""" |
|---|
| 394 |
for driver in cls.INPUT_DRIVERS: |
|---|
| 395 |
if driver.usable(): |
|---|
| 396 |
return driver(*args, **kwargs) |
|---|
| 397 |
raise Error('No usable input driver found') |
|---|
| 398 |
|
|---|
| 399 |
|
|---|
| 400 |
def interact(grammar_or_parser, exceptions=None, *args, **kwargs): |
|---|
| 401 |
"""Start an interactive session with the given grammar or parser object. |
|---|
| 402 |
|
|---|
| 403 |
Arguments are as for :class:`Interact`. |
|---|
| 404 |
""" |
|---|
| 405 |
interact = Interact(grammar_or_parser, *args, **kwargs) |
|---|
| 406 |
interact.loop(exceptions=exceptions) |
|---|