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

571 lines, 389 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 bash_impl
15from core import error
16from core.error import e_usage
17from core import state
18from core import vm
19from display import ui
20from frontend import flag_util
21from frontend import args
22from mycpp.mylib import log, NewDict
23from osh import cmd_eval
24from osh import sh_expr_eval
25from data_lang import j8_lite
26
27from typing import cast, Dict, List, Optional, TYPE_CHECKING
28if TYPE_CHECKING:
29 from core.state import Mem
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(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() not in (value_e.BashArray,
136 value_e.SparseArray):
137 continue
138 if flag_A and val.tag() != value_e.BashAssoc:
139 continue
140
141 decl = [] # type: List[str]
142 if print_flags:
143 flags = [] # type: List[str]
144 if cell.nameref:
145 flags.append('n')
146 if cell.readonly:
147 flags.append('r')
148 if cell.exported:
149 flags.append('x')
150 if val.tag() in (value_e.BashArray, value_e.SparseArray):
151 flags.append('a')
152 elif val.tag() == value_e.BashAssoc:
153 flags.append('A')
154 if len(flags) == 0:
155 flags.append('-')
156
157 decl.extend(["declare -", ''.join(flags), " ", name])
158 else:
159 decl.append(name)
160
161 if val.tag() == value_e.Str:
162 str_val = cast(value.Str, val)
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 decl.extend(
168 ["=",
169 bash_impl.BashArray_ToStrForShellPrint(array_val, name)])
170
171 elif val.tag() == value_e.BashAssoc:
172 assoc_val = cast(value.BashAssoc, val)
173 decl.extend(
174 ["=", bash_impl.BashAssoc_ToStrForShellPrint(assoc_val)])
175
176 elif val.tag() == value_e.SparseArray:
177 sparse_val = cast(value.SparseArray, val)
178 decl.extend(
179 ["=",
180 bash_impl.SparseArray_ToStrForShellPrint(sparse_val)])
181
182 else:
183 pass # note: other types silently ignored
184
185 print(''.join(decl))
186 count += 1
187
188 if print_all or count == len(names):
189 return 0
190 else:
191 return 1
192
193
194def _ExportReadonly(mem, pair, flags):
195 # type: (Mem, AssignArg, int) -> None
196 """For 'export' and 'readonly' to respect += and flags.
197
198 Like 'setvar' (scope_e.LocalOnly), unless dynamic scope is on. That is, it
199 respects shopt --unset dynamic_scope.
200
201 Used for assignment builtins, (( a = b )), {fd}>out, ${x=}, etc.
202 """
203 which_scopes = mem.ScopesForWriting()
204
205 lval = LeftName(pair.var_name, pair.blame_word)
206 if pair.plus_eq:
207 old_val = sh_expr_eval.OldValue(lval, mem, None) # ignore set -u
208 # When 'export e+=', then rval is value.Str('')
209 # When 'export foo', the pair.plus_eq flag is false.
210 assert pair.rval is not None
211 val = cmd_eval.PlusEquals(old_val, pair.rval)
212 else:
213 # NOTE: when rval is None, only flags are changed
214 val = pair.rval
215
216 mem.SetNamed(lval, val, which_scopes, flags=flags)
217
218
219class Export(vm._AssignBuiltin):
220
221 def __init__(self, mem, errfmt):
222 # type: (Mem, ui.ErrorFormatter) -> None
223 self.mem = mem
224 self.errfmt = errfmt
225
226 def Run(self, cmd_val):
227 # type: (cmd_value.Assign) -> int
228 if self.mem.exec_opts.no_exported():
229 self.errfmt.Print_(
230 "YSH doesn't have 'export'. Hint: setglobal ENV.FOO = 'bar'",
231 cmd_val.arg_locs[0])
232 return 1
233
234 arg_r = args.Reader(cmd_val.argv, locs=cmd_val.arg_locs)
235 arg_r.Next()
236 attrs = flag_util.Parse('export_', arg_r)
237 arg = arg_types.export_(attrs.attrs)
238 #arg = attrs
239
240 if arg.f:
241 e_usage(
242 "doesn't accept -f because it's dangerous. "
243 "(The code can usually be restructured with 'source')",
244 loc.Missing)
245
246 if arg.p or len(cmd_val.pairs) == 0:
247 return _PrintVariables(self.mem,
248 cmd_val,
249 attrs,
250 True,
251 builtin=_EXPORT)
252
253 if arg.n:
254 for pair in cmd_val.pairs:
255 if pair.rval is not None:
256 e_usage("doesn't accept RHS with -n",
257 loc.Word(pair.blame_word))
258
259 # NOTE: we don't care if it wasn't found, like bash.
260 self.mem.ClearFlag(pair.var_name, state.ClearExport)
261 else:
262 for pair in cmd_val.pairs:
263 _ExportReadonly(self.mem, pair, state.SetExport)
264
265 return 0
266
267
268def _ReconcileTypes(rval, flag_a, flag_A, blame_word):
269 # type: (Optional[value_t], bool, bool, word_t) -> value_t
270 """Check that -a and -A flags are consistent with RHS.
271
272 Special case: () is allowed to mean empty indexed array or empty assoc array
273 if the context is clear.
274
275 Shared between NewVar and Readonly.
276 """
277 if flag_a and rval is not None and rval.tag() != value_e.BashArray:
278 e_usage("Got -a but RHS isn't an array", loc.Word(blame_word))
279
280 if flag_A and rval:
281 # Special case: declare -A A=() is OK. The () is changed to mean an empty
282 # associative array.
283 if rval.tag() == value_e.BashArray:
284 array_val = cast(value.BashArray, rval)
285 if len(array_val.strs) == 0:
286 # mycpp limitation: NewDict() needs to be typed
287 tmp = NewDict() # type: Dict[str, str]
288 return value.BashAssoc(tmp)
289 #return value.BashArray([])
290
291 if rval.tag() != value_e.BashAssoc:
292 e_usage("Got -A but RHS isn't an associative array",
293 loc.Word(blame_word))
294
295 return rval
296
297
298class Readonly(vm._AssignBuiltin):
299
300 def __init__(self, mem, errfmt):
301 # type: (Mem, ui.ErrorFormatter) -> None
302 self.mem = mem
303 self.errfmt = errfmt
304
305 def Run(self, cmd_val):
306 # type: (cmd_value.Assign) -> int
307 arg_r = args.Reader(cmd_val.argv, locs=cmd_val.arg_locs)
308 arg_r.Next()
309 attrs = flag_util.Parse('readonly', arg_r)
310 arg = arg_types.readonly(attrs.attrs)
311
312 if arg.p or len(cmd_val.pairs) == 0:
313 return _PrintVariables(self.mem,
314 cmd_val,
315 attrs,
316 True,
317 builtin=_READONLY)
318
319 for pair in cmd_val.pairs:
320 if pair.rval is None:
321 if arg.a:
322 rval = value.BashArray([]) # type: value_t
323 elif arg.A:
324 # mycpp limitation: NewDict() needs to be typed
325 tmp = NewDict() # type: Dict[str, str]
326 rval = value.BashAssoc(tmp)
327 else:
328 rval = None
329 else:
330 rval = pair.rval
331
332 rval = _ReconcileTypes(rval, arg.a, arg.A, pair.blame_word)
333
334 # NOTE:
335 # - when rval is None, only flags are changed
336 # - dynamic scope because flags on locals can be changed, etc.
337 _ExportReadonly(self.mem, pair, state.SetReadOnly)
338
339 return 0
340
341
342class NewVar(vm._AssignBuiltin):
343 """declare/typeset/local."""
344
345 def __init__(self, mem, procs, exec_opts, errfmt):
346 # type: (Mem, state.Procs, optview.Exec, ui.ErrorFormatter) -> None
347 self.mem = mem
348 self.procs = procs
349 self.exec_opts = exec_opts
350 self.errfmt = errfmt
351
352 def _PrintFuncs(self, names):
353 # type: (List[str]) -> int
354 status = 0
355 for name in names:
356 proc_val = self.procs.GetShellFunc(name)
357 if proc_val:
358 if self.exec_opts.extdebug():
359 tok = proc_val.name_tok
360 assert tok is not None, tok
361 assert tok.line is not None, tok.line
362 filename_str = ui.GetFilenameString(tok.line)
363 # Note: the filename could have a newline, and this won't
364 # be a single line. But meh, this is a bash feature.
365 line = '%s %d %s' % (name, tok.line.line_num, filename_str)
366 print(line)
367 else:
368 print(name)
369 # TODO: Could print LST for -f, or render LST. Bash does this. 'trap'
370 # could use that too.
371 else:
372 status = 1
373 return status
374
375 def Run(self, cmd_val):
376 # type: (cmd_value.Assign) -> int
377 arg_r = args.Reader(cmd_val.argv, locs=cmd_val.arg_locs)
378 arg_r.Next()
379 attrs = flag_util.Parse('new_var', arg_r)
380 arg = arg_types.new_var(attrs.attrs)
381
382 status = 0
383
384 if arg.f:
385 names = arg_r.Rest()
386 if len(names):
387 # This is only used for a STATUS QUERY now. We only show the name,
388 # not the body.
389 status = self._PrintFuncs(names)
390 else:
391 # Disallow this since it would be incompatible.
392 e_usage('with -f expects function names', loc.Missing)
393 return status
394
395 if arg.F:
396 names = arg_r.Rest()
397 if len(names):
398 status = self._PrintFuncs(names)
399 else:
400 # bash quirk: with no names, they're printed in a different format!
401 for func_name in self.procs.ShellFuncNames():
402 print('declare -f %s' % (func_name))
403 return status
404
405 if arg.p: # Lookup and print variables.
406 return _PrintVariables(self.mem, cmd_val, attrs, True)
407 elif len(cmd_val.pairs) == 0:
408 return _PrintVariables(self.mem, cmd_val, attrs, False)
409
410 if not self.exec_opts.ignore_flags_not_impl():
411 if arg.i:
412 e_usage(
413 "doesn't implement flag -i (shopt --set ignore_flags_not_impl)",
414 loc.Missing)
415
416 if arg.l or arg.u:
417 # Just print a warning! The program may still run.
418 self.errfmt.Print_(
419 "Warning: OSH doesn't implement flags -l or -u (shopt --set ignore_flags_not_impl)",
420 loc.Missing)
421
422 #
423 # Set variables
424 #
425
426 if cmd_val.builtin_id == builtin_i.local:
427 which_scopes = scope_e.LocalOnly
428 else: # declare/typeset
429 if arg.g:
430 which_scopes = scope_e.GlobalOnly
431 else:
432 which_scopes = scope_e.LocalOnly
433
434 flags = 0
435 if arg.x == '-':
436 flags |= state.SetExport
437 if arg.r == '-':
438 flags |= state.SetReadOnly
439 if arg.n == '-':
440 flags |= state.SetNameref
441
442 if arg.x == '+':
443 flags |= state.ClearExport
444 if arg.r == '+':
445 flags |= state.ClearReadOnly
446 if arg.n == '+':
447 flags |= state.ClearNameref
448
449 for pair in cmd_val.pairs:
450 rval = pair.rval
451 # declare -a foo=(a b); declare -a foo; should not reset to empty array
452 if rval is None and (arg.a or arg.A):
453 old_val = self.mem.GetValue(pair.var_name)
454 if arg.a:
455 if old_val.tag() not in (value_e.BashArray,
456 value_e.SparseArray):
457 rval = value.BashArray([])
458 elif arg.A:
459 if old_val.tag() != value_e.BashAssoc:
460 # mycpp limitation: NewDict() needs to be typed
461 tmp = NewDict() # type: Dict[str, str]
462 rval = value.BashAssoc(tmp)
463
464 lval = LeftName(pair.var_name, pair.blame_word)
465
466 if pair.plus_eq:
467 old_val = sh_expr_eval.OldValue(lval, self.mem,
468 None) # ignore set -u
469 # When 'typeset e+=', then rval is value.Str('')
470 # When 'typeset foo', the pair.plus_eq flag is false.
471 assert pair.rval is not None
472 rval = cmd_eval.PlusEquals(old_val, pair.rval)
473 else:
474 rval = _ReconcileTypes(rval, arg.a, arg.A, pair.blame_word)
475
476 self.mem.SetNamed(lval, rval, which_scopes, flags=flags)
477
478 return status
479
480
481# TODO:
482# - It would make more sense to treat no args as an error (bash doesn't.)
483# - Should we have strict builtins? Or just make it stricter?
484# - Typed args: unset (mylist[0]) is like Python's del
485# - It has the same word as 'setvar', which makes sense
486
487
488class Unset(vm._Builtin):
489
490 def __init__(
491 self,
492 mem, # type: state.Mem
493 procs, # type: state.Procs
494 unsafe_arith, # type: sh_expr_eval.UnsafeArith
495 errfmt, # type: ui.ErrorFormatter
496 ):
497 # type: (...) -> None
498 self.mem = mem
499 self.procs = procs
500 self.unsafe_arith = unsafe_arith
501 self.errfmt = errfmt
502
503 def _UnsetVar(self, arg, location, proc_fallback):
504 # type: (str, loc_t, bool) -> bool
505 """
506 Returns:
507 bool: whether the 'unset' builtin should succeed with code 0.
508 """
509 lval = self.unsafe_arith.ParseLValue(arg, location)
510
511 #log('unsafe lval %s', lval)
512 found = False
513 try:
514 found = self.mem.Unset(lval, scope_e.Shopt)
515 except error.Runtime as e:
516 # note: in bash, myreadonly=X fails, but declare myreadonly=X doesn't
517 # fail because it's a builtin. So I guess the same is true of 'unset'.
518 msg = e.UserErrorString()
519 self.errfmt.Print_(msg, blame_loc=location)
520 return False
521
522 if proc_fallback and not found:
523 self.procs.EraseShellFunc(arg)
524
525 return True
526
527 def Run(self, cmd_val):
528 # type: (cmd_value.Argv) -> int
529 attrs, arg_r = flag_util.ParseCmdVal('unset', cmd_val)
530 arg = arg_types.unset(attrs.attrs)
531
532 argv, arg_locs = arg_r.Rest2()
533 for i, name in enumerate(argv):
534 location = arg_locs[i]
535
536 if arg.f:
537 self.procs.EraseShellFunc(name)
538
539 elif arg.v:
540 if not self._UnsetVar(name, location, False):
541 return 1
542
543 else:
544 # proc_fallback: Try to delete var first, then func.
545 if not self._UnsetVar(name, location, True):
546 return 1
547
548 return 0
549
550
551class Shift(vm._Builtin):
552
553 def __init__(self, mem):
554 # type: (Mem) -> None
555 self.mem = mem
556
557 def Run(self, cmd_val):
558 # type: (cmd_value.Argv) -> int
559 num_args = len(cmd_val.argv) - 1
560 if num_args == 0:
561 n = 1
562 elif num_args == 1:
563 arg = cmd_val.argv[1]
564 try:
565 n = int(arg)
566 except ValueError:
567 e_usage("Invalid shift argument %r" % arg, loc.Missing)
568 else:
569 e_usage('got too many arguments', loc.Missing)
570
571 return self.mem.Shift(n)