OILS / ysh / val_ops.py View on Github | oils.pub

561 lines, 325 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.SparseArray):
367 val = cast(value.SparseArray, UP_val)
368 return not bash_impl.SparseArray_IsEmpty(val)
369
370 elif case(value_e.Bool):
371 val = cast(value.Bool, UP_val)
372 return val.b
373
374 elif case(value_e.Int):
375 val = cast(value.Int, UP_val)
376 return not mops.Equal(val.i, mops.BigInt(0))
377
378 elif case(value_e.Float):
379 val = cast(value.Float, UP_val)
380 return val.f != 0.0
381
382 elif case(value_e.List):
383 val = cast(value.List, UP_val)
384 return len(val.items) > 0
385
386 elif case(value_e.Dict):
387 val = cast(value.Dict, UP_val)
388 return len(val.d) > 0
389
390 else:
391 return True # all other types are Truthy
392
393
394def ExactlyEqual(left, right, blame_loc):
395 # type: (value_t, value_t, loc_t) -> bool
396
397 if left.tag() == value_e.Float or right.tag() == value_e.Float:
398 raise error.TypeErrVerbose(
399 "Equality isn't defined on Float values (OILS-ERR-202)", blame_loc)
400
401 if left.tag() != right.tag():
402 return False
403
404 UP_left = left
405 UP_right = right
406 with tagswitch(left) as case:
407 if case(value_e.Undef):
408 return True # there's only one Undef
409
410 elif case(value_e.Null):
411 return True # there's only one Null
412
413 elif case(value_e.Bool):
414 left = cast(value.Bool, UP_left)
415 right = cast(value.Bool, UP_right)
416 return left.b == right.b
417
418 elif case(value_e.Int):
419 left = cast(value.Int, UP_left)
420 right = cast(value.Int, UP_right)
421 return mops.Equal(left.i, right.i)
422
423 elif case(value_e.Float):
424 raise AssertionError()
425
426 elif case(value_e.Str):
427 left = cast(value.Str, UP_left)
428 right = cast(value.Str, UP_right)
429 return left.s == right.s
430
431 elif case(value_e.BashArray):
432 left = cast(value.BashArray, UP_left)
433 right = cast(value.BashArray, UP_right)
434 return bash_impl.BashArray_Equals(left, right)
435
436 elif case(value_e.SparseArray):
437 left = cast(value.SparseArray, UP_left)
438 right = cast(value.SparseArray, UP_right)
439 return bash_impl.SparseArray_Equals(left, right)
440
441 elif case(value_e.List):
442 left = cast(value.List, UP_left)
443 right = cast(value.List, UP_right)
444 if len(left.items) != len(right.items):
445 return False
446
447 for i in xrange(0, len(left.items)):
448 if not ExactlyEqual(left.items[i], right.items[i], blame_loc):
449 return False
450
451 return True
452
453 elif case(value_e.BashAssoc):
454 left = cast(value.BashAssoc, UP_left)
455 right = cast(value.BashAssoc, UP_right)
456 return bash_impl.BashAssoc_Equals(left, right)
457
458 elif case(value_e.Dict):
459 left = cast(value.Dict, UP_left)
460 right = cast(value.Dict, UP_right)
461 if len(left.d) != len(right.d):
462 return False
463
464 for k in left.d:
465 if (k not in right.d or
466 not ExactlyEqual(right.d[k], left.d[k], blame_loc)):
467 return False
468
469 return True
470
471 raise error.TypeErrVerbose(
472 "Can't compare two values of type %s" % ui.ValType(left), blame_loc)
473
474
475def Contains(needle, haystack):
476 # type: (value_t, value_t) -> bool
477 """Haystack must be a Dict.
478
479 We should have mylist->find(x) !== -1 for searching through a List.
480 Things with different perf characteristics should look different.
481 """
482 UP_haystack = haystack
483 with tagswitch(haystack) as case:
484 if case(value_e.Dict):
485 haystack = cast(value.Dict, UP_haystack)
486 s = ToStr(needle, "LHS of 'in' should be Str", loc.Missing)
487 return s in haystack.d
488
489 else:
490 raise error.TypeErr(haystack, "RHS of 'in' should be Dict",
491 loc.Missing)
492
493 return False
494
495
496def MatchRegex(left, right, mem):
497 # type: (value_t, value_t, Optional[state.Mem]) -> bool
498 """
499 Args:
500 mem: Whether to set or clear matches
501 """
502 UP_right = right
503
504 with tagswitch(right) as case:
505 if case(value_e.Str): # plain ERE
506 right = cast(value.Str, UP_right)
507
508 right_s = right.s
509 regex_flags = 0
510 capture = eggex_ops.No # type: eggex_ops_t
511
512 elif case(value_e.Eggex):
513 right = cast(value.Eggex, UP_right)
514
515 right_s = regex_translate.AsPosixEre(right)
516 regex_flags = regex_translate.LibcFlags(right.canonical_flags)
517 capture = eggex_ops.Yes(right.convert_funcs, right.convert_toks,
518 right.capture_names)
519
520 else:
521 raise error.TypeErr(right, 'Expected Str or Regex for RHS of ~',
522 loc.Missing)
523
524 UP_left = left
525 left_s = None # type: Optional[str]
526 with tagswitch(left) as case:
527 if case(value_e.Str):
528 left = cast(value.Str, UP_left)
529 left_s = left.s
530 else:
531 raise error.TypeErrVerbose('LHS must be a string', loc.Missing)
532
533 indices = libc.regex_search(right_s, regex_flags, left_s, 0)
534 if indices is not None:
535 if mem:
536 mem.SetRegexMatch(RegexMatch(left_s, indices, capture))
537 return True
538 else:
539 if mem:
540 mem.SetRegexMatch(regex_match.No)
541 return False
542
543
544def IndexMetaMethod(obj):
545 # type: (Obj) -> Optional[value_t]
546 """
547 Returns value.{BuiltinFunc,Func} -- but not callable Obj?
548 """
549 if not obj.prototype:
550 return None
551 index_val = obj.prototype.d.get('__index__')
552 if not index_val:
553 return None
554
555 if index_val.tag() not in (value_e.BuiltinFunc, value_e.Func):
556 return None
557
558 return index_val
559
560
561# vim: sw=4