OILS / core / error.py View on Github | oils.pub

369 lines, 114 significant
1""" core/error.py """
2from __future__ import print_function
3
4from _devbuild.gen.syntax_asdl import loc_e, loc_t, loc
5from _devbuild.gen.value_asdl import (value, value_t, value_str)
6from core import num
7from mycpp.mylib import NewDict
8
9from typing import Dict, Union, NoReturn, TYPE_CHECKING
10
11# For storing errors in List[T]
12if TYPE_CHECKING:
13 IOError_OSError = Union[IOError, OSError]
14
15
16def _ValType(val):
17 # type: (value_t) -> str
18 """Duplicate ui.ValType for now"""
19 return value_str(val.tag(), dot=False)
20
21
22class _ErrorWithLocation(Exception):
23 """A parse error that can be formatted.
24
25 Formatting is in ui.PrintError.
26 """
27
28 def __init__(self, msg, location):
29 # type: (str, loc_t) -> None
30
31 self.msg = msg
32
33 # Ensure that the location field is always populated
34 if location is None:
35 self.location = loc.Missing # type: loc_t
36 else:
37 self.location = location
38
39 def HasLocation(self):
40 # type: () -> bool
41 return self.location.tag() != loc_e.Missing
42
43 def UserErrorString(self):
44 # type: () -> str
45 return self.msg
46
47 def __repr__(self):
48 # type: () -> str
49 return '<%s %r>' % (self.msg, self.location)
50
51
52class Usage(_ErrorWithLocation):
53 """For flag parsing errors in builtins and main()
54
55 Called by e_usage(). TODO: Should settle on a single interface that
56 can be translated. Sometimes we use 'raise error.Usage()'
57 """
58
59 def __init__(self, msg, location):
60 # type: (str, loc_t) -> None
61 _ErrorWithLocation.__init__(self, msg, location)
62
63
64class Parse(_ErrorWithLocation):
65 """Used in the parsers."""
66
67 def __init__(self, msg, location):
68 # type: (str, loc_t) -> None
69 _ErrorWithLocation.__init__(self, msg, location)
70
71
72class WordFailure(_ErrorWithLocation):
73 """Raised for failure of word evaluation
74
75 Meant to be caught.
76 """
77
78 def __init__(self, msg, location):
79 # type: (str, loc_t) -> None
80 _ErrorWithLocation.__init__(self, msg, location)
81
82
83class FailGlob(WordFailure):
84 """Raised when a glob matches nothing when failglob is set.
85
86 Meant to be caught.
87 """
88
89 def __init__(self, msg, location):
90 # type: (str, loc_t) -> None
91 WordFailure.__init__(self, 'failglob: ' + msg, location)
92
93
94class VarSubFailure(WordFailure):
95 """Raised when a variable substitution fails. For example, this
96 is thrown with ${!ref} when the variable "ref" contains invalid
97 variable name such as ref="a b c".
98
99 Meant to be caught.
100 """
101
102 def __init__(self, msg, location):
103 # type: (str, loc_t) -> None
104 WordFailure.__init__(self, msg, location)
105
106
107class RedirectEval(_ErrorWithLocation):
108 """Used in the CommandEvaluator.
109
110 A bad redirect causes the SimpleCommand to return with status 1. To
111 make it fatal, use set -o errexit.
112 """
113
114 def __init__(self, msg, location):
115 # type: (str, loc_t) -> None
116 _ErrorWithLocation.__init__(self, msg, location)
117
118
119class FatalRuntime(_ErrorWithLocation):
120 """An exception that propagates to the top level.
121
122 Used in the evaluators, and also also used in test builtin for
123 invalid argument.
124 """
125
126 def __init__(self, exit_status, msg, location):
127 # type: (int, str, loc_t) -> None
128 _ErrorWithLocation.__init__(self, msg, location)
129 self.exit_status = exit_status
130
131 def ExitStatus(self):
132 # type: () -> int
133 return self.exit_status
134
135
136class Strict(FatalRuntime):
137 """Depending on shell options, these errors may be caught and ignored.
138
139 For example, if options like these are ON:
140
141 set -o strict_arith
142 set -o strict_word_eval
143
144 then we re-raise the error so it's caught by the top level. Otherwise
145 we catch it and return a dummy value like '' or -1 (i.e. what bash commonly
146 does.)
147
148 TODO: Have levels, like:
149
150 OILS_STRICT_PRINT=2 # print warnings at level 2 and above
151 OILS_STRICT_DIE=1 # abort the program at level 1 and above
152 """
153
154 def __init__(self, msg, location):
155 # type: (str, loc_t) -> None
156 FatalRuntime.__init__(self, 1, msg, location)
157
158
159class ErrExit(FatalRuntime):
160 """For set -e.
161
162 Travels between WordEvaluator and CommandEvaluator.
163 """
164
165 def __init__(self, exit_status, msg, location, show_code=False):
166 # type: (int, str, loc_t, bool) -> None
167 FatalRuntime.__init__(self, exit_status, msg, location)
168 self.show_code = show_code
169
170
171class Expr(FatalRuntime):
172 """e.g. KeyError, IndexError, ZeroDivisionError."""
173
174 def __init__(self, msg, location):
175 # type: (str, loc_t) -> None
176
177 # Unique status of 3 for expression errors -- for both the caught and
178 # uncaught case.
179 #
180 # Caught: try sets _status register to 3
181 # Uncaught: shell exits with status 3
182 FatalRuntime.__init__(self, 3, msg, location)
183
184
185class Structured(FatalRuntime):
186 """An error that can be exposed via the _error Dict.
187
188 Including:
189 - Errors raised by the 'error' builtin
190 - J8 encode and decode errors.
191 """
192
193 def __init__(self, status, msg, location, properties=None):
194 # type: (int, str, loc_t, Dict[str, value_t]) -> None
195 FatalRuntime.__init__(self, status, msg, location)
196 self.properties = properties
197
198 def ToDict(self):
199 # type: () -> value.Dict
200
201 d = NewDict() # type: Dict[str, value_t]
202
203 # The _error Dict order is odd -- the optional properties come BEFORE
204 # required fields. We always want the required fields to be present so
205 # it makes sense.
206 if self.properties is not None:
207 d.update(self.properties)
208
209 # _error.code is better than _error.status
210 d['code'] = num.ToBig(self.ExitStatus())
211 d['message'] = value.Str(self.msg)
212
213 return value.Dict(d)
214
215
216class AssertionErr(Expr):
217 """An assertion."""
218
219 def __init__(self, msg, location):
220 # type: (str, loc_t) -> None
221 Expr.__init__(self, msg, location)
222
223
224class TypeErrVerbose(Expr):
225 """e.g. ~ on a bool or float, 'not' on an int."""
226
227 def __init__(self, msg, location):
228 # type: (str, loc_t) -> None
229 Expr.__init__(self, msg, location)
230
231
232class TypeErr(TypeErrVerbose):
233
234 def __init__(self, actual_val, msg, location):
235 # type: (value_t, str, loc_t) -> None
236 TypeErrVerbose.__init__(self,
237 "%s, got %s" % (msg, _ValType(actual_val)),
238 location)
239
240
241class Runtime(Exception):
242 """An error that's meant to be caught, i.e. it's non-fatal.
243
244 Thrown by core/state.py and caught by builtins
245 """
246
247 def __init__(self, msg):
248 # type: (str) -> None
249 self.msg = msg
250
251 def UserErrorString(self):
252 # type: () -> str
253 return self.msg
254
255
256class Decode(Exception):
257 """
258 List of J8 errors:
259 - message isn't UTF-8 - Id.Lit_Chars - need loc
260 - Invalid token Id.Unkown_Tok - need loc
261 - Unclosed double quote string -- need loc
262 - Parse error, e.g. [}{]
263
264 - Invalid escapes:
265 - b"" and u"" don't accept \\u1234
266 - u"" doesn't accept \\yff
267 - "" doesn't accept \\yff or \\u{123456}
268 """
269
270 def __init__(self, msg, s, start_pos, end_pos, line_num):
271 # type: (str, str, int, int, int) -> None
272 self.msg = msg
273 self.s = s # string being decoded
274 self.start_pos = start_pos
275 self.end_pos = end_pos
276 self.line_num = line_num
277
278 def Message(self):
279 # type: () -> str
280
281 # Show 10 chars of context for now
282 start = max(0, self.start_pos - 4)
283 end = min(len(self.s), self.end_pos + 4)
284
285 part = self.s[start:end]
286 return self.msg + ' (line %d, offset %d-%d: %r)' % (
287 self.line_num, self.start_pos, self.end_pos, part)
288
289 def __str__(self):
290 # type: () -> str
291 return self.Message()
292
293
294class Encode(Exception):
295 """
296 List of J8 encode errors:
297 - object cycle
298 - unprintable object like Eggex
299 When encoding JSON:
300 - binary data that can't be represented in JSON
301 - if using Unicode replacement char, then it won't fail
302 """
303
304 def __init__(self, msg):
305 # type: (str) -> None
306 self.msg = msg
307
308 def Message(self):
309 # type: () -> str
310 return self.msg
311
312
313def e_usage(msg, location):
314 # type: (str, loc_t) -> NoReturn
315 """Convenience wrapper for arg parsing / validation errors.
316
317 Usually causes a builtin to fail with status 2, but the script can continue
318 if 'set +o errexit'. Main programs like bin/ysh also use this.
319
320 Caught by
321
322 - RunAssignBuiltin and RunBuiltin, with optional LOCATION INFO
323 - various main() programs, without location info
324
325 Probably should separate these two cases?
326
327 - builtins pass Token() or loc::Missing()
328 - tool interfaces don't pass any location info
329 """
330 raise Usage(msg, location)
331
332
333def e_strict(msg, location):
334 # type: (str, loc_t) -> NoReturn
335 """Convenience wrapper for strictness errors.
336
337 Like e_die(), except the script MAY continue executing after these errors.
338
339 TODO: This could have a level too?
340 """
341 raise Strict(msg, location)
342
343
344def p_die(msg, location):
345 # type: (str, loc_t) -> NoReturn
346 """Convenience wrapper for parse errors.
347
348 Exits with status 2. See core/main_loop.py.
349 """
350 raise Parse(msg, location)
351
352
353def e_die(msg, location=None):
354 # type: (str, loc_t) -> NoReturn
355 """Convenience wrapper for fatal runtime errors.
356
357 Usually exits with status 1. See osh/cmd_eval.py.
358 """
359 raise FatalRuntime(1, msg, location)
360
361
362def e_die_status(status, msg, location=None):
363 # type: (int, str, loc_t) -> NoReturn
364 """Wrapper for C++ semantics.
365
366 Note that it doesn't take positional args, so you should use %
367 formatting.
368 """
369 raise FatalRuntime(status, msg, location)