OILS / builtin / readline_osh.py View on Github | oilshell.org

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