コード例 #1
0
ファイル: state.py プロジェクト: nicolashahn/oil
    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()
コード例 #2
0
ファイル: state_test.py プロジェクト: nicolashahn/oil
    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)
コード例 #3
0
ファイル: state.py プロジェクト: nicolashahn/oil
    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))
コード例 #4
0
ファイル: word_eval.py プロジェクト: jedahan/oil
    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)
コード例 #5
0
ファイル: state.py プロジェクト: nicolashahn/oil
    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
コード例 #6
0
ファイル: state.py プロジェクト: nicolashahn/oil
    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]))
コード例 #7
0
ファイル: state.py プロジェクト: jedahan/oil
    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()
コード例 #8
0
ファイル: state.py プロジェクト: jedahan/oil
    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__)
コード例 #9
0
ファイル: word_eval.py プロジェクト: nicolashahn/oil
  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)