OILS / ysh / val_ops.py View on Github | oilshell.org

552 lines, 318 significant
1from __future__ import print_function
2
3from errno import EINTR
4
5from _devbuild.gen.syntax_asdl import loc, loc_t, command_t
6from _devbuild.gen.value_asdl import (value, value_e, value_t, eggex_ops,
7 eggex_ops_t, regex_match, RegexMatch,
8 Obj)
9from core import bash_impl
10from core import error
11from core.error import e_die
12from display import ui
13from mycpp import mops
14from mycpp import mylib
15from mycpp.mylib import tagswitch, log
16from ysh import regex_translate
17
18from typing import TYPE_CHECKING, cast, Dict, List, Optional
19
20import libc
21import posix_ as posix
22
23_ = log
24
25if TYPE_CHECKING:
26 from core import state
27
28
29def ToInt(val, msg, blame_loc):
30 # type: (value_t, str, loc_t) -> int
31 UP_val = val
32 if val.tag() == value_e.Int:
33 val = cast(value.Int, UP_val)
34 return mops.BigTruncate(val.i)
35
36 raise error.TypeErr(val, msg, blame_loc)
37
38
39def ToFloat(val, msg, blame_loc):
40 # type: (value_t, str, loc_t) -> float
41 UP_val = val
42 if val.tag() == value_e.Float:
43 val = cast(value.Float, UP_val)
44 return val.f
45
46 raise error.TypeErr(val, msg, blame_loc)
47
48
49def ToStr(val, msg, blame_loc):
50 # type: (value_t, str, loc_t) -> str
51 UP_val = val
52 if val.tag() == value_e.Str:
53 val = cast(value.Str, UP_val)
54 return val.s
55
56 raise error.TypeErr(val, msg, blame_loc)
57
58
59def ToList(val, msg, blame_loc):
60 # type: (value_t, str, loc_t) -> List[value_t]
61 UP_val = val
62 if val.tag() == value_e.List:
63 val = cast(value.List, UP_val)
64 return val.items
65
66 raise error.TypeErr(val, msg, blame_loc)
67
68
69def ToDict(val, msg, blame_loc):
70 # type: (value_t, str, loc_t) -> Dict[str, value_t]
71 UP_val = val
72 if val.tag() == value_e.Dict:
73 val = cast(value.Dict, UP_val)
74 return val.d
75
76 raise error.TypeErr(val, msg, blame_loc)
77
78
79def ToCommandFrag(val, msg, blame_loc):
80 # type: (value_t, str, loc_t) -> command_t
81 UP_val = val
82 if val.tag() == value_e.CommandFrag:
83 val = cast(value.CommandFrag, UP_val)
84 return val.c
85
86 raise error.TypeErr(val, msg, blame_loc)
87
88
89def Stringify(val, blame_loc, op_desc):
90 # type: (value_t, loc_t, str) -> str
91 """
92 Args:
93 op_desc: could be empty string ''
94 or 'Expr Sub ' or 'Expr Splice ', with trailing space
95
96 Used by:
97
98 $[x] Expr Sub - stringify operator
99 @[x] Expr splice - each element is stringified
100 @x Splice value
101
102 str() Builtin function
103 join() Each element is stringified, e.g. join([1,2])
104 Not sure I like join([null, true]), but it's consistent
105 Str.replace() ^"x = $x" after eggex conversion function
106 """
107 if blame_loc is None:
108 blame_loc = loc.Missing
109
110 UP_val = val
111 with tagswitch(val) as case:
112 if case(value_e.Str): # trivial case
113 val = cast(value.Str, UP_val)
114 return val.s
115
116 elif case(value_e.Null):
117 s = 'null' # JSON spelling
118
119 elif case(value_e.Bool):
120 val = cast(value.Bool, UP_val)
121 s = 'true' if val.b else 'false' # JSON spelling
122
123 elif case(value_e.Int):
124 val = cast(value.Int, UP_val)
125 # e.g. decimal '42', the only sensible representation
126 s = mops.ToStr(val.i)
127
128 elif case(value_e.Float):
129 val = cast(value.Float, UP_val)
130 s = str(val.f)
131
132 elif case(value_e.Eggex):
133 val = cast(value.Eggex, UP_val)
134 s = regex_translate.AsPosixEre(val) # lazily converts to ERE
135
136 else:
137 pass # mycpp workaround
138
139 if val.tag() == value_e.List:
140 # Special error message for using the wrong sigil, or maybe join
141 raise error.TypeErrVerbose(
142 "%sgot a List, which can't be stringified (OILS-ERR-203)" %
143 op_desc, blame_loc)
144
145 raise error.TypeErr(
146 val,
147 "%sexpected one of (Null Bool Int Float Str Eggex)" % op_desc,
148 blame_loc)
149
150 return s
151
152
153def ToShellArray(val, blame_loc, prefix=''):
154 # type: (value_t, loc_t, str) -> List[str]
155 """
156 Used by
157
158 @[x] expression splice
159 @x splice value
160
161 Dicts do NOT get spliced, but they iterate over their keys
162 So this function NOT use Iterator.
163 """
164 UP_val = val
165 with tagswitch(val) as case2:
166 if case2(value_e.List):
167 val = cast(value.List, UP_val)
168 strs = [] # type: List[str]
169 # Note: it would be nice to add the index to the error message
170 # prefix, WITHOUT allocating a string for every item
171 for item in val.items:
172 strs.append(Stringify(item, blame_loc, prefix))
173
174 # I thought about getting rid of this to keep OSH and YSH separate,
175 # but:
176 # - readarray/mapfile returns bash array (ysh-user-feedback depends on it)
177 # - ysh-options tests parse_at too
178 elif case2(value_e.BashArray):
179 array_val = cast(value.BashArray, UP_val)
180 strs = bash_impl.BashArray_GetValues(array_val)
181
182 elif case2(value_e.SparseArray):
183 sparse_val = cast(value.SparseArray, UP_val)
184 strs = bash_impl.SparseArray_GetValues(sparse_val)
185
186 else:
187 raise error.TypeErr(val, "%sexpected List" % prefix, blame_loc)
188
189 return strs
190
191
192class Iterator(object):
193 """Interface for various types of for loop."""
194
195 def __init__(self):
196 # type: () -> None
197 self.i = 0
198
199 def Index(self):
200 # type: () -> int
201 return self.i
202
203 def Next(self):
204 # type: () -> None
205 self.i += 1
206
207 def FirstValue(self):
208 # type: () -> Optional[value_t]
209 """Return a value, or None if done
210
211 e.g. return Dict key or List value
212 """
213 raise NotImplementedError()
214
215 def SecondValue(self):
216 # type: () -> value_t
217 """Return Dict value or FAIL"""
218 raise AssertionError("Shouldn't have called this")
219
220
221class StdinIterator(Iterator):
222 """ for x in <> { """
223
224 def __init__(self, blame_loc):
225 # type: (loc_t) -> None
226 Iterator.__init__(self)
227 self.blame_loc = blame_loc
228 self.f = mylib.Stdin()
229
230 def FirstValue(self):
231 # type: () -> Optional[value_t]
232
233 # line, eof = read_osh.ReadLineSlowly(None, with_eol=False)
234 try:
235 line = self.f.readline()
236 except (IOError, OSError) as e: # signals
237 if e.errno == EINTR:
238 # Caller will can run traps with cmd_ev, like ReadLineSlowly
239 return value.Interrupted
240 else:
241 # For possible errors from f.readline(), see
242 # man read
243 # man getline
244 # e.g.
245 # - ENOMEM getline() allocation failure
246 # - EISDIR getline() read from directory descriptor!
247 #
248 # Note: the read builtin returns status 1 for EISDIR.
249 #
250 # We'll raise a top-level error like Python. (Awk prints a
251 # warning message)
252 e_die("I/O error in for <> loop: %s" % posix.strerror(e.errno),
253 self.blame_loc)
254
255 #log('L %r', line)
256 if len(line) == 0:
257 return None # Done
258 elif line.endswith('\n'):
259 # TODO: optimize this to prevent extra garbage
260 line = line[:-1]
261
262 return value.Str(line)
263
264
265class ArrayIter(Iterator):
266 """ for x in 1 2 3 { """
267
268 def __init__(self, strs):
269 # type: (List[str]) -> None
270 Iterator.__init__(self)
271 self.strs = strs
272 self.n = len(strs)
273
274 def FirstValue(self):
275 # type: () -> Optional[value_t]
276 if self.i == self.n:
277 return None
278 return value.Str(self.strs[self.i])
279
280
281class RangeIterator(Iterator):
282 """ for x in (m:n) { """
283
284 def __init__(self, val):
285 # type: (value.Range) -> None
286 Iterator.__init__(self)
287 self.val = val
288
289 def FirstValue(self):
290 # type: () -> Optional[value_t]
291 if self.val.lower + self.i >= self.val.upper:
292 return None
293
294 # TODO: range should be BigInt too
295 return value.Int(mops.IntWiden(self.val.lower + self.i))
296
297
298class ListIterator(Iterator):
299 """ for x in (mylist) { """
300
301 def __init__(self, val):
302 # type: (value.List) -> None
303 Iterator.__init__(self)
304 self.val = val
305 self.n = len(val.items)
306
307 def FirstValue(self):
308 # type: () -> Optional[value_t]
309 if self.i == self.n:
310 return None
311 return self.val.items[self.i]
312
313
314class DictIterator(Iterator):
315 """ for x in (mydict) { """
316
317 def __init__(self, val):
318 # type: (value.Dict) -> None
319 Iterator.__init__(self)
320
321 # TODO: Don't materialize these Lists
322 self.keys = val.d.keys() # type: List[str]
323 self.values = val.d.values() # type: List[value_t]
324
325 self.n = len(val.d)
326 assert self.n == len(self.keys)
327
328 def FirstValue(self):
329 # type: () -> value_t
330 if self.i == self.n:
331 return None
332 return value.Str(self.keys[self.i])
333
334 def SecondValue(self):
335 # type: () -> value_t
336 return self.values[self.i]
337
338
339def ToBool(val):
340 # type: (value_t) -> bool
341 """Convert any value to a boolean.
342
343 TODO: expose this as Bool(x), like Python's bool(x).
344 """
345 UP_val = val
346 with tagswitch(val) as case:
347 if case(value_e.Undef):
348 return False
349
350 elif case(value_e.Null):
351 return False
352
353 elif case(value_e.Str):
354 val = cast(value.Str, UP_val)
355 return len(val.s) != 0
356
357 # OLD TYPES
358 elif case(value_e.BashArray):
359 val = cast(value.BashArray, UP_val)
360 return not bash_impl.BashArray_IsEmpty(val)
361
362 elif case(value_e.BashAssoc):
363 val = cast(value.BashAssoc, UP_val)
364 return not bash_impl.BashAssoc_IsEmpty(val)
365
366 elif case(value_e.Bool):
367 val = cast(value.Bool, UP_val)
368 return val.b
369
370 elif case(value_e.Int):
371 val = cast(value.Int, UP_val)
372 return not mops.Equal(val.i, mops.BigInt(0))
373
374 elif case(value_e.Float):
375 val = cast(value.Float, UP_val)
376 return val.f != 0.0
377
378 elif case(value_e.List):
379 val = cast(value.List, UP_val)
380 return len(val.items) > 0
381
382 elif case(value_e.Dict):
383 val = cast(value.Dict, UP_val)
384 return len(val.d) > 0
385
386 else:
387 return True # all other types are Truthy
388
389
390def ExactlyEqual(left, right, blame_loc):
391 # type: (value_t, value_t, loc_t) -> bool
392
393 if left.tag() == value_e.Float or right.tag() == value_e.Float:
394 raise error.TypeErrVerbose(
395 "Equality isn't defined on Float values (OILS-ERR-202)", blame_loc)
396
397 if left.tag() != right.tag():
398 return False
399
400 UP_left = left
401 UP_right = right
402 with tagswitch(left) as case:
403 if case(value_e.Undef):
404 return True # there's only one Undef
405
406 elif case(value_e.Null):
407 return True # there's only one Null
408
409 elif case(value_e.Bool):
410 left = cast(value.Bool, UP_left)
411 right = cast(value.Bool, UP_right)
412 return left.b == right.b
413
414 elif case(value_e.Int):
415 left = cast(value.Int, UP_left)
416 right = cast(value.Int, UP_right)
417 return mops.Equal(left.i, right.i)
418
419 elif case(value_e.Float):
420 raise AssertionError()
421
422 elif case(value_e.Str):
423 left = cast(value.Str, UP_left)
424 right = cast(value.Str, UP_right)
425 return left.s == right.s
426
427 elif case(value_e.BashArray):
428 left = cast(value.BashArray, UP_left)
429 right = cast(value.BashArray, UP_right)
430 return bash_impl.BashArray_Equals(left, right)
431
432 elif case(value_e.List):
433 left = cast(value.List, UP_left)
434 right = cast(value.List, UP_right)
435 if len(left.items) != len(right.items):
436 return False
437
438 for i in xrange(0, len(left.items)):
439 if not ExactlyEqual(left.items[i], right.items[i], blame_loc):
440 return False
441
442 return True
443
444 elif case(value_e.BashAssoc):
445 left = cast(value.BashAssoc, UP_left)
446 right = cast(value.BashAssoc, UP_right)
447 return bash_impl.BashAssoc_Equals(left, right)
448
449 elif case(value_e.Dict):
450 left = cast(value.Dict, UP_left)
451 right = cast(value.Dict, UP_right)
452 if len(left.d) != len(right.d):
453 return False
454
455 for k in left.d:
456 if (k not in right.d or
457 not ExactlyEqual(right.d[k], left.d[k], blame_loc)):
458 return False
459
460 return True
461
462 raise error.TypeErrVerbose(
463 "Can't compare two values of type %s" % ui.ValType(left), blame_loc)
464
465
466def Contains(needle, haystack):
467 # type: (value_t, value_t) -> bool
468 """Haystack must be a Dict.
469
470 We should have mylist->find(x) !== -1 for searching through a List.
471 Things with different perf characteristics should look different.
472 """
473 UP_haystack = haystack
474 with tagswitch(haystack) as case:
475 if case(value_e.Dict):
476 haystack = cast(value.Dict, UP_haystack)
477 s = ToStr(needle, "LHS of 'in' should be Str", loc.Missing)
478 return s in haystack.d
479
480 else:
481 raise error.TypeErr(haystack, "RHS of 'in' should be Dict",
482 loc.Missing)
483
484 return False
485
486
487def MatchRegex(left, right, mem):
488 # type: (value_t, value_t, Optional[state.Mem]) -> bool
489 """
490 Args:
491 mem: Whether to set or clear matches
492 """
493 UP_right = right
494
495 with tagswitch(right) as case:
496 if case(value_e.Str): # plain ERE
497 right = cast(value.Str, UP_right)
498
499 right_s = right.s
500 regex_flags = 0
501 capture = eggex_ops.No # type: eggex_ops_t
502
503 elif case(value_e.Eggex):
504 right = cast(value.Eggex, UP_right)
505
506 right_s = regex_translate.AsPosixEre(right)
507 regex_flags = regex_translate.LibcFlags(right.canonical_flags)
508 capture = eggex_ops.Yes(right.convert_funcs, right.convert_toks,
509 right.capture_names)
510
511 else:
512 raise error.TypeErr(right, 'Expected Str or Regex for RHS of ~',
513 loc.Missing)
514
515 UP_left = left
516 left_s = None # type: Optional[str]
517 with tagswitch(left) as case:
518 if case(value_e.Str):
519 left = cast(value.Str, UP_left)
520 left_s = left.s
521 else:
522 raise error.TypeErrVerbose('LHS must be a string', loc.Missing)
523
524 indices = libc.regex_search(right_s, regex_flags, left_s, 0)
525 if indices is not None:
526 if mem:
527 mem.SetRegexMatch(RegexMatch(left_s, indices, capture))
528 return True
529 else:
530 if mem:
531 mem.SetRegexMatch(regex_match.No)
532 return False
533
534
535def IndexMetaMethod(obj):
536 # type: (Obj) -> Optional[value_t]
537 """
538 Returns value.{BuiltinFunc,Func} -- but not callable Obj?
539 """
540 if not obj.prototype:
541 return None
542 index_val = obj.prototype.d.get('__index__')
543 if not index_val:
544 return None
545
546 if index_val.tag() not in (value_e.BuiltinFunc, value_e.Func):
547 return None
548
549 return index_val
550
551
552# vim: sw=4