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

543 lines, 315 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 Dict_)
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
27if 0:
28
29 def PlainDict(d):
30 # type: (Dict[str, value_t]) -> Dict_
31 """
32 Shorthand for "plain old data", i.e. data without behavior
33 """
34 return Dict_(d, None)
35
36
37def ToInt(val, msg, blame_loc):
38 # type: (value_t, str, loc_t) -> int
39 UP_val = val
40 if val.tag() == value_e.Int:
41 val = cast(value.Int, UP_val)
42 return mops.BigTruncate(val.i)
43
44 raise error.TypeErr(val, msg, blame_loc)
45
46
47def ToFloat(val, msg, blame_loc):
48 # type: (value_t, str, loc_t) -> float
49 UP_val = val
50 if val.tag() == value_e.Float:
51 val = cast(value.Float, UP_val)
52 return val.f
53
54 raise error.TypeErr(val, msg, blame_loc)
55
56
57def ToStr(val, msg, blame_loc):
58 # type: (value_t, str, loc_t) -> str
59 UP_val = val
60 if val.tag() == value_e.Str:
61 val = cast(value.Str, UP_val)
62 return val.s
63
64 raise error.TypeErr(val, msg, blame_loc)
65
66
67def ToList(val, msg, blame_loc):
68 # type: (value_t, str, loc_t) -> List[value_t]
69 UP_val = val
70 if val.tag() == value_e.List:
71 val = cast(value.List, UP_val)
72 return val.items
73
74 raise error.TypeErr(val, msg, blame_loc)
75
76
77def ToDict(val, msg, blame_loc):
78 # type: (value_t, str, loc_t) -> Dict[str, value_t]
79 UP_val = val
80 if val.tag() == value_e.Dict:
81 val = cast(Dict_, UP_val)
82 return val.d
83
84 raise error.TypeErr(val, msg, blame_loc)
85
86
87def ToCommand(val, msg, blame_loc):
88 # type: (value_t, str, loc_t) -> command_t
89 UP_val = val
90 if val.tag() == value_e.Command:
91 val = cast(value.Command, UP_val)
92 return val.c
93
94 raise error.TypeErr(val, msg, blame_loc)
95
96
97def Stringify(val, blame_loc, prefix=''):
98 # type: (value_t, loc_t, str) -> str
99 """
100 Used by
101
102 $[x] stringify operator
103 @[x] expression splice - each element is stringified
104 @x splice value
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 # TODO: what precision does this have?
130 # The default could be like awk or Python, and then we also allow
131 # ${myfloat %.3f} and more.
132 # Python 3 seems to give a few more digits than Python 2 for str(1.0/3)
133 s = str(val.f)
134
135 elif case(value_e.Eggex):
136 val = cast(value.Eggex, UP_val)
137 s = regex_translate.AsPosixEre(val) # lazily converts to ERE
138
139 elif case(value_e.List):
140 raise error.TypeErrVerbose(
141 "%sgot a List, which can't be stringified. Perhaps use @ instead of $, or use join()"
142 % prefix, blame_loc)
143
144 else:
145 raise error.TypeErr(
146 val, "%sexpected Null, Bool, Int, Float, Eggex" % prefix,
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=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: (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(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(Dict_, UP_left)
447 right = cast(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(Dict_, UP_left)
459 right = cast(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(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
543# vim: sw=4