| 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 | Press ``?`` at any location to contextual help. |
|---|
| 18 | """ |
|---|
| 19 | |
|---|
| 20 | import os |
|---|
| 21 | import sys |
|---|
| 22 | import readline |
|---|
| 23 | import cly |
|---|
| 24 | import cly.rlext |
|---|
| 25 | import cly.console as console |
|---|
| 26 | |
|---|
| 27 | |
|---|
| 28 | __all__ = ['Interact'] |
|---|
| 29 | |
|---|
| 30 | |
|---|
| 31 | class Interact(object): |
|---|
| 32 | """CLY interaction through readline. Due to readline limitations, only one |
|---|
| 33 | Interact object can be active within an application. |
|---|
| 34 | |
|---|
| 35 | Constructor arguments: |
|---|
| 36 | |
|---|
| 37 | ``parser``: ``Parser`` or ``Grammar`` object |
|---|
| 38 | The parser/grammar to use for interaction. |
|---|
| 39 | |
|---|
| 40 | ``application='cly'``: string |
|---|
| 41 | The application name. Used to construct the history file name and |
|---|
| 42 | prompt, if not provided. |
|---|
| 43 | |
|---|
| 44 | ``prompt=None``: string |
|---|
| 45 | The prompt. |
|---|
| 46 | |
|---|
| 47 | ``user_context=None``: `anything` |
|---|
| 48 | A user-specified object to pass to the parser. The parser builds each |
|---|
| 49 | parse ``Context`` with this object, which in turn will deliver this |
|---|
| 50 | object on to terminal nodes that have set ``with_context=True``. |
|---|
| 51 | |
|---|
| 52 | ``with_context=False``: `boolean` |
|---|
| 53 | Force ``user_context`` to be passed to all action nodes, unless they |
|---|
| 54 | explicitly set the member variable ``with_context=False``. |
|---|
| 55 | |
|---|
| 56 | ``history_file=None``: `string` |
|---|
| 57 | Defaults to ``~/.<application>_history``. |
|---|
| 58 | |
|---|
| 59 | ``history_length=500``: `integer` |
|---|
| 60 | Lines of history to keep. |
|---|
| 61 | |
|---|
| 62 | ``completion_key='tab'``: `string` |
|---|
| 63 | Key to use for completion, per the readline documentation. |
|---|
| 64 | |
|---|
| 65 | ``completion_delimiters=' \t'``: `string` |
|---|
| 66 | Characters that terminate completion. |
|---|
| 67 | |
|---|
| 68 | ``help_key='?'``: `key` |
|---|
| 69 | Key to use for tab completion. |
|---|
| 70 | |
|---|
| 71 | """ |
|---|
| 72 | _cli_inject_text = '' |
|---|
| 73 | _completion_candidates = [] |
|---|
| 74 | _parser = None |
|---|
| 75 | prompt = None |
|---|
| 76 | user_context = None |
|---|
| 77 | history_file = None |
|---|
| 78 | application = None |
|---|
| 79 | |
|---|
| 80 | def __init__(self, parser, application='cly', prompt=None, |
|---|
| 81 | user_context=None, with_context=None, history_file=None, |
|---|
| 82 | history_length=500, completion_key='tab', |
|---|
| 83 | #completion_delimiters='`!#$%^&*()=+[{]}\|;\'",<>? \t', |
|---|
| 84 | completion_delimiters=' \t', |
|---|
| 85 | help_key='?', inhibit_exceptions=False, |
|---|
| 86 | with_backtrace=False): |
|---|
| 87 | if prompt is None: |
|---|
| 88 | prompt = application + '> ' |
|---|
| 89 | if history_file is None: |
|---|
| 90 | history_file = os.path.expanduser('~/.%s_history' % application) |
|---|
| 91 | if isinstance(parser, cly.Grammar): |
|---|
| 92 | parser = cly.Parser(parser) |
|---|
| 93 | |
|---|
| 94 | if with_context is not None: |
|---|
| 95 | parser.with_context = with_context |
|---|
| 96 | if user_context is not None: |
|---|
| 97 | parser.user_context = user_context |
|---|
| 98 | Interact._parser = parser |
|---|
| 99 | Interact.prompt = prompt |
|---|
| 100 | Interact.application = application |
|---|
| 101 | Interact.user_context = user_context |
|---|
| 102 | Interact.history_file = history_file |
|---|
| 103 | Interact.history_length = history_length |
|---|
| 104 | Interact.completion_delimiters = completion_delimiters |
|---|
| 105 | Interact.completion_key = completion_key |
|---|
| 106 | |
|---|
| 107 | try: |
|---|
| 108 | readline.set_history_length(history_length) |
|---|
| 109 | readline.read_history_file(history_file) |
|---|
| 110 | except: |
|---|
| 111 | pass |
|---|
| 112 | |
|---|
| 113 | readline.parse_and_bind("%s: complete" % completion_key) |
|---|
| 114 | readline.set_completer_delims(self.completion_delimiters) |
|---|
| 115 | readline.set_completer(Interact._cli_completion) |
|---|
| 116 | readline.set_startup_hook(Interact._cli_injector) |
|---|
| 117 | |
|---|
| 118 | # Use custom readline extensions |
|---|
| 119 | cly.rlext.bind_key(ord(help_key), Interact._cli_help) |
|---|
| 120 | |
|---|
| 121 | |
|---|
| 122 | def once(self, default_text='', callback=None): |
|---|
| 123 | """Input one command from the user and return the result of the |
|---|
| 124 | executed command. `callback` is called with the Interact object before |
|---|
| 125 | each line is displayed.""" |
|---|
| 126 | Interact._cli_inject_text = default_text |
|---|
| 127 | |
|---|
| 128 | while True: |
|---|
| 129 | command = '' |
|---|
| 130 | try: |
|---|
| 131 | command = raw_input(self.prompt) |
|---|
| 132 | except KeyboardInterrupt: |
|---|
| 133 | print |
|---|
| 134 | continue |
|---|
| 135 | except EOFError: |
|---|
| 136 | print |
|---|
| 137 | return None |
|---|
| 138 | |
|---|
| 139 | try: |
|---|
| 140 | context = Interact._parser.parse(command, user_context=self.user_context) |
|---|
| 141 | context.execute() |
|---|
| 142 | except cly.ParseError, e: |
|---|
| 143 | self.print_error(context, e) |
|---|
| 144 | return context |
|---|
| 145 | |
|---|
| 146 | def loop(self, inhibit_exceptions=False, with_backtrace=False): |
|---|
| 147 | """Repeatedly read and execute commands from the user. |
|---|
| 148 | |
|---|
| 149 | Arguments: |
|---|
| 150 | |
|---|
| 151 | ``inhibit_exceptions=True``: `boolean` |
|---|
| 152 | Normally, ``interact_loop`` will pass exceptions back to the caller for |
|---|
| 153 | handling. Setting this to ``True`` will cause an error message to |
|---|
| 154 | be printed, but interaction will continue. |
|---|
| 155 | |
|---|
| 156 | ``with_backtrace=False``: `boolean` |
|---|
| 157 | Whether to print a full backtrace when ``inhibit_exceptions=True``. |
|---|
| 158 | """ |
|---|
| 159 | try: |
|---|
| 160 | while True: |
|---|
| 161 | try: |
|---|
| 162 | if not self.once(): |
|---|
| 163 | break |
|---|
| 164 | except Exception, e: |
|---|
| 165 | if inhibit_exceptions: |
|---|
| 166 | if with_backtrace: |
|---|
| 167 | import traceback |
|---|
| 168 | console.error(traceback.format_exc()) |
|---|
| 169 | else: |
|---|
| 170 | console.error('error: %s' % e) |
|---|
| 171 | else: |
|---|
| 172 | raise |
|---|
| 173 | finally: |
|---|
| 174 | self.write_history() |
|---|
| 175 | |
|---|
| 176 | def print_error(self, context, e): |
|---|
| 177 | """Called by `once()` to print a ParseError.""" |
|---|
| 178 | candidates = [help[1] for help in context.help()] |
|---|
| 179 | if len(candidates) > 1: |
|---|
| 180 | message = '%s (candidates are %s)' |
|---|
| 181 | else: |
|---|
| 182 | message = '%s (expected %s)' |
|---|
| 183 | message = message % (str(e), ', '.join(candidates)) |
|---|
| 184 | self.error_at_cursor(context, message) |
|---|
| 185 | |
|---|
| 186 | def error_at_cursor(self, context, text): |
|---|
| 187 | """Attempt to intelligently print an error at the current cursor |
|---|
| 188 | offset.""" |
|---|
| 189 | text = str(text) |
|---|
| 190 | term_width = console.termwidth() |
|---|
| 191 | indent = ' ' * (context.cursor % term_width |
|---|
| 192 | + len(Interact.prompt)) |
|---|
| 193 | if len(indent + text) > term_width: |
|---|
| 194 | console.error(indent + '^') |
|---|
| 195 | console.error(text) |
|---|
| 196 | else: |
|---|
| 197 | console.error(indent + '^ ' + text) |
|---|
| 198 | |
|---|
| 199 | def write_history(self): |
|---|
| 200 | """ Write command line history out. """ |
|---|
| 201 | try: |
|---|
| 202 | readline.write_history_file(self.history_file) |
|---|
| 203 | except: |
|---|
| 204 | pass |
|---|
| 205 | |
|---|
| 206 | @staticmethod |
|---|
| 207 | def _dump_traceback(exception): |
|---|
| 208 | import traceback |
|---|
| 209 | from StringIO import StringIO |
|---|
| 210 | out = StringIO() |
|---|
| 211 | traceback.print_exc(file = out) |
|---|
| 212 | print >>sys.stderr, str(exception) |
|---|
| 213 | print >>sys.stderr, out.getvalue() |
|---|
| 214 | |
|---|
| 215 | |
|---|
| 216 | @staticmethod |
|---|
| 217 | def _cli_injector(): |
|---|
| 218 | readline.insert_text(Interact._cli_inject_text) |
|---|
| 219 | Interact._cli_inject_text = '' |
|---|
| 220 | |
|---|
| 221 | |
|---|
| 222 | @staticmethod |
|---|
| 223 | def _cli_completion(text, state): |
|---|
| 224 | line = readline.get_line_buffer()[0:readline.get_begidx()] |
|---|
| 225 | ctx = None |
|---|
| 226 | try: |
|---|
| 227 | result = Interact._parser.parse(line) |
|---|
| 228 | if not state: |
|---|
| 229 | Interact._completion_candidates = list(result.candidates(text)) |
|---|
| 230 | if Interact._completion_candidates: |
|---|
| 231 | return Interact._completion_candidates.pop() |
|---|
| 232 | return None |
|---|
| 233 | except cly.Error: |
|---|
| 234 | return None |
|---|
| 235 | except Exception, e: |
|---|
| 236 | Interact._dump_traceback(e) |
|---|
| 237 | cly.rlext.force_redisplay() |
|---|
| 238 | raise |
|---|
| 239 | |
|---|
| 240 | |
|---|
| 241 | @staticmethod |
|---|
| 242 | def _cli_help(key, count): |
|---|
| 243 | try: |
|---|
| 244 | command = readline.get_line_buffer()[:cly.rlext.cursor()] |
|---|
| 245 | context = Interact._parser.parse(command) |
|---|
| 246 | if context.remaining.strip(): |
|---|
| 247 | print |
|---|
| 248 | candidates = [help[1] for help in context.help()] |
|---|
| 249 | text = '%s^ invalid token (candidates are %s)' % \ |
|---|
| 250 | (' ' * (context.cursor + len(Interact.prompt)), |
|---|
| 251 | ', '.join(candidates)) |
|---|
| 252 | console.error(text) |
|---|
| 253 | cly.rlext.force_redisplay() |
|---|
| 254 | return |
|---|
| 255 | help = context.help() |
|---|
| 256 | print |
|---|
| 257 | help.format(sys.stdout) |
|---|
| 258 | cly.rlext.force_redisplay() |
|---|
| 259 | except Exception, e: |
|---|
| 260 | Interact._dump_traceback(e) |
|---|
| 261 | cly.rlext.force_redisplay() |
|---|
| 262 | raise |
|---|