OILS / builtin / assign_osh.py View on Github | oils.pub

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