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

293 lines, 180 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.syntax_asdl import loc
9from _devbuild.gen.value_asdl import value_e
10from core import pyutil
11from core import vm
12from core.error import e_usage
13from frontend import flag_util
14from mycpp import mops
15from mycpp import mylib
16from mycpp.mylib import log
17
18from typing import Optional, Any, TYPE_CHECKING
19if TYPE_CHECKING:
20 from _devbuild.gen.runtime_asdl import cmd_value
21 from frontend.py_readline import Readline
22 from core import sh_init
23 from display import ui
24 from state import Mem
25
26_ = log
27
28import sys # REMOVE ME
29
30class ctx_Keymap(object):
31
32 def __init__(self, readline, keymap_name=None):
33 # type: (Readline, 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 Bind(vm._Builtin):
49 """Interactive interface to readline bindings"""
50
51 def __init__(self, readline, errfmt, mem):
52 # type: (Optional[Readline], ui.ErrorFormatter, Mem) -> None
53 self.readline = readline
54 self.errfmt = errfmt
55 self.mem = mem
56 self.exclusive_flags = ["q", "u", "r", "x", "f"]
57
58 readline.set_bind_shell_command_hook(lambda *args: self.bind_shell_command_hook(*args))
59
60 def Run(self, cmd_val):
61 # type: (cmd_value.Argv) -> int
62 readline = self.readline
63 if not readline:
64 e_usage("is disabled because Oils wasn't compiled with 'readline'",
65 loc.Missing)
66
67 attrs, arg_r = flag_util.ParseCmdVal('bind', cmd_val)
68
69 # print("attrs:\n", attrs)
70 # print("attrs.attrs:\n", attrs.attrs)
71 # print("attrs.attrs.m:\n", attrs.attrs["m"])
72 # print("type(attrs.attrs.m):\n", type(attrs.attrs["m"]))
73 # print("type(attrs.attrs[m]):\n", type(attrs.attrs["m"]))
74 # print("attrs.attrs[m].tag() :\n", attrs.attrs["m"].tag())
75 # print("attrs.attrs[m].tag() == value_e.Undef:\n", attrs.attrs["m"].tag() == value_e.Undef)
76 # print(arg_r)
77 # print("Reader argv=%s locs=%s n=%i i=%i" % (arg_r.argv, str(arg_r.locs), arg_r.n, arg_r.i))
78
79 # Check mutually-exclusive flags and non-flag args
80 found = False
81 for flag in self.exclusive_flags:
82 if (flag in attrs.attrs and
83 attrs.attrs[flag].tag() != value_e.Undef):
84 # print("\tFound flag: {0} with tag: {1}".format(flag, attrs.attrs[flag].tag()))
85 if found:
86 self.errfmt.Print_(
87 "error: Can only use one of the following flags at a time: -"
88 + ", -".join(self.exclusive_flags),
89 blame_loc=cmd_val.arg_locs[0])
90 return 1
91 else:
92 found = True
93 if found and not arg_r.AtEnd():
94 self.errfmt.Print_(
95 "error: Too many arguments. Check your quoting. Also, you cannot mix normal bindings with the following flags: -" +
96 ", -".join(self.exclusive_flags),
97 blame_loc=cmd_val.arg_locs[0])
98 return 1
99
100 arg = arg_types.bind(attrs.attrs)
101 # print("arg:\n", arg)
102 # print("dir(arg):\n", dir(arg))
103 # for prop in dir(arg):
104 # if not prop.startswith('__'):
105 # value = getattr(arg, prop)
106 # print("Property: {0}, Value: {1}".format(prop, value))
107 # print("arg.m:\n", arg.m)
108
109 try:
110 with ctx_Keymap(readline, arg.m): # Replicates bind's -m behavior
111
112 # This gauntlet of ifs is meant to replicate bash behavior, in case we
113 # need to relax the mutual exclusion of flags like bash does
114
115 # List names of functions
116 if arg.l:
117 readline.list_funmap_names()
118
119 # Print function names and bindings
120 if arg.p:
121 readline.function_dumper(True) # reusable as input
122 if arg.P:
123 readline.function_dumper(False)
124
125 # Print macros
126 if arg.s:
127 readline.macro_dumper(True) # reusable as input
128 if arg.S:
129 readline.macro_dumper(False)
130
131 # Print readline variable names
132 if arg.v:
133 readline.variable_dumper(True)
134 if arg.V:
135 readline.variable_dumper(False)
136
137 if arg.f is not None:
138 readline.read_init_file(arg.f)
139
140 if arg.q is not None:
141 readline.query_bindings(arg.q)
142
143 if arg.u is not None:
144 readline.unbind_rl_function(arg.u)
145
146 if arg.r is not None:
147 readline.unbind_keyseq(arg.r)
148
149 if arg.x is not None:
150 # print("arg.x: %s" % arg.x)
151 readline.bind_shell_command(arg.x)
152
153 if arg.X:
154 readline.print_shell_cmd_map()
155
156 bindings, arg_locs = arg_r.Rest2()
157 #log('bindings %d locs %d', len(arg_r.argv), len(arg_r.locs))
158
159 for i, binding in enumerate(bindings):
160 try:
161 #log("Binding %s (%d)", binding, i)
162 #log("Arg loc %s (%d)", arg_locs[i], i)
163 readline.parse_and_bind(binding)
164 except ValueError as e:
165 msg = e.message # type: str
166 self.errfmt.Print_("bind error: %s" % msg, arg_locs[i])
167 return 1
168
169 except ValueError as e:
170 # only print out the exception message if non-empty
171 # some bash bind errors return non-zero, but print to stdout
172 # temp var to work around mycpp runtime limitation
173 msg2 = e.message # type: str
174 if msg2 is not None and len(msg2) > 0:
175 self.errfmt.Print_("bind error: %s" % msg2, loc.Missing)
176 return 1
177
178 return 0
179
180 def bind_shell_command_hook(self, cmd, line_buffer, point):
181 # type: (str, str, int) -> (str, str)
182 print("Executing cmd: %s" % cmd)
183 print("Setting READLINE_LINE to: %s" % line_buffer)
184 print("Setting READLINE_POINT to: %s" % point)
185 sys.stdout.flush()
186
187 return (line_buffer, str(point))
188
189
190class History(vm._Builtin):
191 """Show interactive command history."""
192
193 def __init__(
194 self,
195 readline, # type: Optional[Readline]
196 sh_files, # type: sh_init.ShellFiles
197 errfmt, # type: ui.ErrorFormatter
198 f, # type: mylib.Writer
199 ):
200 # type: (...) -> None
201 self.readline = readline
202 self.sh_files = sh_files
203 self.errfmt = errfmt
204 self.f = f # this hook is for unit testing only
205
206 def Run(self, cmd_val):
207 # type: (cmd_value.Argv) -> int
208 # NOTE: This builtin doesn't do anything in non-interactive mode in bash?
209 # It silently exits zero.
210 # zsh -c 'history' produces an error.
211 readline = self.readline
212 if not readline:
213 e_usage("is disabled because Oils wasn't compiled with 'readline'",
214 loc.Missing)
215
216 attrs, arg_r = flag_util.ParseCmdVal('history', cmd_val)
217 arg = arg_types.history(attrs.attrs)
218
219 # Clear all history
220 if arg.c:
221 readline.clear_history()
222 return 0
223
224 if arg.a:
225 hist_file = self.sh_files.HistoryFile()
226 if hist_file is None:
227 return 1
228
229 try:
230 readline.write_history_file(hist_file)
231 except (IOError, OSError) as e:
232 self.errfmt.Print_(
233 'Error writing HISTFILE %r: %s' %
234 (hist_file, pyutil.strerror(e)), loc.Missing)
235 return 1
236
237 return 0
238
239 if arg.r:
240 hist_file = self.sh_files.HistoryFile()
241 if hist_file is None:
242 return 1
243
244 try:
245 readline.read_history_file(hist_file)
246 except (IOError, OSError) as e:
247 self.errfmt.Print_(
248 'Error reading HISTFILE %r: %s' %
249 (hist_file, pyutil.strerror(e)), loc.Missing)
250 return 1
251
252 return 0
253
254 # Delete history entry by id number
255 arg_d = mops.BigTruncate(arg.d)
256 if arg_d >= 0:
257 cmd_index = arg_d - 1
258
259 try:
260 readline.remove_history_item(cmd_index)
261 except ValueError:
262 e_usage("couldn't find item %d" % arg_d, loc.Missing)
263
264 return 0
265
266 # Returns 0 items in non-interactive mode?
267 num_items = readline.get_current_history_length()
268 #log('len = %d', num_items)
269
270 num_arg, num_arg_loc = arg_r.Peek2()
271
272 if num_arg is None:
273 start_index = 1
274 else:
275 try:
276 num_to_show = int(num_arg)
277 except ValueError:
278 e_usage('got invalid argument %r' % num_arg, num_arg_loc)
279 start_index = max(1, num_items + 1 - num_to_show)
280
281 arg_r.Next()
282 if not arg_r.AtEnd():
283 e_usage('got too many arguments', loc.Missing)
284
285 # TODO:
286 # - Exclude lines that don't parse from the history! bash and zsh don't do
287 # that.
288 # - Consolidate multiline commands.
289
290 for i in xrange(start_index, num_items + 1): # 1-based index
291 item = readline.get_history_item(i)
292 self.f.write('%5d %s\n' % (i, item))
293 return 0