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

654 lines, 460 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
326 if arg.f:
327 e_usage(
328 "doesn't accept -f because it's dangerous. "
329 "(The code can usually be restructured with 'source')",
330 loc.Missing)
331
332 if arg.p or len(cmd_val.pairs) == 0:
333 return _PrintVariables(self.mem,
334 self.errfmt,
335 cmd_val,
336 attrs,
337 True,
338 builtin=_EXPORT)
339
340 if arg.n:
341 for pair in cmd_val.pairs:
342 if pair.rval is not None:
343 e_usage("doesn't accept RHS with -n",
344 loc.Word(pair.blame_word))
345
346 # NOTE: we don't care if it wasn't found, like bash.
347 self.mem.ClearFlag(pair.var_name, state.ClearExport)
348 else:
349 which_scopes = self.mem.ScopesForWriting()
350 for pair in cmd_val.pairs:
351 _AssignVarForBuiltin(self.mem, pair.rval, pair, which_scopes,
352 state.SetExport, self.arith_ev, False,
353 False)
354
355 return 0
356
357
358def _ReconcileTypes(rval, flag_a, flag_A, pair, mem):
359 # type: (Optional[value_t], bool, bool, AssignArg, state.Mem) -> value_t
360 """Check that -a and -A flags are consistent with RHS.
361
362 If RHS is empty and the current value of LHS has a different type from the
363 one expected by the -a and -A flags, we create an empty array.
364
365 Special case: () is allowed to mean empty indexed array or empty assoc array
366 if the context is clear.
367
368 Shared between NewVar and Readonly.
369
370 """
371
372 if rval is None:
373 # declare -a foo=(a b); declare -a foo; should not reset to empty array
374 if flag_a:
375 old_val = mem.GetValue(pair.var_name)
376 if old_val.tag() not in (value_e.InternalStringArray,
377 value_e.BashArray):
378 rval = bash_impl.BashArray_New()
379 elif flag_A:
380 old_val = mem.GetValue(pair.var_name)
381 if old_val.tag() != value_e.BashAssoc:
382 rval = bash_impl.BashAssoc_New()
383 else:
384 if flag_a:
385 if rval.tag() != value_e.InitializerList:
386 e_usage("Got -a but RHS isn't an initializer list",
387 loc.Word(pair.blame_word))
388 elif flag_A:
389 if rval.tag() != value_e.InitializerList:
390 e_usage("Got -A but RHS isn't an initializer list",
391 loc.Word(pair.blame_word))
392
393 return rval
394
395
396class Readonly(vm._AssignBuiltin):
397
398 def __init__(self, mem, arith_ev, errfmt):
399 # type: (state.Mem, sh_expr_eval.ArithEvaluator, ui.ErrorFormatter) -> None
400 self.mem = mem
401 self.arith_ev = arith_ev
402 self.errfmt = errfmt
403
404 def Run(self, cmd_val):
405 # type: (cmd_value.Assign) -> int
406 arg_r = args.Reader(cmd_val.argv, locs=cmd_val.arg_locs)
407 arg_r.Next()
408 attrs = flag_util.Parse('readonly', arg_r)
409 arg = arg_types.readonly(attrs.attrs)
410
411 if arg.p or len(cmd_val.pairs) == 0:
412 return _PrintVariables(self.mem,
413 self.errfmt,
414 cmd_val,
415 attrs,
416 True,
417 builtin=_READONLY)
418
419 which_scopes = self.mem.ScopesForWriting()
420 for pair in cmd_val.pairs:
421 rval = _ReconcileTypes(pair.rval, arg.a, arg.A, pair, self.mem)
422
423 # NOTE:
424 # - when rval is None, only flags are changed
425 # - dynamic scope because flags on locals can be changed, etc.
426 _AssignVarForBuiltin(self.mem, rval, pair, which_scopes,
427 state.SetReadOnly, self.arith_ev, arg.a,
428 arg.A)
429
430 return 0
431
432
433class NewVar(vm._AssignBuiltin):
434 """declare/typeset/local."""
435
436 def __init__(
437 self,
438 mem, # type: state.Mem
439 procs, # type: state.Procs
440 exec_opts, # type: optview.Exec
441 arith_ev, # type: sh_expr_eval.ArithEvaluator
442 errfmt, # type: ui.ErrorFormatter
443 ):
444 # type: (...) -> None
445 self.mem = mem
446 self.procs = procs
447 self.exec_opts = exec_opts
448 self.arith_ev = arith_ev
449 self.errfmt = errfmt
450
451 def _PrintFuncs(self, names, print_source):
452 # type: (List[str], bool) -> int
453 status = 0
454 for name in names:
455 proc_val = self.procs.GetShellFunc(name)
456 was_printed = False
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
468 # print function body if we can
469 elif print_source:
470 ui.PrintShFunction(proc_val)
471
472 # Fall back to name only, e.g. for rare non-Bracegroup functions like
473 # f() ( echo hi )
474 else:
475 print(name)
476 else:
477 status = 1
478 return status
479
480 def Run(self, cmd_val):
481 # type: (cmd_value.Assign) -> int
482 arg_r = args.Reader(cmd_val.argv, locs=cmd_val.arg_locs)
483 arg_r.Next()
484 attrs = flag_util.Parse('new_var', arg_r)
485 arg = arg_types.new_var(attrs.attrs)
486
487 status = 0
488
489 if arg.f:
490 names = arg_r.Rest()
491 if len(names):
492 # This is only used for a STATUS QUERY now. We only show the name,
493 # not the body.
494 status = self._PrintFuncs(names, True)
495 else:
496 # Disallow this since it would be incompatible.
497 e_usage('with -f expects function names', loc.Missing)
498 return status
499
500 if arg.F:
501 names = arg_r.Rest()
502 if len(names):
503 status = self._PrintFuncs(names, False)
504 else:
505 # bash quirk: with no names, they're printed in a different format!
506 for func_name in self.procs.ShellFuncNames():
507 print('declare -f %s' % (func_name))
508 return status
509
510 if arg.p: # Lookup and print variables.
511 return _PrintVariables(self.mem, self.errfmt, cmd_val, attrs, True)
512 elif len(cmd_val.pairs) == 0:
513 return _PrintVariables(self.mem, self.errfmt, cmd_val, attrs,
514 False)
515
516 if not self.exec_opts.ignore_flags_not_impl():
517 if arg.i:
518 e_usage(
519 "doesn't implement flag -i (shopt --set ignore_flags_not_impl)",
520 loc.Missing)
521
522 if arg.l or arg.u:
523 # Just print a warning! The program may still run.
524 self.errfmt.Print_(
525 "Warning: OSH doesn't implement flags -l or -u (shopt --set ignore_flags_not_impl)",
526 loc.Missing)
527
528 #
529 # Set variables
530 #
531
532 if cmd_val.builtin_id == builtin_i.local:
533 which_scopes = scope_e.LocalOnly
534 else: # declare/typeset
535 if arg.g:
536 which_scopes = scope_e.GlobalOnly
537 else:
538 which_scopes = scope_e.LocalOnly
539
540 flags = 0
541 if arg.x == '-':
542 flags |= state.SetExport
543 if arg.r == '-':
544 flags |= state.SetReadOnly
545 if arg.n == '-':
546 flags |= state.SetNameref
547
548 if arg.x == '+':
549 flags |= state.ClearExport
550 if arg.r == '+':
551 flags |= state.ClearReadOnly
552 if arg.n == '+':
553 flags |= state.ClearNameref
554
555 for pair in cmd_val.pairs:
556 rval = _ReconcileTypes(pair.rval, arg.a, arg.A, pair, self.mem)
557
558 _AssignVarForBuiltin(self.mem, rval, pair, which_scopes, flags,
559 self.arith_ev, arg.a, arg.A)
560
561 return status
562
563
564# TODO:
565# - It would make more sense to treat no args as an error (bash doesn't.)
566# - Should we have strict builtins? Or just make it stricter?
567# - Typed args: unset (mylist[0]) is like Python's del
568# - It has the same word as 'setvar', which makes sense
569
570
571class Unset(vm._Builtin):
572
573 def __init__(
574 self,
575 mem, # type: state.Mem
576 procs, # type: state.Procs
577 unsafe_arith, # type: sh_expr_eval.UnsafeArith
578 errfmt, # type: ui.ErrorFormatter
579 ):
580 # type: (...) -> None
581 self.mem = mem
582 self.procs = procs
583 self.unsafe_arith = unsafe_arith
584 self.errfmt = errfmt
585
586 def _UnsetVar(self, arg, location, proc_fallback):
587 # type: (str, loc_t, bool) -> bool
588 """
589 Returns:
590 bool: whether the 'unset' builtin should succeed with code 0.
591 """
592 lval = self.unsafe_arith.ParseLValue(arg, location)
593
594 #log('unsafe lval %s', lval)
595 found = False
596 try:
597 found = self.mem.Unset(lval, scope_e.Shopt)
598 except error.Runtime as e:
599 # note: in bash, myreadonly=X fails, but declare myreadonly=X doesn't
600 # fail because it's a builtin. So I guess the same is true of 'unset'.
601 msg = e.UserErrorString()
602 self.errfmt.Print_(msg, blame_loc=location)
603 return False
604
605 if proc_fallback and not found:
606 self.procs.EraseShellFunc(arg)
607
608 return True
609
610 def Run(self, cmd_val):
611 # type: (cmd_value.Argv) -> int
612 attrs, arg_r = flag_util.ParseCmdVal('unset', cmd_val)
613 arg = arg_types.unset(attrs.attrs)
614
615 argv, arg_locs = arg_r.Rest2()
616 for i, name in enumerate(argv):
617 location = arg_locs[i]
618
619 if arg.f:
620 self.procs.EraseShellFunc(name)
621
622 elif arg.v:
623 if not self._UnsetVar(name, location, False):
624 return 1
625
626 else:
627 # proc_fallback: Try to delete var first, then func.
628 if not self._UnsetVar(name, location, True):
629 return 1
630
631 return 0
632
633
634class Shift(vm._Builtin):
635
636 def __init__(self, mem):
637 # type: (state.Mem) -> None
638 self.mem = mem
639
640 def Run(self, cmd_val):
641 # type: (cmd_value.Argv) -> int
642 num_args = len(cmd_val.argv) - 1
643 if num_args == 0:
644 n = 1
645 elif num_args == 1:
646 arg = cmd_val.argv[1]
647 try:
648 n = int(arg)
649 except ValueError:
650 e_usage("Invalid shift argument %r" % arg, loc.Missing)
651 else:
652 e_usage('got too many arguments', loc.Missing)
653
654 return self.mem.Shift(n)