OILS / builtin / assign_osh.py View on Github | oilshell.org

581 lines, 394 significant
1#!/usr/bin/env python2
2from __future__ import print_function
3
4from _devbuild.gen import arg_types
5from _devbuild.gen.option_asdl import builtin_i
6from _devbuild.gen.runtime_asdl import (
7 scope_e,
8 cmd_value,
9 AssignArg,
10)
11from _devbuild.gen.value_asdl import (value, value_e, value_t, LeftName)
12from _devbuild.gen.syntax_asdl import loc, loc_t, word_t
13
14from core import error
15from core.error import e_usage
16from core import state
17from core import vm
18from frontend import flag_util
19from frontend import args
20from mycpp import mylib
21from mycpp.mylib import log
22from osh import cmd_eval
23from osh import sh_expr_eval
24from data_lang import j8_lite
25
26from typing import cast, Optional, Dict, List, TYPE_CHECKING
27if TYPE_CHECKING:
28 from core.state import Mem
29 from core import optview
30 from core import ui
31 from frontend.args import _Attributes
32
33_ = log
34
35_OTHER = 0
36_READONLY = 1
37_EXPORT = 2
38
39
40def _PrintVariables(mem, cmd_val, attrs, print_flags, builtin=_OTHER):
41 # type: (Mem, cmd_value.Assign, _Attributes, bool, int) -> int
42 """
43 Args:
44 print_flags: whether to print flags
45 builtin: is it the readonly or export builtin?
46 """
47 flag = attrs.attrs
48
49 # Turn dynamic vars to static.
50 tmp_g = flag.get('g')
51 tmp_a = flag.get('a')
52 tmp_A = flag.get('A')
53
54 flag_g = (cast(value.Bool, tmp_g).b
55 if tmp_g and tmp_g.tag() == value_e.Bool else False)
56 flag_a = (cast(value.Bool, tmp_a).b
57 if tmp_a and tmp_a.tag() == value_e.Bool else False)
58 flag_A = (cast(value.Bool, tmp_A).b
59 if tmp_A and tmp_A.tag() == value_e.Bool else False)
60
61 tmp_n = flag.get('n')
62 tmp_r = flag.get('r')
63 tmp_x = flag.get('x')
64
65 #log('FLAG %r', flag)
66
67 # SUBTLE: export -n vs. declare -n. flag vs. OPTION.
68 # flags are value.Bool, while options are Undef or Str.
69 # '+', '-', or None
70 flag_n = (cast(value.Str, tmp_n).s if tmp_n and tmp_n.tag() == value_e.Str
71 else None) # type: Optional[str]
72 flag_r = (cast(value.Str, tmp_r).s if tmp_r and tmp_r.tag() == value_e.Str
73 else None) # type: Optional[str]
74 flag_x = (cast(value.Str, tmp_x).s if tmp_x and tmp_x.tag() == value_e.Str
75 else None) # type: Optional[str]
76
77 if cmd_val.builtin_id == builtin_i.local:
78 if flag_g and not mem.IsGlobalScope():
79 return 1
80 which_scopes = scope_e.LocalOnly
81 elif flag_g:
82 which_scopes = scope_e.GlobalOnly
83 else:
84 which_scopes = mem.ScopesForReading() # reading
85
86 if len(cmd_val.pairs) == 0:
87 print_all = True
88 cells = mem.GetAllCells(which_scopes)
89 names = sorted(cells) # type: List[str]
90 else:
91 print_all = False
92 names = []
93 cells = {}
94 for pair in cmd_val.pairs:
95 name = pair.var_name
96 if pair.rval and pair.rval.tag() == value_e.Str:
97 # Invalid: declare -p foo=bar
98 # Add a sentinel so we skip it, but know to exit with status 1.
99 s = cast(value.Str, pair.rval).s
100 invalid = "%s=%s" % (name, s)
101 names.append(invalid)
102 cells[invalid] = None
103 else:
104 names.append(name)
105 cells[name] = mem.GetCell(name, which_scopes)
106
107 count = 0
108 for name in names:
109 cell = cells[name]
110 if cell is None:
111 continue # Invalid
112 val = cell.val
113 #log('name %r %s', name, val)
114
115 if val.tag() == value_e.Undef:
116 continue
117 if builtin == _READONLY and not cell.readonly:
118 continue
119 if builtin == _EXPORT and not cell.exported:
120 continue
121
122 if flag_n == '-' and not cell.nameref:
123 continue
124 if flag_n == '+' and cell.nameref:
125 continue
126 if flag_r == '-' and not cell.readonly:
127 continue
128 if flag_r == '+' and cell.readonly:
129 continue
130 if flag_x == '-' and not cell.exported:
131 continue
132 if flag_x == '+' and cell.exported:
133 continue
134
135 if flag_a and val.tag() != value_e.BashArray:
136 continue
137 if flag_A and val.tag() != value_e.BashAssoc:
138 continue
139
140 decl = [] # type: List[str]
141 if print_flags:
142 flags = [] # type: List[str]
143 if cell.nameref:
144 flags.append('n')
145 if cell.readonly:
146 flags.append('r')
147 if cell.exported:
148 flags.append('x')
149 if val.tag() == value_e.BashArray:
150 flags.append('a')
151 elif val.tag() == value_e.BashAssoc:
152 flags.append('A')
153 if len(flags) == 0:
154 flags.append('-')
155
156 decl.extend(["declare -", ''.join(flags), " ", name])
157 else:
158 decl.append(name)
159
160 if val.tag() == value_e.Str:
161 str_val = cast(value.Str, val)
162 # TODO: Use fastfunc.ShellEncode()
163 decl.extend(["=", j8_lite.MaybeShellEncode(str_val.s)])
164
165 elif val.tag() == value_e.BashArray:
166 array_val = cast(value.BashArray, val)
167
168 # mycpp rewrite: None in array_val.strs
169 has_holes = False
170 for s in array_val.strs:
171 if s is None:
172 has_holes = True
173 break
174
175 if has_holes:
176 # Note: Arrays with unset elements are printed in the form:
177 # declare -p arr=(); arr[3]='' arr[4]='foo' ...
178 decl.append("=()")
179 first = True
180 for i, element in enumerate(array_val.strs):
181 if element is not None:
182 if first:
183 decl.append(";")
184 first = False
185 decl.extend([
186 " ", name, "[",
187 str(i), "]=",
188 j8_lite.MaybeShellEncode(element)
189 ])
190 else:
191 body = [] # type: List[str]
192 for element in array_val.strs:
193 if len(body) > 0:
194 body.append(" ")
195 body.append(j8_lite.MaybeShellEncode(element))
196 decl.extend(["=(", ''.join(body), ")"])
197
198 elif val.tag() == value_e.BashAssoc:
199 assoc_val = cast(value.BashAssoc, val)
200 body = []
201 for key in sorted(assoc_val.d):
202 if len(body) > 0:
203 body.append(" ")
204
205 key_quoted = j8_lite.ShellEncode(key)
206 value_quoted = j8_lite.MaybeShellEncode(assoc_val.d[key])
207
208 body.extend(["[", key_quoted, "]=", value_quoted])
209 if len(body) > 0:
210 decl.extend(["=(", ''.join(body), ")"])
211
212 else:
213 pass # note: other types silently ignored
214
215 print(''.join(decl))
216 count += 1
217
218 if print_all or count == len(names):
219 return 0
220 else:
221 return 1
222
223
224def _ExportReadonly(mem, pair, flags):
225 # type: (Mem, AssignArg, int) -> None
226 """For 'export' and 'readonly' to respect += and flags.
227
228 Like 'setvar' (scope_e.LocalOnly), unless dynamic scope is on. That is, it
229 respects shopt --unset dynamic_scope.
230
231 Used for assignment builtins, (( a = b )), {fd}>out, ${x=}, etc.
232 """
233 which_scopes = mem.ScopesForWriting()
234
235 lval = LeftName(pair.var_name, pair.blame_word)
236 if pair.plus_eq:
237 old_val = sh_expr_eval.OldValue(lval, mem, None) # ignore set -u
238 # When 'export e+=', then rval is value.Str('')
239 # When 'export foo', the pair.plus_eq flag is false.
240 assert pair.rval is not None
241 val = cmd_eval.PlusEquals(old_val, pair.rval)
242 else:
243 # NOTE: when rval is None, only flags are changed
244 val = pair.rval
245
246 mem.SetNamed(lval, val, which_scopes, flags=flags)
247
248
249class Export(vm._AssignBuiltin):
250
251 def __init__(self, mem, errfmt):
252 # type: (Mem, ui.ErrorFormatter) -> None
253 self.mem = mem
254 self.errfmt = errfmt
255
256 def Run(self, cmd_val):
257 # type: (cmd_value.Assign) -> int
258 arg_r = args.Reader(cmd_val.argv, locs=cmd_val.arg_locs)
259 arg_r.Next()
260 attrs = flag_util.Parse('export_', arg_r)
261 arg = arg_types.export_(attrs.attrs)
262 #arg = attrs
263
264 if arg.f:
265 e_usage(
266 "doesn't accept -f because it's dangerous. "
267 "(The code can usually be restructured with 'source')",
268 loc.Missing)
269
270 if arg.p or len(cmd_val.pairs) == 0:
271 return _PrintVariables(self.mem,
272 cmd_val,
273 attrs,
274 True,
275 builtin=_EXPORT)
276
277 if arg.n:
278 for pair in cmd_val.pairs:
279 if pair.rval is not None:
280 e_usage("doesn't accept RHS with -n",
281 loc.Word(pair.blame_word))
282
283 # NOTE: we don't care if it wasn't found, like bash.
284 self.mem.ClearFlag(pair.var_name, state.ClearExport)
285 else:
286 for pair in cmd_val.pairs:
287 _ExportReadonly(self.mem, pair, state.SetExport)
288
289 return 0
290
291
292def _ReconcileTypes(rval, flag_a, flag_A, blame_word):
293 # type: (Optional[value_t], bool, bool, word_t) -> value_t
294 """Check that -a and -A flags are consistent with RHS.
295
296 Special case: () is allowed to mean empty indexed array or empty assoc array
297 if the context is clear.
298
299 Shared between NewVar and Readonly.
300 """
301 if flag_a and rval is not None and rval.tag() != value_e.BashArray:
302 e_usage("Got -a but RHS isn't an array", loc.Word(blame_word))
303
304 if flag_A and rval:
305 # Special case: declare -A A=() is OK. The () is changed to mean an empty
306 # associative array.
307 if rval.tag() == value_e.BashArray:
308 array_val = cast(value.BashArray, rval)
309 if len(array_val.strs) == 0:
310 return value.BashAssoc({})
311 #return value.BashArray([])
312
313 if rval.tag() != value_e.BashAssoc:
314 e_usage("Got -A but RHS isn't an associative array",
315 loc.Word(blame_word))
316
317 return rval
318
319
320class Readonly(vm._AssignBuiltin):
321
322 def __init__(self, mem, errfmt):
323 # type: (Mem, ui.ErrorFormatter) -> None
324 self.mem = mem
325 self.errfmt = errfmt
326
327 def Run(self, cmd_val):
328 # type: (cmd_value.Assign) -> int
329 arg_r = args.Reader(cmd_val.argv, locs=cmd_val.arg_locs)
330 arg_r.Next()
331 attrs = flag_util.Parse('readonly', arg_r)
332 arg = arg_types.readonly(attrs.attrs)
333
334 if arg.p or len(cmd_val.pairs) == 0:
335 return _PrintVariables(self.mem,
336 cmd_val,
337 attrs,
338 True,
339 builtin=_READONLY)
340
341 for pair in cmd_val.pairs:
342 if pair.rval is None:
343 if arg.a:
344 rval = value.BashArray([]) # type: value_t
345 elif arg.A:
346 rval = value.BashAssoc({})
347 else:
348 rval = None
349 else:
350 rval = pair.rval
351
352 rval = _ReconcileTypes(rval, arg.a, arg.A, pair.blame_word)
353
354 # NOTE:
355 # - when rval is None, only flags are changed
356 # - dynamic scope because flags on locals can be changed, etc.
357 _ExportReadonly(self.mem, pair, state.SetReadOnly)
358
359 return 0
360
361
362class NewVar(vm._AssignBuiltin):
363 """declare/typeset/local."""
364
365 def __init__(self, mem, procs, exec_opts, errfmt):
366 # type: (Mem, state.Procs, optview.Exec, ui.ErrorFormatter) -> None
367 self.mem = mem
368 self.procs = procs
369 self.exec_opts = exec_opts
370 self.errfmt = errfmt
371
372 def _PrintFuncs(self, names):
373 # type: (List[str]) -> int
374 status = 0
375 for name in names:
376 if self.procs.GetProc(name):
377 print(name)
378 # TODO: Could print LST for -f, or render LST. Bash does this. 'trap'
379 # could use that too.
380 else:
381 status = 1
382 return status
383
384 def Run(self, cmd_val):
385 # type: (cmd_value.Assign) -> int
386 arg_r = args.Reader(cmd_val.argv, locs=cmd_val.arg_locs)
387 arg_r.Next()
388 attrs = flag_util.Parse('new_var', arg_r)
389 arg = arg_types.new_var(attrs.attrs)
390
391 status = 0
392
393 if arg.f:
394 names = arg_r.Rest()
395 if len(names):
396 # This is only used for a STATUS QUERY now. We only show the name,
397 # not the body.
398 status = self._PrintFuncs(names)
399 else:
400 # Disallow this since it would be incompatible.
401 e_usage('with -f expects function names', loc.Missing)
402 return status
403
404 if arg.F:
405 names = arg_r.Rest()
406 if len(names):
407 status = self._PrintFuncs(names)
408 else:
409 # bash quirk: with no names, they're printed in a different format!
410 for func_name in sorted(self.procs.procs): # TODO
411 print('declare -f %s' % (func_name))
412 return status
413
414 if arg.p: # Lookup and print variables.
415 return _PrintVariables(self.mem, cmd_val, attrs, True)
416 elif len(cmd_val.pairs) == 0:
417 return _PrintVariables(self.mem, cmd_val, attrs, False)
418
419 if not self.exec_opts.ignore_flags_not_impl():
420 if arg.i:
421 e_usage(
422 "doesn't implement flag -i (shopt --set ignore_flags_not_impl)",
423 loc.Missing)
424
425 if arg.l or arg.u:
426 # Just print a warning! The program may still run.
427 self.errfmt.Print_(
428 "Warning: OSH doesn't implement flags -l or -u (shopt --set ignore_flags_not_impl)",
429 loc.Missing)
430
431 #
432 # Set variables
433 #
434
435 if cmd_val.builtin_id == builtin_i.local:
436 which_scopes = scope_e.LocalOnly
437 else: # declare/typeset
438 if arg.g:
439 which_scopes = scope_e.GlobalOnly
440 else:
441 which_scopes = scope_e.LocalOnly
442
443 flags = 0
444 if arg.x == '-':
445 flags |= state.SetExport
446 if arg.r == '-':
447 flags |= state.SetReadOnly
448 if arg.n == '-':
449 flags |= state.SetNameref
450
451 if arg.x == '+':
452 flags |= state.ClearExport
453 if arg.r == '+':
454 flags |= state.ClearReadOnly
455 if arg.n == '+':
456 flags |= state.ClearNameref
457
458 for pair in cmd_val.pairs:
459 rval = pair.rval
460 # declare -a foo=(a b); declare -a foo; should not reset to empty array
461 if rval is None and (arg.a or arg.A):
462 old_val = self.mem.GetValue(pair.var_name)
463 if arg.a:
464 if old_val.tag() != value_e.BashArray:
465 rval = value.BashArray([])
466 elif arg.A:
467 if old_val.tag() != value_e.BashAssoc:
468 rval = value.BashAssoc({})
469
470 lval = LeftName(pair.var_name, pair.blame_word)
471
472 if pair.plus_eq:
473 old_val = sh_expr_eval.OldValue(lval, self.mem,
474 None) # ignore set -u
475 # When 'typeset e+=', then rval is value.Str('')
476 # When 'typeset foo', the pair.plus_eq flag is false.
477 assert pair.rval is not None
478 rval = cmd_eval.PlusEquals(old_val, pair.rval)
479 else:
480 rval = _ReconcileTypes(rval, arg.a, arg.A, pair.blame_word)
481
482 self.mem.SetNamed(lval, rval, which_scopes, flags=flags)
483
484 return status
485
486
487# TODO:
488# - It would make more sense to treat no args as an error (bash doesn't.)
489# - Should we have strict builtins? Or just make it stricter?
490# - Typed args: unset (mylist[0]) is like Python's del
491# - It has the same word as 'setvar', which makes sense
492
493
494class Unset(vm._Builtin):
495
496 def __init__(
497 self,
498 mem, # type: state.Mem
499 procs, # type: state.Procs
500 unsafe_arith, # type: sh_expr_eval.UnsafeArith
501 errfmt, # type: ui.ErrorFormatter
502 ):
503 # type: (...) -> None
504 self.mem = mem
505 self.procs = procs
506 self.unsafe_arith = unsafe_arith
507 self.errfmt = errfmt
508
509 def _UnsetVar(self, arg, location, proc_fallback):
510 # type: (str, loc_t, bool) -> bool
511 """
512 Returns:
513 bool: whether the 'unset' builtin should succeed with code 0.
514 """
515 lval = self.unsafe_arith.ParseLValue(arg, location)
516
517 #log('unsafe lval %s', lval)
518 found = False
519 try:
520 found = self.mem.Unset(lval, scope_e.Shopt)
521 except error.Runtime as e:
522 # note: in bash, myreadonly=X fails, but declare myreadonly=X doesn't
523 # fail because it's a builtin. So I guess the same is true of 'unset'.
524 msg = e.UserErrorString()
525 self.errfmt.Print_(msg, blame_loc=location)
526 return False
527
528 if proc_fallback and not found:
529 # TODO: Hmmmm, this is an interesting interaction. Is this right?
530 mylib.dict_erase(self.procs.procs, arg)
531 # mylib.dict_erase(self.procs, arg)
532
533 return True
534
535 def Run(self, cmd_val):
536 # type: (cmd_value.Argv) -> int
537 attrs, arg_r = flag_util.ParseCmdVal('unset', cmd_val)
538 arg = arg_types.unset(attrs.attrs)
539
540 argv, arg_locs = arg_r.Rest2()
541 for i, name in enumerate(argv):
542 location = arg_locs[i]
543
544 if arg.f:
545 # TODO: here too
546 mylib.dict_erase(self.procs.procs, name)
547 # mylib.dict_erase(self.procs, name)
548
549 elif arg.v:
550 if not self._UnsetVar(name, location, False):
551 return 1
552
553 else:
554 # proc_fallback: Try to delete var first, then func.
555 if not self._UnsetVar(name, location, True):
556 return 1
557
558 return 0
559
560
561class Shift(vm._Builtin):
562
563 def __init__(self, mem):
564 # type: (Mem) -> None
565 self.mem = mem
566
567 def Run(self, cmd_val):
568 # type: (cmd_value.Argv) -> int
569 num_args = len(cmd_val.argv) - 1
570 if num_args == 0:
571 n = 1
572 elif num_args == 1:
573 arg = cmd_val.argv[1]
574 try:
575 n = int(arg)
576 except ValueError:
577 e_usage("Invalid shift argument %r" % arg, loc.Missing)
578 else:
579 e_usage('got too many arguments', loc.Missing)
580
581 return self.mem.Shift(n)