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

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