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

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