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

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