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

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