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

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