OILS / core / error.py View on Github | oilshell.org

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