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

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