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

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