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

514 lines, 303 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
204class StdinIterator(Iterator):
205 """ for x in <> { """
206
207 def __init__(self):
208 # type: () -> None
209 Iterator.__init__(self)
210 self.f = mylib.Stdin()
211
212 def FirstValue(self):
213 # type: () -> Optional[value_t]
214
215 # line, eof = read_osh.ReadLineSlowly(None, with_eol=False)
216 try:
217 line = self.f.readline()
218 except (IOError, OSError) as e: # signals
219 # TODO: run traps run traps with cmd_ev, like ReadLineSlowly
220 if e.errno == EINTR:
221 pass
222
223 if len(line) == 0:
224 return None
225 elif line.endswith('\n'):
226 # TODO: optimize this to prevent extra garbage
227 line = line[:-1]
228
229 return value.Str(line)
230
231
232class ArrayIter(Iterator):
233 """ for x in 1 2 3 { """
234
235 def __init__(self, strs):
236 # type: (List[str]) -> None
237 Iterator.__init__(self)
238 self.strs = strs
239 self.n = len(strs)
240
241 def FirstValue(self):
242 # type: () -> Optional[value_t]
243 if self.i == self.n:
244 return None
245 return value.Str(self.strs[self.i])
246
247
248class RangeIterator(Iterator):
249 """ for x in (m:n) { """
250
251 def __init__(self, val):
252 # type: (value.Range) -> None
253 Iterator.__init__(self)
254 self.val = val
255
256 def FirstValue(self):
257 # type: () -> Optional[value_t]
258 if self.val.lower + self.i >= self.val.upper:
259 return None
260
261 # TODO: range should be BigInt too
262 return value.Int(mops.IntWiden(self.val.lower + self.i))
263
264
265class ListIterator(Iterator):
266 """ for x in (mylist) { """
267
268 def __init__(self, val):
269 # type: (value.List) -> None
270 Iterator.__init__(self)
271 self.val = val
272 self.n = len(val.items)
273
274 def FirstValue(self):
275 # type: () -> Optional[value_t]
276 if self.i == self.n:
277 return None
278 return self.val.items[self.i]
279
280
281class DictIterator(Iterator):
282 """ for x in (mydict) { """
283
284 def __init__(self, val):
285 # type: (value.Dict) -> None
286 Iterator.__init__(self)
287
288 # TODO: Don't materialize these Lists
289 self.keys = val.d.keys() # type: List[str]
290 self.values = val.d.values() # type: List[value_t]
291
292 self.n = len(val.d)
293 assert self.n == len(self.keys)
294
295 def FirstValue(self):
296 # type: () -> value_t
297 if self.i == self.n:
298 return None
299 return value.Str(self.keys[self.i])
300
301 def SecondValue(self):
302 # type: () -> value_t
303 return self.values[self.i]
304
305
306def ToBool(val):
307 # type: (value_t) -> bool
308 """Convert any value to a boolean.
309
310 TODO: expose this as Bool(x), like Python's bool(x).
311 """
312 UP_val = val
313 with tagswitch(val) as case:
314 if case(value_e.Undef):
315 return False
316
317 elif case(value_e.Null):
318 return False
319
320 elif case(value_e.Str):
321 val = cast(value.Str, UP_val)
322 return len(val.s) != 0
323
324 # OLD TYPES
325 elif case(value_e.BashArray):
326 val = cast(value.BashArray, UP_val)
327 return len(val.strs) != 0
328
329 elif case(value_e.BashAssoc):
330 val = cast(value.BashAssoc, UP_val)
331 return len(val.d) != 0
332
333 elif case(value_e.Bool):
334 val = cast(value.Bool, UP_val)
335 return val.b
336
337 elif case(value_e.Int):
338 val = cast(value.Int, UP_val)
339 return not mops.Equal(val.i, mops.BigInt(0))
340
341 elif case(value_e.Float):
342 val = cast(value.Float, UP_val)
343 return val.f != 0.0
344
345 elif case(value_e.List):
346 val = cast(value.List, UP_val)
347 return len(val.items) > 0
348
349 elif case(value_e.Dict):
350 val = cast(value.Dict, UP_val)
351 return len(val.d) > 0
352
353 else:
354 return True # all other types are Truthy
355
356
357def ExactlyEqual(left, right, blame_loc):
358 # type: (value_t, value_t, loc_t) -> bool
359 if left.tag() != right.tag():
360 return False
361
362 UP_left = left
363 UP_right = right
364 with tagswitch(left) as case:
365 if case(value_e.Undef):
366 return True # there's only one Undef
367
368 elif case(value_e.Null):
369 return True # there's only one Null
370
371 elif case(value_e.Bool):
372 left = cast(value.Bool, UP_left)
373 right = cast(value.Bool, UP_right)
374 return left.b == right.b
375
376 elif case(value_e.Int):
377 left = cast(value.Int, UP_left)
378 right = cast(value.Int, UP_right)
379 return mops.Equal(left.i, right.i)
380
381 elif case(value_e.Float):
382 # Note: could provide floatEquals(), and suggest it
383 # Suggested idiom is abs(f1 - f2) < 0.1
384 raise error.TypeErrVerbose("Equality isn't defined on Float",
385 blame_loc)
386
387 elif case(value_e.Str):
388 left = cast(value.Str, UP_left)
389 right = cast(value.Str, UP_right)
390 return left.s == right.s
391
392 elif case(value_e.BashArray):
393 left = cast(value.BashArray, UP_left)
394 right = cast(value.BashArray, UP_right)
395 if len(left.strs) != len(right.strs):
396 return False
397
398 for i in xrange(0, len(left.strs)):
399 if left.strs[i] != right.strs[i]:
400 return False
401
402 return True
403
404 elif case(value_e.List):
405 left = cast(value.List, UP_left)
406 right = cast(value.List, UP_right)
407 if len(left.items) != len(right.items):
408 return False
409
410 for i in xrange(0, len(left.items)):
411 if not ExactlyEqual(left.items[i], right.items[i], blame_loc):
412 return False
413
414 return True
415
416 elif case(value_e.BashAssoc):
417 left = cast(value.Dict, UP_left)
418 right = cast(value.Dict, UP_right)
419 if len(left.d) != len(right.d):
420 return False
421
422 for k in left.d.keys():
423 if k not in right.d or right.d[k] != left.d[k]:
424 return False
425
426 return True
427
428 elif case(value_e.Dict):
429 left = cast(value.Dict, UP_left)
430 right = cast(value.Dict, UP_right)
431 if len(left.d) != len(right.d):
432 return False
433
434 for k in left.d.keys():
435 if (k not in right.d or
436 not ExactlyEqual(right.d[k], left.d[k], blame_loc)):
437 return False
438
439 return True
440
441 raise error.TypeErrVerbose(
442 "Can't compare two values of type %s" % ui.ValType(left), blame_loc)
443
444
445def Contains(needle, haystack):
446 # type: (value_t, value_t) -> bool
447 """Haystack must be a Dict.
448
449 We should have mylist->find(x) !== -1 for searching through a List.
450 Things with different perf characteristics should look different.
451 """
452 UP_haystack = haystack
453 with tagswitch(haystack) as case:
454 if case(value_e.Dict):
455 haystack = cast(value.Dict, UP_haystack)
456 s = ToStr(needle, "LHS of 'in' should be Str", loc.Missing)
457 return s in haystack.d
458
459 else:
460 raise error.TypeErr(haystack, "RHS of 'in' should be Dict",
461 loc.Missing)
462
463 return False
464
465
466def MatchRegex(left, right, mem):
467 # type: (value_t, value_t, Optional[state.Mem]) -> bool
468 """
469 Args:
470 mem: Whether to set or clear matches
471 """
472 UP_right = right
473
474 with tagswitch(right) as case:
475 if case(value_e.Str): # plain ERE
476 right = cast(value.Str, UP_right)
477
478 right_s = right.s
479 regex_flags = 0
480 capture = eggex_ops.No # type: eggex_ops_t
481
482 elif case(value_e.Eggex):
483 right = cast(value.Eggex, UP_right)
484
485 right_s = regex_translate.AsPosixEre(right)
486 regex_flags = regex_translate.LibcFlags(right.canonical_flags)
487 capture = eggex_ops.Yes(right.convert_funcs, right.convert_toks,
488 right.capture_names)
489
490 else:
491 raise error.TypeErr(right, 'Expected Str or Regex for RHS of ~',
492 loc.Missing)
493
494 UP_left = left
495 left_s = None # type: str
496 with tagswitch(left) as case:
497 if case(value_e.Str):
498 left = cast(value.Str, UP_left)
499 left_s = left.s
500 else:
501 raise error.TypeErrVerbose('LHS must be a string', loc.Missing)
502
503 indices = libc.regex_search(right_s, regex_flags, left_s, 0)
504 if indices is not None:
505 if mem:
506 mem.SetRegexMatch(RegexMatch(left_s, indices, capture))
507 return True
508 else:
509 if mem:
510 mem.SetRegexMatch(regex_match.No)
511 return False
512
513
514# vim: sw=4