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

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