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

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