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

533 lines, 309 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 core 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 # TODO: return value.Interrupted, so we can run traps
224 # with cmd_ev, like ReadLineSlowly
225 #log('EINTR')
226 return None
227 else:
228 # For possible errors, see
229 # man read
230 # man getline
231 # e.g.
232 # - ENOMEM getline() allocation failure
233 # - EISDIR getline() read from directory descriptor!
234 #
235 # Note: the read builtin returns status 1 for EISDIR.
236 #
237 # We'll raise a top-level error like Python. (Awk prints a
238 # warning message)
239 e_die("I/O error in for <> loop: %s" % posix.strerror(e.errno),
240 self.blame_loc)
241
242 if len(line) == 0:
243 return None # Done
244 elif line.endswith('\n'):
245 # TODO: optimize this to prevent extra garbage
246 line = line[:-1]
247
248 return value.Str(line)
249
250
251class ArrayIter(Iterator):
252 """ for x in 1 2 3 { """
253
254 def __init__(self, strs):
255 # type: (List[str]) -> None
256 Iterator.__init__(self)
257 self.strs = strs
258 self.n = len(strs)
259
260 def FirstValue(self):
261 # type: () -> Optional[value_t]
262 if self.i == self.n:
263 return None
264 return value.Str(self.strs[self.i])
265
266
267class RangeIterator(Iterator):
268 """ for x in (m:n) { """
269
270 def __init__(self, val):
271 # type: (value.Range) -> None
272 Iterator.__init__(self)
273 self.val = val
274
275 def FirstValue(self):
276 # type: () -> Optional[value_t]
277 if self.val.lower + self.i >= self.val.upper:
278 return None
279
280 # TODO: range should be BigInt too
281 return value.Int(mops.IntWiden(self.val.lower + self.i))
282
283
284class ListIterator(Iterator):
285 """ for x in (mylist) { """
286
287 def __init__(self, val):
288 # type: (value.List) -> None
289 Iterator.__init__(self)
290 self.val = val
291 self.n = len(val.items)
292
293 def FirstValue(self):
294 # type: () -> Optional[value_t]
295 if self.i == self.n:
296 return None
297 return self.val.items[self.i]
298
299
300class DictIterator(Iterator):
301 """ for x in (mydict) { """
302
303 def __init__(self, val):
304 # type: (value.Dict) -> None
305 Iterator.__init__(self)
306
307 # TODO: Don't materialize these Lists
308 self.keys = val.d.keys() # type: List[str]
309 self.values = val.d.values() # type: List[value_t]
310
311 self.n = len(val.d)
312 assert self.n == len(self.keys)
313
314 def FirstValue(self):
315 # type: () -> value_t
316 if self.i == self.n:
317 return None
318 return value.Str(self.keys[self.i])
319
320 def SecondValue(self):
321 # type: () -> value_t
322 return self.values[self.i]
323
324
325def ToBool(val):
326 # type: (value_t) -> bool
327 """Convert any value to a boolean.
328
329 TODO: expose this as Bool(x), like Python's bool(x).
330 """
331 UP_val = val
332 with tagswitch(val) as case:
333 if case(value_e.Undef):
334 return False
335
336 elif case(value_e.Null):
337 return False
338
339 elif case(value_e.Str):
340 val = cast(value.Str, UP_val)
341 return len(val.s) != 0
342
343 # OLD TYPES
344 elif case(value_e.BashArray):
345 val = cast(value.BashArray, UP_val)
346 return len(val.strs) != 0
347
348 elif case(value_e.BashAssoc):
349 val = cast(value.BashAssoc, UP_val)
350 return len(val.d) != 0
351
352 elif case(value_e.Bool):
353 val = cast(value.Bool, UP_val)
354 return val.b
355
356 elif case(value_e.Int):
357 val = cast(value.Int, UP_val)
358 return not mops.Equal(val.i, mops.BigInt(0))
359
360 elif case(value_e.Float):
361 val = cast(value.Float, UP_val)
362 return val.f != 0.0
363
364 elif case(value_e.List):
365 val = cast(value.List, UP_val)
366 return len(val.items) > 0
367
368 elif case(value_e.Dict):
369 val = cast(value.Dict, UP_val)
370 return len(val.d) > 0
371
372 else:
373 return True # all other types are Truthy
374
375
376def ExactlyEqual(left, right, blame_loc):
377 # type: (value_t, value_t, loc_t) -> bool
378 if left.tag() != right.tag():
379 return False
380
381 UP_left = left
382 UP_right = right
383 with tagswitch(left) as case:
384 if case(value_e.Undef):
385 return True # there's only one Undef
386
387 elif case(value_e.Null):
388 return True # there's only one Null
389
390 elif case(value_e.Bool):
391 left = cast(value.Bool, UP_left)
392 right = cast(value.Bool, UP_right)
393 return left.b == right.b
394
395 elif case(value_e.Int):
396 left = cast(value.Int, UP_left)
397 right = cast(value.Int, UP_right)
398 return mops.Equal(left.i, right.i)
399
400 elif case(value_e.Float):
401 # Note: could provide floatEquals(), and suggest it
402 # Suggested idiom is abs(f1 - f2) < 0.1
403 raise error.TypeErrVerbose("Equality isn't defined on Float",
404 blame_loc)
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