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 = list(reversed(self.func_name_stack)) # TODO: Reuse this object too? return runtime.StrArray(strs) if name == 'LINENO': return self.line_num # Instead of BASH_SOURCE. Using Oil _ convnetion. if name == 'SOURCE_NAME': return self.source_name cell, _ = self._FindCellAndNamespace(name, lookup_mode, is_read=True) if cell: return cell.val return runtime.Undef()
def testGetVar(self): mem = _InitMem() # readonly a=x mem.SetVar(runtime.LhsName('a'), runtime.Str('x'), (var_flags_e.ReadOnly, ), scope_e.Dynamic) val = mem.GetVar('a', scope_e.Dynamic) test_lib.AssertAsdlEqual(self, runtime.Str('x'), val) val = mem.GetVar('undef', scope_e.Dynamic) test_lib.AssertAsdlEqual(self, runtime.Undef(), val)
def GetSpecialVar(self, op_id): if op_id == Id.VSub_Bang: # $! n = self.last_job_id if n == -1: return runtime.Undef() # could be an error elif op_id == Id.VSub_QMark: # $? # TODO: Have to parse status somewhere. # External commands need WIFEXITED test. What about subshells? n = self.last_status 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 runtime.Str(str(n))
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 = runtime.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 = runtime.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 = runtime.Undef() else: val = runtime.Str(s) elif val.tag == value_e.AssocArray: key = self.arith_ev.Eval(anode, int_coerce=False) try: val = runtime.Str(val.d[key]) except KeyError: val = runtime.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) # 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: # TODO: Use dependency injection #val = self.prompt._EvalPS1(val) prompt = ui.PROMPT.EvalPrompt(val) val = runtime.Str(prompt) 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} 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, do_fnmatch=True) assert replace_val.tag == value_e.Str, replace_val replace_str = replace_val.s else: replace_str = '' # Either GlobReplacer or ConstStringReplacer replacer = libstr.MakeReplacer(pat_val.s, replace_str, op.spids[0]) if val.tag == value_e.Str: s = replacer.Replace(val.s, op) val = runtime.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 = runtime.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 = libstr.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 = libstr.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 = runtime.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 = runtime.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 SetVar(self, lval, value, 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? 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', value) if lval.spids: span_id = lval.spids[0] line_span = self.arena.GetLineSpan(span_id) line_id = line_span.line_id #line = arena.GetLine(line_id) path, line_num = self.arena.GetDebugInfo(line_id) col = line_span.col #length = line_span.length util.log('--- spid %s: %s, line %d, col %d', span_id, path, line_num + 1, col) # TODO: Need the arena to look it up the line spid and line number. # Maybe this should return one of (cell, scope). existing cell, or the # scope to put it in? # _FindCellOrScope cell, namespace = self._FindCellAndNamespace( lval.name, lookup_mode) if cell: if value is not None: if cell.readonly: # TODO: error context e_die("Can't assign to readonly value %r", lval.name) cell.val = value if var_flags_e.Exported in new_flags: cell.exported = True if var_flags_e.ReadOnly in new_flags: cell.readonly = True else: if value is None: value = runtime.Undef() # export foo, readonly foo cell = runtime.cell(value, var_flags_e.Exported in new_flags, var_flags_e.ReadOnly 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: # a[1]=(1 2 3) if value.tag == value_e.StrArray: e_die("Can't assign array to array member" ) # TODO: error context cell, namespace = self._FindCellAndNamespace( lval.name, lookup_mode) if cell: if cell.val.tag != value_e.StrArray: # s=x # s[1]=y e_die("Can't index non-array") # TODO: error context if cell.readonly: e_die("Can't assign to readonly value") strs = cell.val.strs try: strs[lval.index] = value.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] = value.s else: # When the array doesn't exist yet, it is created filled with None. # Access to the array needs to explicitly filter those sentinel values. # It also wastes memory. But indexed access is fast. # What should be optimized for? Bash uses a linked list. Random access # takes linear time, but iteration skips unset entries automatically. # - Maybe represent as hash table? Then it's not an ASDL type? # representations: # - array_item.Str array_item.Undef # - parallel array: val.strs, val.undefs # - or change ASDL type checking # - ASDL language does not allow: StrArray(string?* strs) # - or add dict to ASDL? Didn't it support obj? # - finding the max index is linear time? # - also you have to sort the indices # # array ops: # a=(1 2) # a[1]=x # a+=(1 2) # ${a[@]} - get all # ${#a[@]} - length # ${!a[@]} - keys # That seems pretty minimal. items = [None] * lval.index items.append(value.s) new_value = runtime.StrArray(items) # arrays can't be exported cell = runtime.cell(new_value, False, var_flags_e.ReadOnly in new_flags) namespace[lval.name] = cell else: raise AssertionError
def GetArgNum(self, arg_num): index = self.num_shifted + arg_num - 1 if index >= len(self.argv): return runtime.Undef() return runtime.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 runtime.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 runtime.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 runtime.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 runtime.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 runtime.Undef()
def SetVar(self, lval, value, new_flags, lookup_mode, strict_array=False): """ 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? 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', value) if lval.spids: span_id = lval.spids[0] line_span = self.arena.GetLineSpan(span_id) line_id = line_span.line_id #line = arena.GetLine(line_id) path, line_num = self.arena.GetDebugInfo(line_id) col = line_span.col #length = line_span.length util.log('--- spid %s: %s, line %d, col %d', span_id, path, line_num + 1, col) # TODO: Need the arena to look it up the line spid and line number. # Maybe this should return one of (cell, scope). existing cell, or the # scope to put it in? # _FindCellOrScope cell, namespace = self._FindCellAndNamespace( lval.name, lookup_mode) if cell: if value is not None: if cell.readonly: # TODO: error context e_die("Can't assign to readonly value %r", lval.name) cell.val = value 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 value is None: # set -o nounset; local foo; echo $foo # It's still undefined! value = runtime.Undef() # export foo, readonly foo cell = runtime.cell(value, 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 value.tag == value_e.StrArray: e_die("Can't assign array to array member", span_id=left_spid) cell, namespace = self._FindCellAndNamespace( lval.name, lookup_mode) if not cell: self._BindNewArrayWithEntry(namespace, lval, value, new_flags) return # bash/mksh have annoying behavior of letting you do LHS assignment to # Undef, which then turns into an array. (Undef means that set -o # nounset fails.) cell_tag = cell.val.tag if (cell_tag == value_e.Str or (cell_tag == value_e.Undef and strict_array)): # 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) if cell_tag == value_e.Undef: if cell.is_assoc_array: self._BindNewAssocArrayWithEntry(namespace, lval, value, new_flags) else: self._BindNewArrayWithEntry(namespace, lval, value, new_flags) return if cell_tag == value_e.StrArray: strs = cell.val.strs try: strs[lval.index] = value.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] = value.s return if cell_tag == value_e.AssocArray: cell.val.d[lval.index] = value.s return else: raise AssertionError(lval.__class__.__name__)
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: val = runtime.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: # Always maybe_decay_array with ${a[*]} or "${a[*]}" val = runtime.StrArray(val.strs) else: raise AssertionError(op_id) # unknown elif part.bracket_op.tag == bracket_op_e.ArrayIndex: anode = part.bracket_op.expr index = self.arith_ev.Eval(anode) if val.tag == value_e.Undef: pass # it will be checked later elif val.tag == value_e.Str: # TODO: Implement this as an extension. Requires unicode support. # Bash treats it as an array. e_die("Can't index string %r with integer", part.token.val) elif val.tag == value_e.StrArray: try: s = val.strs[index] except IndexError: s = None if s is None: val = runtime.Undef() else: val = runtime.Str(s) else: raise AssertionError(part.bracket_op.tag) if part.prefix_op: val = self._EmptyStrOrError(val) # maybe error val = self._ApplyPrefixOp(val, part.prefix_op) # 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.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) 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, do_fnmatch=True) assert replace_val.tag == value_e.Str, replace_val replace_str = replace_val.s else: replace_str = '' pat = pat_val.s if val.tag == value_e.Str: s = libstr.PatSub(val.s, op, pat, replace_str) val = runtime.Str(s) elif val.tag == value_e.StrArray: strs = [] for s in val.strs: if s is not None: strs.append(libstr.PatSub(s, op, pat, replace_str)) val = runtime.StrArray(strs) else: raise AssertionError(val.__class__.__name__) elif op.tag == suffix_op_e.Slice: # NOTE: The beginning can be negative, but Python handles this. Might # want to make it explicit. # TODO: Check out of bounds errors? begin > end? if op.begin: begin = self.arith_ev.Eval(op.begin) else: begin = 0 if op.length: length = self.arith_ev.Eval(op.length) end = begin + length else: length = None end = None # Python supports None as the end if val.tag == value_e.Str: # Slice characters in a string. # TODO: Need to support unicode? Write spec # tests. val = runtime.Str(val.s[begin : end]) 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 = runtime.StrArray(strs) else: raise AssertionError(val.__class__.__name__) # After applying suffixes, process maybe_decay_array here. if maybe_decay_array and val.tag == value_e.StrArray: val = self._DecayArray(val) # 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)