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

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