OILS / builtin / readline_osh.py View on Github | oils.pub

378 lines, 227 significant
1#!/usr/bin/env python2
2"""
3readline_osh.py - Builtins that are dependent on GNU readline.
4"""
5from __future__ import print_function
6
7from _devbuild.gen import arg_types
8from _devbuild.gen.runtime_asdl import cmd_value, scope_e
9from _devbuild.gen.syntax_asdl import loc
10from _devbuild.gen.value_asdl import value, value_e, value_str
11from core import pyutil, state, vm
12from core.error import e_usage
13from frontend import flag_util, location
14from mycpp import mops
15from mycpp import mylib
16from mycpp.mylib import log, tagswitch
17from osh import cmd_eval
18
19from typing import Optional, Tuple, Any, Dict, cast, TYPE_CHECKING
20if TYPE_CHECKING:
21 from builtin.meta_oils import Eval
22 from frontend.py_readline import Readline
23 from core import sh_init
24 from core.state import Mem
25 from display import ui
26
27_ = log
28
29
30class ctx_Keymap(object):
31
32 def __init__(self, readline, keymap_name=None):
33 # type: (Readline, Optional[str]) -> None
34 self.readline = readline
35 self.orig_keymap_name = keymap_name
36
37 def __enter__(self):
38 # type: () -> None
39 if self.orig_keymap_name is not None:
40 self.readline.use_temp_keymap(self.orig_keymap_name)
41
42 def __exit__(self, type, value, traceback):
43 # type: (Any, Any, Any) -> None
44 if self.orig_keymap_name is not None:
45 self.readline.restore_orig_keymap()
46
47
48class ctx_EnvVars(object):
49 """
50 Context manager for temporarily setting environment variables.
51
52 Ignores any pre-existing values for the env vars.
53 """
54
55 def __init__(self, mem, env_vars):
56 # type: (Mem, Dict[str, str]) -> None
57 self.mem = mem
58 self.env_vars = env_vars
59
60 def __enter__(self):
61 # type: () -> None
62 for name, val in self.env_vars.items():
63 state.ExportGlobalString(self.mem, name, val)
64
65 def __exit__(self, type, value, traceback):
66 # type: (Any, Any, Any) -> None
67 # Clean up env vars after command execution
68 for name in self.env_vars:
69 self.mem.Unset(location.LName(name), scope_e.GlobalOnly)
70
71
72class BindXCallback(object):
73 """A callable we pass to readline for executing shell commands."""
74
75 def __init__(self, eval, mem, errfmt):
76 # type: (Eval, Mem, ui.ErrorFormatter) -> None
77 self.eval = eval
78 self.mem = mem
79 self.errfmt = errfmt
80
81 def __call__(self, cmd, line_buffer, point):
82 # type: (str, str, int) -> Tuple[int, str, int]
83 """Execute a shell command through the evaluator.
84
85 Args:
86 cmd: The shell command to execute
87 line_buffer: The current line buffer
88 point: The current cursor position
89 """
90
91 with ctx_EnvVars(self.mem, {
92 'READLINE_LINE': line_buffer,
93 'READLINE_POINT': str(point)
94 }):
95 # TODO: refactor out shared code from Eval, cache parse tree?
96
97 cmd_val = cmd_eval.MakeBuiltinArgv([cmd])
98 status = self.eval.Run(cmd_val)
99
100 # Retrieve READLINE_* env vars to check for changes
101 readline_line = self._get_rl_env_var('READLINE_LINE')
102 readline_point = self._get_rl_env_var('READLINE_POINT')
103
104 post_line_buffer = readline_line if readline_line is not None else line_buffer
105 post_point = int(
106 readline_point) if readline_point is not None else point
107
108 return (status, post_line_buffer, post_point)
109
110 def _get_rl_env_var(self, envvar_name):
111 # type: (str) -> Optional[str]
112 """Retrieve the value of an env var, return None if undefined"""
113
114 envvar_val = self.mem.GetValue(envvar_name, scope_e.GlobalOnly)
115 with tagswitch(envvar_val) as case:
116 if case(value_e.Str):
117 return cast(value.Str, envvar_val).s
118 elif case(value_e.Undef):
119 return None
120 else:
121 # bash has silent weird failures if you set the readline env vars
122 # to something besides a string. Unfortunately, we can't easily
123 # raise an exception, since we have to thread in/out of readline,
124 # and we can't return a meaningful error code from the bound
125 # commands either, because bash doesn't. So, we print an error.
126 self.errfmt.Print_(
127 'expected Str for %s, got %s' %
128 (envvar_name, value_str(envvar_val.tag())), loc.Missing)
129 return None
130
131
132class Bind(vm._Builtin):
133 """Interactive interface to readline bindings"""
134
135 def __init__(self, readline, errfmt, bindx_cb):
136 # type: (Optional[Readline], ui.ErrorFormatter, BindXCallback) -> None
137 self.readline = readline
138 self.errfmt = errfmt
139 self.exclusive_flags = ["q", "u", "r", "x", "f"]
140 self.bindx_cb = bindx_cb
141 if self.readline:
142 self.readline.set_bind_shell_command_hook(self.bindx_cb)
143
144 def Run(self, cmd_val):
145 # type: (cmd_value.Argv) -> int
146 readline = self.readline
147
148 if not readline:
149 e_usage("is disabled because Oils wasn't compiled with 'readline'",
150 loc.Missing)
151
152 attrs, arg_r = flag_util.ParseCmdVal('bind', cmd_val)
153
154 # Check mutually-exclusive flags and non-flag args
155 # Bash allows you to mix args all over, but unfortunately, the execution
156 # order is unrelated to the command line order. OSH makes many of the
157 # options mutually-exclusive.
158 found = False
159 for flag in self.exclusive_flags:
160 if (flag in attrs.attrs and
161 attrs.attrs[flag].tag() != value_e.Undef):
162 if found:
163 self.errfmt.Print_(
164 "error: Can only use one of the following flags at a time: -"
165 + ", -".join(self.exclusive_flags),
166 blame_loc=cmd_val.arg_locs[0])
167 return 1
168 else:
169 found = True
170 if found and not arg_r.AtEnd():
171 self.errfmt.Print_(
172 "error: Too many arguments. Also, you cannot mix normal bindings with the following flags: -"
173 + ", -".join(self.exclusive_flags),
174 blame_loc=cmd_val.arg_locs[0])
175 return 1
176
177 arg = arg_types.bind(attrs.attrs)
178
179 try:
180 with ctx_Keymap(readline, arg.m): # Replicates bind's -m behavior
181
182 # This gauntlet of ifs is meant to replicate bash behavior, in case we
183 # need to relax the mutual exclusion of flags like bash does
184
185 # List names of functions
186 if arg.l:
187 readline.list_funmap_names()
188
189 # Print function names and bindings
190 if arg.p:
191 readline.function_dumper(True) # reusable as input
192 if arg.P:
193 readline.function_dumper(False)
194
195 # Print macros
196 if arg.s:
197 readline.macro_dumper(True) # reusable as input
198 if arg.S:
199 readline.macro_dumper(False)
200
201 # Print readline variable names
202 if arg.v:
203 readline.variable_dumper(True)
204 if arg.V:
205 readline.variable_dumper(False)
206
207 # Read bindings from a file
208 if arg.f is not None:
209 readline.read_init_file(arg.f)
210
211 # Query which keys are bound to a readline fn
212 if arg.q is not None:
213 readline.query_bindings(arg.q)
214
215 # Unbind all keys bound to a readline fn
216 if arg.u is not None:
217 readline.unbind_rl_function(arg.u)
218
219 # Remove all bindings to a key sequence
220 if arg.r is not None:
221 readline.unbind_keyseq(arg.r)
222
223 # Bind custom shell commands to a key sequence
224 if arg.x is not None:
225 self._BindShellCmd(arg.x)
226
227 # Print custom shell bindings
228 if arg.X:
229 readline.print_shell_cmd_map()
230
231 bindings, arg_locs = arg_r.Rest2()
232
233 # Bind keyseqs to readline fns
234 for i, binding in enumerate(bindings):
235 try:
236 readline.parse_and_bind(binding)
237 except ValueError as e:
238 msg = e.message # type: str
239 self.errfmt.Print_("bind error: %s" % msg, arg_locs[i])
240 return 1
241
242 except ValueError as e:
243 # only print out the exception message if non-empty
244 # some bash bind errors return non-zero, but print to stdout
245 # temp var to work around mycpp runtime limitation
246 msg2 = e.message # type: str
247 if msg2 is not None and len(msg2) > 0:
248 self.errfmt.Print_("bind error: %s" % msg2, loc.Missing)
249 return 1
250
251 return 0
252
253 def _BindShellCmd(self, bindseq):
254 # type: (str) -> None
255
256 cmdseq_split = bindseq.strip().split(":", 1)
257 if len(cmdseq_split) != 2:
258 raise ValueError("%s: missing colon separator" % bindseq)
259
260 # Below checks prevent need to do so in C, but also ensure rl_generic_bind
261 # will not try to incorrectly xfree `cmd`/`data`, which doesn't belong to it
262 keyseq = cmdseq_split[0].rstrip()
263 if len(keyseq) <= 2:
264 raise ValueError("%s: empty/invalid key sequence" % keyseq)
265 if keyseq[0] != '"' or keyseq[-1] != '"':
266 raise ValueError(
267 "%s: missing double-quotes around the key sequence" % keyseq)
268 keyseq = keyseq[1:-1]
269
270 cmd = cmdseq_split[1]
271
272 self.readline.bind_shell_command(keyseq, cmd)
273
274
275class History(vm._Builtin):
276 """Show interactive command history."""
277
278 def __init__(
279 self,
280 readline, # type: Optional[Readline]
281 sh_files, # type: sh_init.ShellFiles
282 errfmt, # type: ui.ErrorFormatter
283 f, # type: mylib.Writer
284 ):
285 # type: (...) -> None
286 self.readline = readline
287 self.sh_files = sh_files
288 self.errfmt = errfmt
289 self.f = f # this hook is for unit testing only
290
291 def Run(self, cmd_val):
292 # type: (cmd_value.Argv) -> int
293 # NOTE: This builtin doesn't do anything in non-interactive mode in bash?
294 # It silently exits zero.
295 # zsh -c 'history' produces an error.
296 readline = self.readline
297 if not readline:
298 e_usage("is disabled because Oils wasn't compiled with 'readline'",
299 loc.Missing)
300
301 attrs, arg_r = flag_util.ParseCmdVal('history', cmd_val)
302 arg = arg_types.history(attrs.attrs)
303
304 # Clear all history
305 if arg.c:
306 readline.clear_history()
307 return 0
308
309 if arg.a:
310 hist_file = self.sh_files.HistoryFile()
311 if hist_file is None:
312 return 1
313
314 try:
315 readline.write_history_file(hist_file)
316 except (IOError, OSError) as e:
317 self.errfmt.Print_(
318 'Error writing HISTFILE %r: %s' %
319 (hist_file, pyutil.strerror(e)), loc.Missing)
320 return 1
321
322 return 0
323
324 if arg.r:
325 hist_file = self.sh_files.HistoryFile()
326 if hist_file is None:
327 return 1
328
329 try:
330 readline.read_history_file(hist_file)
331 except (IOError, OSError) as e:
332 self.errfmt.Print_(
333 'Error reading HISTFILE %r: %s' %
334 (hist_file, pyutil.strerror(e)), loc.Missing)
335 return 1
336
337 return 0
338
339 # Delete history entry by id number
340 arg_d = mops.BigTruncate(arg.d)
341 if arg_d >= 0:
342 cmd_index = arg_d - 1
343
344 try:
345 readline.remove_history_item(cmd_index)
346 except ValueError:
347 e_usage("couldn't find item %d" % arg_d, loc.Missing)
348
349 return 0
350
351 # Returns 0 items in non-interactive mode?
352 num_items = readline.get_current_history_length()
353 #log('len = %d', num_items)
354
355 num_arg, num_arg_loc = arg_r.Peek2()
356
357 if num_arg is None:
358 start_index = 1
359 else:
360 try:
361 num_to_show = int(num_arg)
362 except ValueError:
363 e_usage('got invalid argument %r' % num_arg, num_arg_loc)
364 start_index = max(1, num_items + 1 - num_to_show)
365
366 arg_r.Next()
367 if not arg_r.AtEnd():
368 e_usage('got too many arguments', loc.Missing)
369
370 # TODO:
371 # - Exclude lines that don't parse from the history! bash and zsh don't do
372 # that.
373 # - Consolidate multiline commands.
374
375 for i in xrange(start_index, num_items + 1): # 1-based index
376 item = readline.get_history_item(i)
377 self.f.write('%5d %s\n' % (i, item))
378 return 0