def ShortFlag(self, short_name, arg_type=None, help=None): # type: (str, Optional[int], Optional[str]) -> None """ This is very similar to ShortFlag for FlagSpecAndMore, except we have separate arity0 and arity1 dicts. """ assert short_name.startswith('-'), short_name assert len(short_name) == 2, short_name char = short_name[1] if arg_type is None: self.arity0.append(char) else: self.arity1[char] = SetToArg_(char, _FlagType(arg_type), False) typ = _FlagType(arg_type) default_val = _Default(arg_type) if self.typed: self.defaults[char] = default_val else: # TODO: remove when all builtins converted self.defaults[char] = value.Undef() self.fields[char] = typ
def Unset(self, lval, lookup_mode): """ Returns: ok bool, found bool. ok is false if the cell is read-only. found is false if the name is not there. """ if lval.tag == lvalue_e.Named: # unset x cell, namespace = self._FindCellAndNamespace( lval.name, lookup_mode) if cell: found = True if cell.readonly: return False, found namespace[lval.name].val = value.Undef() cell.exported = False return True, found # found else: return True, False elif lval.tag == lvalue_e.Indexed: # unset a[1] raise NotImplementedError else: raise AssertionError
def ShortOption(self, char, help=None): # type: (str, Optional[str]) -> None """Define an option that can be turned off with + and on with -.""" assert len(char) == 1 # 'r' for -r +r self.options[char] = True self.defaults[char] = value.Undef() # '+' or '-'. TODO: Should we make it a bool? self.fields[char] = flag_type.Str()
def PlusFlag(self, char, help=None): # type: (str, Optional[str]) -> None """Define an option that can be turned off with + and on with -. It's actually a ternary value: plus, minus, or unset. """ assert len(char) == 1 # 'r' for -r +r self.plus_flags.append(char) self.defaults[char] = value.Undef() # '+' or '-'. TODO: Should we make it a bool? self.fields[char] = flag_type.Str()
def testGetVar(self): mem = _InitMem() # readonly a=x mem.SetVar(lvalue.Named('a'), value.Str('x'), (var_flags_e.ReadOnly, ), scope_e.Dynamic) val = mem.GetVar('a', scope_e.Dynamic) test_lib.AssertAsdlEqual(self, value.Str('x'), val) val = mem.GetVar('undef', scope_e.Dynamic) test_lib.AssertAsdlEqual(self, value.Undef(), val)
def testGetValue(self): mem = _InitMem() # readonly a=x mem.SetValue(lvalue.Named('a'), value.Str('x'), scope_e.Dynamic, flags=state.SetReadOnly) val = mem.GetValue('a', scope_e.Dynamic) test_lib.AssertAsdlEqual(self, value.Str('x'), val) val = mem.GetValue('undef', scope_e.Dynamic) test_lib.AssertAsdlEqual(self, value.Undef(), val)
def _EvalIndirectArrayExpansion(self, name, index): """Expands ${!ref} when $ref has the form `name[index]`. Args: name, index: arbitrary strings Returns: value, or None if invalid """ if not match.IsValidVarName(name): return None val = self.mem.GetVar(name) if val.tag == value_e.StrArray: if index in ('@', '*'): # TODO: maybe_decay_array return value.StrArray(val.strs) try: index_num = int(index) except ValueError: return None try: return value.Str(val.strs[index_num]) except IndexError: return value.Undef() elif val.tag == value_e.AssocArray: if index in ('@', '*'): raise NotImplementedError try: return value.Str(val.d[index]) except KeyError: return value.Undef() elif val.tag == value_e.Undef: return value.Undef() elif val.tag == value_e.Str: return None else: raise AssertionError
def PyToValue(py_val): # type: (Any) -> value_t if py_val is None: val = value.Undef() # type: value_t elif isinstance(py_val, bool): val = value.Bool(py_val) elif isinstance(py_val, int): val = value.Int(py_val) elif isinstance(py_val, float): val = value.Float() # TODO: ASDL needs float primitive elif isinstance(py_val, str): val = value.Str(py_val) else: raise AssertionError(py_val) return val
def GetSpecialVar(self, op_id): if op_id == Id.VSub_Bang: # $! n = self.last_job_id if n == -1: return value.Undef() # could be an error elif op_id == Id.VSub_QMark: # $? # External commands need WIFEXITED test. What about subshells? n = self.last_status[-1] elif op_id == Id.VSub_Pound: # $# n = self.argv_stack[-1].GetNumArgs() elif op_id == Id.VSub_Dollar: # $$ n = self.root_pid else: raise NotImplementedError(op_id) return value.Str(str(n))
def ShortFlag(self, short_name, arg_type=None, help=None): # type: (str, Optional[int], Optional[str]) -> None """ This is very similar to ShortFlag for FlagSpecAndMore, except we have separate arity0 and arity1 dicts. """ assert short_name.startswith('-'), short_name assert len(short_name) == 2, short_name char = short_name[1] if arg_type is None: self.arity0[char] = True else: self.arity1[char] = args.SetToArg(char, _FlagType(arg_type)) # TODO: callers should pass flag_type if arg_type is None: typ = flag_type.Bool() # type: flag_type_t default = value.Bool(False) # type: value_t elif arg_type == args.Int: typ = flag_type.Int() default = value.Int(-1) elif arg_type == args.Float: typ = flag_type.Float() default = value.Float(0.0) elif arg_type == args.String: typ = flag_type.Str() default = value.Str('') elif isinstance(arg_type, list): typ = flag_type.Enum(arg_type) default = value.Str('') # This isn't valid else: raise AssertionError(arg_type) if self.typed: self.defaults[char] = default else: # TODO: remove when all builtins converted self.defaults[char] = value.Undef() self.fields[char] = typ
def _Default(arg_type, arg_default=None): # type: (Union[None, int, List[str]], Optional[str]) -> value_t # for enum or string # note: not using this for integers yet if arg_default is not None: return value.Str(arg_default) # early return if arg_type is None: default = value.Bool(False) # type: value_t elif arg_type == args.Int: default = value.Int(-1) # positive values aren't allowed now elif arg_type == args.Float: default = value.Float(0.0) elif arg_type == args.String: default = value.Undef() # e.g. read -d '' is NOT the default elif isinstance(arg_type, list): default = value.Str('') # This isn't valid else: raise AssertionError(arg_type) return default
def Eval(self, node): # type: (arith_expr_t) -> value_t """ Args: node: arith_expr_t Returns: None for Undef (e.g. empty cell) TODO: Don't return 0! int for Str List[int] for MaybeStrArray Dict[str, str] for AssocArray (TODO: Should we support this?) NOTE: (( A['x'] = 'x' )) and (( x = A['x'] )) are syntactically valid in bash, but don't do what you'd think. 'x' sometimes a variable name and sometimes a key. """ # OSH semantics: Variable NAMES cannot be formed dynamically; but INTEGERS # can. ${foo:-3}4 is OK. $? will be a compound word too, so we don't have # to handle that as a special case. UP_node = node with tagswitch(node) as case: if case(arith_expr_e.VarRef): # $(( x )) (can be array) node = cast(arith_expr__VarRef, UP_node) tok = node.token return _LookupVar(tok.val, self.mem, self.exec_opts) elif case( arith_expr_e.ArithWord): # $(( $x )) $(( ${x}${y} )), etc. node = cast(arith_expr__ArithWord, UP_node) return self.word_ev.EvalWordToString(node.w) elif case(arith_expr_e.UnaryAssign): # a++ node = cast(arith_expr__UnaryAssign, UP_node) op_id = node.op_id old_int, lval = self._EvalLhsAndLookupArith(node.child) if op_id == Id.Node_PostDPlus: # post-increment new_int = old_int + 1 ret = old_int elif op_id == Id.Node_PostDMinus: # post-decrement new_int = old_int - 1 ret = old_int elif op_id == Id.Arith_DPlus: # pre-increment new_int = old_int + 1 ret = new_int elif op_id == Id.Arith_DMinus: # pre-decrement new_int = old_int - 1 ret = new_int else: raise AssertionError(op_id) #log('old %d new %d ret %d', old_int, new_int, ret) self._Store(lval, new_int) return value.Int(ret) elif case(arith_expr_e.BinaryAssign): # a=1, a+=5, a[1]+=5 node = cast(arith_expr__BinaryAssign, UP_node) op_id = node.op_id if op_id == Id.Arith_Equal: lval = _EvalLhsArith(node.left, self.mem, self) # Disallowing (( a = myarray )) # It has to be an integer rhs_int = self.EvalToInt(node.right) self._Store(lval, rhs_int) return value.Int(rhs_int) old_int, lval = self._EvalLhsAndLookupArith(node.left) rhs = self.EvalToInt(node.right) if op_id == Id.Arith_PlusEqual: new_int = old_int + rhs elif op_id == Id.Arith_MinusEqual: new_int = old_int - rhs elif op_id == Id.Arith_StarEqual: new_int = old_int * rhs elif op_id == Id.Arith_SlashEqual: if rhs == 0: e_die('Divide by zero') # TODO: location new_int = old_int / rhs elif op_id == Id.Arith_PercentEqual: if rhs == 0: e_die('Divide by zero') # TODO: location new_int = old_int % rhs elif op_id == Id.Arith_DGreatEqual: new_int = old_int >> rhs elif op_id == Id.Arith_DLessEqual: new_int = old_int << rhs elif op_id == Id.Arith_AmpEqual: new_int = old_int & rhs elif op_id == Id.Arith_PipeEqual: new_int = old_int | rhs elif op_id == Id.Arith_CaretEqual: new_int = old_int ^ rhs else: raise AssertionError(op_id) # shouldn't get here self._Store(lval, new_int) return value.Int(new_int) elif case(arith_expr_e.Unary): node = cast(arith_expr__Unary, UP_node) op_id = node.op_id i = self.EvalToInt(node.child) if op_id == Id.Node_UnaryPlus: ret = i elif op_id == Id.Node_UnaryMinus: ret = -i elif op_id == Id.Arith_Bang: # logical negation ret = 1 if i == 0 else 0 elif op_id == Id.Arith_Tilde: # bitwise complement ret = ~i else: raise AssertionError(op_id) # shouldn't get here return value.Int(ret) elif case(arith_expr_e.Binary): node = cast(arith_expr__Binary, UP_node) op_id = node.op_id # Short-circuit evaluation for || and &&. if op_id == Id.Arith_DPipe: lhs = self.EvalToInt(node.left) if lhs == 0: rhs = self.EvalToInt(node.right) ret = int(rhs != 0) else: ret = 1 # true return value.Int(ret) if op_id == Id.Arith_DAmp: lhs = self.EvalToInt(node.left) if lhs == 0: ret = 0 # false else: rhs = self.EvalToInt(node.right) ret = int(rhs != 0) return value.Int(ret) if op_id == Id.Arith_LBracket: # NOTE: Similar to bracket_op_e.ArrayIndex in osh/word_eval.py left = self.Eval(node.left) UP_left = left with tagswitch(left) as case: if case(value_e.MaybeStrArray): left = cast(value__MaybeStrArray, UP_left) rhs_int = self.EvalToInt(node.right) try: # could be None because representation is sparse s = left.strs[rhs_int] except IndexError: s = None elif case(value_e.AssocArray): left = cast(value__AssocArray, UP_left) key = self.EvalWordToString(node.right) s = left.d.get(key) else: # TODO: Add error context e_die( 'Expected array or assoc in index expression, got %s', ui.ValType(left)) if s is None: val = value.Undef() # type: value_t else: val = value.Str(s) return val if op_id == Id.Arith_Comma: self.Eval(node.left) # throw away result return self.Eval(node.right) # Rest are integers lhs = self.EvalToInt(node.left) rhs = self.EvalToInt(node.right) if op_id == Id.Arith_Plus: ret = lhs + rhs elif op_id == Id.Arith_Minus: ret = lhs - rhs elif op_id == Id.Arith_Star: ret = lhs * rhs elif op_id == Id.Arith_Slash: if rhs == 0: # TODO: Could also blame / e_die('Divide by zero', span_id=location.SpanForArithExpr(node.right)) ret = lhs / rhs elif op_id == Id.Arith_Percent: if rhs == 0: # TODO: Could also blame / e_die('Divide by zero', span_id=location.SpanForArithExpr(node.right)) ret = lhs % rhs elif op_id == Id.Arith_DStar: # OVM is stripped of certain functions that are somehow necessary for # exponentiation. # Python/ovm_stub_pystrtod.c:21: PyOS_double_to_string: Assertion `0' # failed. if rhs < 0: e_die("Exponent can't be less than zero" ) # TODO: error location ret = 1 for i in xrange(rhs): ret *= lhs elif op_id == Id.Arith_DEqual: ret = int(lhs == rhs) elif op_id == Id.Arith_NEqual: ret = int(lhs != rhs) elif op_id == Id.Arith_Great: ret = int(lhs > rhs) elif op_id == Id.Arith_GreatEqual: ret = int(lhs >= rhs) elif op_id == Id.Arith_Less: ret = int(lhs < rhs) elif op_id == Id.Arith_LessEqual: ret = int(lhs <= rhs) elif op_id == Id.Arith_Pipe: ret = lhs | rhs elif op_id == Id.Arith_Amp: ret = lhs & rhs elif op_id == Id.Arith_Caret: ret = lhs ^ rhs # Note: how to define shift of negative numbers? elif op_id == Id.Arith_DLess: ret = lhs << rhs elif op_id == Id.Arith_DGreat: ret = lhs >> rhs else: raise AssertionError(op_id) return value.Int(ret) elif case(arith_expr_e.TernaryOp): node = cast(arith_expr__TernaryOp, UP_node) cond = self.EvalToInt(node.cond) if cond: # nonzero return self.Eval(node.true_expr) else: return self.Eval(node.false_expr) else: raise AssertionError(node.tag_())
def _EvalBracedVarSub(self, part, part_vals, quoted): """ Args: part_vals: output param to append to. """ # We have four types of operator that interact. # # 1. Bracket: value -> (value, bool maybe_decay_array) # # 2. Then these four cases are mutually exclusive: # # a. Prefix length: value -> value # b. Test: value -> part_value[] # c. Other Suffix: value -> value # d. no operator: you have a value # # That is, we don't have both prefix and suffix operators. # # 3. Process maybe_decay_array here before returning. maybe_decay_array = False # for $*, ${a[*]}, etc. var_name = None # For ${foo=default} # 1. Evaluate from (var_name, var_num, token Id) -> value if part.token.id == Id.VSub_Name: var_name = part.token.val val = self.mem.GetVar(var_name) #log('EVAL NAME %s -> %s', var_name, val) elif part.token.id == Id.VSub_Number: var_num = int(part.token.val) val = self._EvalVarNum(var_num) else: # $* decays val, maybe_decay_array = self._EvalSpecialVar(part.token.id, quoted) # 2. Bracket: value -> (value v, bool maybe_decay_array) # maybe_decay_array is for joining ${a[*]} and unquoted ${a[@]} AFTER # suffix ops are applied. If we take the length with a prefix op, the # distinction is ignored. if part.bracket_op: if part.bracket_op.tag == bracket_op_e.WholeArray: op_id = part.bracket_op.op_id if op_id == Id.Lit_At: if not quoted: maybe_decay_array = True # ${a[@]} decays but "${a[@]}" doesn't if val.tag == value_e.Undef: val = self._EmptyStrArrayOrError(part.token) elif val.tag == value_e.Str: e_die("Can't index string with @: %r", val, part=part) elif val.tag == value_e.StrArray: # TODO: Is this a no-op? Just leave 'val' alone. val = value.StrArray(val.strs) elif op_id == Id.Arith_Star: maybe_decay_array = True # both ${a[*]} and "${a[*]}" decay if val.tag == value_e.Undef: val = self._EmptyStrArrayOrError(part.token) elif val.tag == value_e.Str: e_die("Can't index string with *: %r", val, part=part) elif val.tag == value_e.StrArray: # TODO: Is this a no-op? Just leave 'val' alone. # ${a[*]} or "${a[*]}" : maybe_decay_array is always true val = value.StrArray(val.strs) else: raise AssertionError(op_id) # unknown elif part.bracket_op.tag == bracket_op_e.ArrayIndex: anode = part.bracket_op.expr if val.tag == value_e.Undef: pass # it will be checked later elif val.tag == value_e.Str: # Bash treats any string as an array, so we can't add our own # behavior here without making valid OSH invalid bash. e_die("Can't index string %r with integer", part.token.val, token=part.token) elif val.tag == value_e.StrArray: index = self.arith_ev.Eval(anode) try: # could be None because representation is sparse s = val.strs[index] except IndexError: s = None if s is None: val = value.Undef() else: val = value.Str(s) elif val.tag == value_e.AssocArray: key = self.arith_ev.Eval(anode, int_coerce=False) try: val = value.Str(val.d[key]) except KeyError: val = value.Undef() else: raise AssertionError(val.__class__.__name__) else: raise AssertionError(part.bracket_op.tag) if part.prefix_op: val = self._EmptyStrOrError(val) # maybe error val = self._ApplyPrefixOp(val, part.prefix_op, token=part.token) # NOTE: When applying the length operator, we can't have a test or # suffix afterward. And we don't want to decay the array elif part.suffix_op: op = part.suffix_op if op.tag == suffix_op_e.StringNullary: if op.op_id == Id.VOp0_P: prompt = self.prompt_ev.EvalPrompt(val) val = value.Str(prompt) elif op.op_id == Id.VOp0_Q: val = value.Str(string_ops.ShellQuote(val.s)) else: raise NotImplementedError(op.op_id) elif op.tag == suffix_op_e.StringUnary: if LookupKind(part.suffix_op.op_id) == Kind.VTest: # TODO: Change style to: # if self._ApplyTestOp(...) # return # It should return whether anything was done. If not, we continue to # the end, where we might throw an error. assign_part_vals, effect = self._ApplyTestOp(val, part.suffix_op, quoted, part_vals) # NOTE: Splicing part_values is necessary because of code like # ${undef:-'a b' c 'd # e'}. Each part_value can have a different # do_glob/do_elide setting. if effect == effect_e.SpliceParts: return # EARLY RETURN, part_vals mutated elif effect == effect_e.SpliceAndAssign: if var_name is None: # TODO: error context e_die("Can't assign to special variable") else: # NOTE: This decays arrays too! 'set -o strict_array' could # avoid it. rhs_str = _DecayPartValuesToString(assign_part_vals, self.splitter.GetJoinChar()) state.SetLocalString(self.mem, var_name, rhs_str) return # EARLY RETURN, part_vals mutated elif effect == effect_e.Error: raise NotImplementedError else: # The old one #val = self._EmptyStringPartOrError(part_val, quoted) pass # do nothing, may still be undefined else: val = self._EmptyStrOrError(val) # maybe error # Other suffix: value -> value val = self._ApplyUnarySuffixOp(val, part.suffix_op) elif op.tag == suffix_op_e.PatSub: # PatSub, vectorized val = self._EmptyStrOrError(val) # ${undef//x/y} # globs are supported in the pattern pat_val = self.EvalWordToString(op.pat, do_fnmatch=True) assert pat_val.tag == value_e.Str, pat_val if op.replace: replace_val = self.EvalWordToString(op.replace) assert replace_val.tag == value_e.Str, replace_val replace_str = replace_val.s else: replace_str = '' regex, warnings = glob_.GlobToERE(pat_val.s) if warnings: # TODO: # - Add 'set -o strict-glob' mode and expose warnings. # "Glob is not in CANONICAL FORM". # - Propagate location info back to the 'op.pat' word. pass replacer = string_ops.GlobReplacer(regex, replace_str, op.spids[0]) if val.tag == value_e.Str: s = replacer.Replace(val.s, op) val = value.Str(s) elif val.tag == value_e.StrArray: strs = [] for s in val.strs: if s is not None: strs.append(replacer.Replace(s, op)) val = value.StrArray(strs) else: raise AssertionError(val.__class__.__name__) elif op.tag == suffix_op_e.Slice: val = self._EmptyStrOrError(val) # ${undef:3:1} if op.begin: begin = self.arith_ev.Eval(op.begin) else: begin = 0 if op.length: length = self.arith_ev.Eval(op.length) else: length = None if val.tag == value_e.Str: # Slice UTF-8 characters in a string. s = val.s try: if begin < 0: # It could be negative if we compute unicode length, but that's # confusing. # TODO: Instead of attributing it to the word part, it would be # better if we attributed it to arith_expr begin. raise util.InvalidSlice( "The start index of a string slice can't be negative: %d", begin, part=part) byte_begin = string_ops.AdvanceUtf8Chars(s, begin, 0) if length is None: byte_end = len(s) else: if length < 0: # TODO: Instead of attributing it to the word part, it would be # better if we attributed it to arith_expr begin. raise util.InvalidSlice( "The length of a string slice can't be negative: %d", length, part=part) byte_end = string_ops.AdvanceUtf8Chars(s, length, byte_begin) except (util.InvalidSlice, util.InvalidUtf8) as e: if self.exec_opts.strict_word_eval: raise else: # TODO: # - We don't see the error location here, but we see it when set # -o strict-word-eval. # - Doesn't make the command exit with 1. It just sets the word # to empty string. util.warn(e.UserErrorString()) substr = '' # error condition else: substr = s[byte_begin : byte_end] val = value.Str(substr) elif val.tag == value_e.StrArray: # Slice array entries. # NOTE: unset elements don't count towards the length. strs = [] for s in val.strs[begin:]: if s is not None: strs.append(s) if len(strs) == length: # never true for unspecified length break val = value.StrArray(strs) else: raise AssertionError(val.__class__.__name__) # Not possible # After applying suffixes, process maybe_decay_array here. if maybe_decay_array and val.tag == value_e.StrArray: val = self._DecayArray(val) # For the case where there are no prefix or suffix ops. val = self._EmptyStrOrError(val) # For example, ${a} evaluates to value_t.Str(), but we want a # part_value.StringPartValue. part_val = _ValueToPartValue(val, quoted) part_vals.append(part_val)
def GetVar(self, name, lookup_mode=scope_e.Dynamic): assert isinstance(name, str), name # TODO: Short-circuit down to _FindCellAndNamespace by doing a single hash # lookup: # COMPUTED_VARS = {'PIPESTATUS': 1, 'FUNCNAME': 1, ...} # if name not in COMPUTED_VARS: ... if name == 'PIPESTATUS': return value.StrArray([str(i) for i in self.pipe_status[-1]]) # Do lookup of system globals before looking at user variables. Note: we # could optimize this at compile-time like $?. That would break # ${!varref}, but it's already broken for $?. if name == 'FUNCNAME': # bash wants it in reverse order. This is a little inefficient but we're # not depending on deque(). strs = [] for func_name, source_name, _, _, _ in reversed(self.debug_stack): if func_name: strs.append(func_name) if source_name: strs.append('source') # bash doesn't give name # Temp stacks are ignored if self.has_main: strs.append('main') # bash does this return value.StrArray(strs) # TODO: Reuse this object too? # This isn't the call source, it's the source of the function DEFINITION # (or the sourced # file itself). if name == 'BASH_SOURCE': return value.StrArray(list(reversed(self.bash_source))) # This is how bash source SHOULD be defined, but it's not! if name == 'CALL_SOURCE': strs = [] for func_name, source_name, call_spid, _, _ in reversed(self.debug_stack): # should only happen for the first entry if call_spid == const.NO_INTEGER: continue span = self.arena.GetLineSpan(call_spid) source_str = self.arena.GetLineSourceString(span.line_id) strs.append(source_str) if self.has_main: strs.append('-') # Bash does this to line up with main? return value.StrArray(strs) # TODO: Reuse this object too? if name == 'BASH_LINENO': strs = [] for _, _, call_spid, _, _ in reversed(self.debug_stack): # should only happen for the first entry if call_spid == const.NO_INTEGER: continue span = self.arena.GetLineSpan(call_spid) line_num = self.arena.GetLineNumber(span.line_id) strs.append(str(line_num)) if self.has_main: strs.append('0') # Bash does this to line up with main? return value.StrArray(strs) # TODO: Reuse this object too? if name == 'LINENO': span = self.arena.GetLineSpan(self.current_spid) # TODO: maybe use interned GetLineNumStr? s = str(self.arena.GetLineNumber(span.line_id)) # Perf bug: why is this slow? Commenting it out reduces line count by if 1: self.line_num.s = s # Python's configure takes 75 seconds! else: # WTF this does not show the per bug? self.line_num.s2 = s # Python's configure takes 13 seconds! return self.line_num # This is OSH-specific. Get rid of it in favor of ${BASH_SOURCE[0]} ? if name == 'SOURCE_NAME': # Update and reuse an object. span = self.arena.GetLineSpan(self.current_spid) self.source_name.s = self.arena.GetLineSourceString(span.line_id) return self.source_name cell, _ = self._FindCellAndNamespace(name, lookup_mode, writing=False) if cell: return cell.val return value.Undef()
def SetVar(self, lval, val, new_flags, lookup_mode): """ Args: lval: lvalue val: value, or None if only changing flags new_flags: tuple of flags to set: ReadOnly | Exported () means no flags to start with None means unchanged? scope: Local | Global | Dynamic - for builtins, PWD, etc. NOTE: in bash, PWD=/ changes the directory. But not in dash. """ # STRICTNESS / SANENESS: # # 1) Don't create arrays automatically, e.g. a[1000]=x # 2) Never change types? yeah I think that's a good idea, at least for oil # (not sh, for compatibility). set -o strict-types or something. That # means arrays have to be initialized with let arr = [], which is fine. # This helps with stuff like IFS. It starts off as a string, and assigning # it to a list is en error. I guess you will have to turn this no for # bash? # # TODO: # - COMPUTED_VARS can't be set # - What about PWD / OLDPWD / UID / EUID ? You can simply make them # readonly. # - $PS1 and $PS4 should be PARSED when they are set, to avoid the error on use # - Other validity: $HOME could be checked for existence assert new_flags is not None if lval.tag == lvalue_e.LhsName: #if lval.name == 'ldflags': # TODO: Turn this into a tracing feature. Like osh --tracevar ldflags # --tracevar foo. Has to respect environment variables too. if 0: util.log('--- SETTING ldflags to %s', val) if lval.spids: span_id = lval.spids[0] line_span = self.arena.GetLineSpan(span_id) line_id = line_span.line_id source_str = self.arena.GetLineSourceString(line_id) line_num = self.arena.GetLineNumber(line_id) #length = line_span.length util.log('--- spid %s: %s, line %d, col %d', span_id, source_str, line_num+1, line_span.col) # TODO: Need the arena to look it up the line spid and line number. cell, namespace = self._FindCellAndNamespace(lval.name, lookup_mode) if cell: if val is not None: if cell.readonly: # TODO: error context e_die("Can't assign to readonly value %r", lval.name) cell.val = val if var_flags_e.Exported in new_flags: cell.exported = True if var_flags_e.ReadOnly in new_flags: cell.readonly = True if var_flags_e.AssocArray in new_flags: cell.is_assoc_array = True else: if val is None: # set -o nounset; local foo; echo $foo # It's still undefined! val = value.Undef() # export foo, readonly foo cell = runtime_asdl.cell(val, var_flags_e.Exported in new_flags, var_flags_e.ReadOnly in new_flags, var_flags_e.AssocArray in new_flags) namespace[lval.name] = cell if (cell.val is not None and cell.val.tag == value_e.StrArray and cell.exported): e_die("Can't export array") # TODO: error context elif lval.tag == lvalue_e.LhsIndexedName: # TODO: All paths should have this? We can get here by a[x]=1 or # (( a[ x ] = 1 )). Maybe we should make them different? if lval.spids: left_spid = lval.spids[0] else: left_spid = const.NO_INTEGER # TODO: This is a parse error! # a[1]=(1 2 3) if val.tag == value_e.StrArray: e_die("Can't assign array to array member", span_id=left_spid) # bash/mksh have annoying behavior of letting you do LHS assignment to # Undef, which then turns into an INDEXED array. (Undef means that set # -o nounset fails.) cell, namespace = self._FindCellAndNamespace(lval.name, lookup_mode) if not cell: self._BindNewArrayWithEntry(namespace, lval, val, new_flags) return cell_tag = cell.val.tag if cell_tag == value_e.Str: # s=x # s[1]=y # invalid e_die("Entries in value of type %s can't be assigned to", cell.val.__class__.__name__, span_id=left_spid) if cell.readonly: e_die("Can't assign to readonly value", span_id=left_spid) # This is for the case where we did declare -a foo or declare -A foo. # There IS a cell, but it's still undefined. if cell_tag == value_e.Undef: if cell.is_assoc_array: self._BindNewAssocArrayWithEntry(namespace, lval, val, new_flags) else: self._BindNewArrayWithEntry(namespace, lval, val, new_flags) return if cell_tag == value_e.StrArray: strs = cell.val.strs try: strs[lval.index] = val.s except IndexError: # Fill it in with None. It could look like this: # ['1', 2, 3, None, None, '4', None] # Then ${#a[@]} counts the entries that are not None. # # TODO: strict-array for Oil arrays won't auto-fill. n = lval.index - len(strs) + 1 strs.extend([None] * n) strs[lval.index] = val.s return if cell_tag == value_e.AssocArray: cell.val.d[lval.index] = val.s return else: raise AssertionError(lval.__class__.__name__)
def GetArgNum(self, arg_num): index = self.num_shifted + arg_num - 1 if index >= len(self.argv): return value.Undef() return value.Str(str(self.argv[index]))
def GetVar(self, name, lookup_mode=scope_e.Dynamic): assert isinstance(name, str), name # Do lookup of system globals before looking at user variables. Note: we # could optimize this at compile-time like $?. That would break # ${!varref}, but it's already broken for $?. if name == 'FUNCNAME': # bash wants it in reverse order. This is a little inefficient but we're # not depending on deque(). strs = [] for func_name, source_name, _, _, _ in reversed(self.debug_stack): if func_name: strs.append(func_name) if source_name: strs.append('source') # bash doesn't give name # Temp stacks are ignored if self.has_main: strs.append('main') # bash does this return value.StrArray(strs) # TODO: Reuse this object too? # This isn't the call source, it's the source of the function DEFINITION # (or the sourced # file itself). if name == 'BASH_SOURCE': return value.StrArray(list(reversed(self.bash_source))) # This is how bash source SHOULD be defined, but it's not! if name == 'CALL_SOURCE': strs = [] for func_name, source_name, call_spid, _, _ in reversed( self.debug_stack): # should only happen for the first entry if call_spid == const.NO_INTEGER: continue span = self.arena.GetLineSpan(call_spid) path, _ = self.arena.GetDebugInfo(span.line_id) strs.append(path) if self.has_main: strs.append('-') # Bash does this to line up with main? return value.StrArray(strs) # TODO: Reuse this object too? if name == 'BASH_LINENO': strs = [] for func_name, source_name, call_spid, _, _ in reversed( self.debug_stack): # should only happen for the first entry if call_spid == const.NO_INTEGER: continue span = self.arena.GetLineSpan(call_spid) _, line_num = self.arena.GetDebugInfo(span.line_id) strs.append(str(line_num)) if self.has_main: strs.append('0') # Bash does this to line up with main? return value.StrArray(strs) # TODO: Reuse this object too? if name == 'LINENO': return self.line_num # This is OSH-specific. Get rid of it in favor of ${BASH_SOURCE[0]} ? if name == 'SOURCE_NAME': return self.source_name cell, _ = self._FindCellAndNamespace(name, lookup_mode, writing=False) if cell: return cell.val return value.Undef()
def SetVar(self, lval, val, flags_to_set, lookup_mode, flags_to_clear=(), keyword_id=None): """ Args: lval: lvalue val: value, or None if only changing flags flags_to_set: tuple of flags to set: ReadOnly | Exported () means no flags to start with scope: Local | Global | Dynamic - for builtins, PWD, etc. NOTE: in bash, PWD=/ changes the directory. But not in dash. """ # STRICTNESS / SANENESS: # # 1) Don't create arrays automatically, e.g. a[1000]=x # 2) Never change types? yeah I think that's a good idea, at least for oil # (not sh, for compatibility). set -o strict-types or something. That # means arrays have to be initialized with let arr = [], which is fine. # This helps with stuff like IFS. It starts off as a string, and assigning # it to a list is an error. I guess you will have to turn this no for # bash? # # TODO: # - COMPUTED_VARS can't be set # - What about PWD / OLDPWD / UID / EUID ? You can simply make them # readonly. # - $PS1 and $PS4 should be PARSED when they are set, to avoid the error on use # - Other validity: $HOME could be checked for existence assert flags_to_set is not None if lval.tag == lvalue_e.Named: cell, namespace = self._FindCellAndNamespace( lval.name, lookup_mode) self._CheckOilKeyword(keyword_id, lval, cell) if cell: # Clear before checking readonly bit. # NOTE: Could be cell.flags &= flag_clear_mask if var_flags_e.Exported in flags_to_clear: cell.exported = False if var_flags_e.ReadOnly in flags_to_clear: cell.readonly = False if val is not None: # e.g. declare -rx existing if cell.readonly: # TODO: error context e_die("Can't assign to readonly value %r", lval.name) cell.val = val # NOTE: Could be cell.flags |= flag_set_mask if var_flags_e.Exported in flags_to_set: cell.exported = True if var_flags_e.ReadOnly in flags_to_set: cell.readonly = True else: if val is None: # declare -rx nonexistent # set -o nounset; local foo; echo $foo # It's still undefined! val = value.Undef() # export foo, readonly foo cell = runtime_asdl.cell(val, var_flags_e.Exported in flags_to_set, var_flags_e.ReadOnly in flags_to_set) namespace[lval.name] = cell # Maintain invariant that only strings and undefined cells can be # exported. if (cell.val is not None and cell.val.tag not in (value_e.Undef, value_e.Str) and cell.exported): e_die("Can't export array") # TODO: error context elif lval.tag == lvalue_e.Indexed: # There is no syntax 'declare a[x]' assert val is not None, val # TODO: All paths should have this? We can get here by a[x]=1 or # (( a[ x ] = 1 )). Maybe we should make them different? left_spid = lval.spids[0] if lval.spids else const.NO_INTEGER # bash/mksh have annoying behavior of letting you do LHS assignment to # Undef, which then turns into an INDEXED array. (Undef means that set # -o nounset fails.) cell, namespace = self._FindCellAndNamespace( lval.name, lookup_mode) self._CheckOilKeyword(keyword_id, lval, cell) if not cell: self._BindNewArrayWithEntry(namespace, lval, val, flags_to_set) return if cell.readonly: e_die("Can't assign to readonly array", span_id=left_spid) cell_tag = cell.val.tag # undef[0]=y is allowed if cell_tag == value_e.Undef: self._BindNewArrayWithEntry(namespace, lval, val, flags_to_set) return if cell_tag == value_e.Str: # s=x # s[1]=y # invalid e_die("Entries in value of type %s can't be assigned to", cell.val.__class__.__name__, span_id=left_spid) if cell_tag == value_e.MaybeStrArray: strs = cell.val.strs try: strs[lval.index] = val.s except IndexError: # Fill it in with None. It could look like this: # ['1', 2, 3, None, None, '4', None] # Then ${#a[@]} counts the entries that are not None. # # TODO: strict-array for Oil arrays won't auto-fill. n = lval.index - len(strs) + 1 strs.extend([None] * n) strs[lval.index] = val.s return # AssocArray shouldn't happen because we query IsAssocArray before # evaluating lhs_expr. e_die("Object of this type can't be indexed: %s", cell.val) elif lval.tag == lvalue_e.Keyed: # There is no syntax 'declare A["x"]' assert val is not None, val left_spid = lval.spids[0] if lval.spids else const.NO_INTEGER cell, namespace = self._FindCellAndNamespace( lval.name, lookup_mode) self._CheckOilKeyword(keyword_id, lval, cell) # We already looked it up before making the lvalue assert cell.val.tag == value_e.AssocArray, cell if cell.readonly: e_die("Can't assign to readonly associative array", span_id=left_spid) cell.val.d[lval.key] = val.s else: raise AssertionError(lval.__class__.__name__)