Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[word_eval] Implement 'shopt -s compat_array'. #728

Merged
merged 7 commits into from
Apr 23, 2020
14 changes: 14 additions & 0 deletions osh/sh_expr_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from mycpp.mylib import tagswitch, switch
from osh import bool_stat
from osh import word_
from osh import word_eval

import libc # for fnmatch

Expand Down Expand Up @@ -327,6 +328,12 @@ def _EvalLhsAndLookupArith(self, node):
lval = self.EvalArithLhs(node, runtime.NO_SPID)
val = OldValue(lval, self.mem, self.exec_opts)

# BASH_LINENO, etc.
if val.tag_() in (value_e.MaybeStrArray, value_e.AssocArray) and lval.tag_() == lvalue_e.Named:
named_lval = cast(lvalue__Named, lval)
if word_eval.CheckCompatArray(named_lval.name, self.exec_opts):
val = word_eval.ResolveCompatArray(val)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this part be made obsolete if we made BASH_LINENO etc. immutable?

I guess that would make Oil technically incompatible, but I've been thinking about that. Also it makes sense to tighten up things like PATH=(a b c) and HOME=(a b c) (they shouldn't be arrays)

This doesn't have to be done in this commit; I'm just curious / brainstorming the semantics

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you have already written in another comment, this change also affects the other mutable variables when shopt -s compat_array is set.


# This error message could be better, but we already have one
#if val.tag_() == value_e.MaybeStrArray:
# e_die("Can't use assignment like ++ or += on arrays")
Expand All @@ -347,6 +354,13 @@ def EvalToInt(self, node):
Also used internally.
"""
val = self.Eval(node)

# BASH_LINENO, etc.
if val.tag_() in (value_e.MaybeStrArray, value_e.AssocArray) and node.tag_() == arith_expr_e.VarRef:
tok = cast(Token, node)
if word_eval.CheckCompatArray(tok.val, self.exec_opts):
val = word_eval.ResolveCompatArray(val)

# TODO: Can we avoid the runtime cost of adding location info?
span_id = location.SpanForArithExpr(node)
i = self._ValToIntOrError(val, span_id=span_id)
Expand Down
45 changes: 24 additions & 21 deletions osh/word_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@
# For compatibility, ${BASH_SOURCE} and ${BASH_SOURCE[@]} are both valid.
# ${FUNCNAME} and ${BASH_LINENO} are also the same type of of special variables.
_STRING_AND_ARRAY = ['BASH_SOURCE', 'FUNCNAME', 'BASH_LINENO']
def CheckCompatArray(var_name, opts, is_plain_var_sub=True):
# type: (str, optview.Exec, bool?) -> bool
return opts.compat_array() or (is_plain_var_sub and var_name in _STRING_AND_ARRAY)

def ResolveCompatArray(val):
# type: (value_t) -> value_t
"""Decay ${array} to ${array[0]}."""
if val.tag_() == value_e.MaybeStrArray:
array_val = cast(value__MaybeStrArray, val)
s = array_val.strs[0] if array_val.strs else None
elif val.tag_() == value_e.AssocArray:
assoc_val = cast(value__AssocArray, val)
s = assoc_val.d['0'] if '0' in assoc_val.d else None
else:
raise AssertionError(val.tag_())

if s is None:
return value.Undef()
else:
return value.Str(s)


def EvalSingleQuoted(part):
Expand Down Expand Up @@ -794,23 +814,6 @@ def _DecayArray(self, val):
tmp = [s for s in val.strs if s is not None]
return value.Str(sep.join(tmp))

def _BashArrayCompat(self, val):
# type: (value_t) -> value_t
"""Decay ${array} to ${array[0]}."""
if val.tag_() == value_e.MaybeStrArray:
array_val = cast(value__MaybeStrArray, val)
s = array_val.strs[0] if array_val.strs else None
elif val.tag_() == value_e.AssocArray:
assoc_val = cast(value__AssocArray, val)
s = assoc_val.d['0'] if '0' in assoc_val.d else None
else:
raise AssertionError(val.tag_())

if s is None:
return value.Undef()
else:
return value.Str(s)

def _EmptyStrOrError(self, val, token=None):
# type: (value_t, Optional[Token]) -> value_t
if val.tag_() == value_e.Undef:
Expand Down Expand Up @@ -987,9 +990,9 @@ def _EvalBracedVarSub(self, part, part_vals, quoted):
# ${array@a} is a string
# TODO: An IR for ${} might simplify these lengthy conditions
pass
elif var_name in _STRING_AND_ARRAY:
elif CheckCompatArray(var_name, self.exec_opts, not (part.prefix_op or part.suffix_op)):
# for ${BASH_SOURCE}, etc.
val = self._BashArrayCompat(val)
val = ResolveCompatArray(val)
else:
e_die("Array %r can't be referred to as a scalar (without @ or *)",
var_name, part=part)
Expand Down Expand Up @@ -1216,9 +1219,9 @@ def _EvalSimpleVarSub(self, token, part_vals, quoted):
# TODO: Special case for LINENO
val = self.mem.GetVar(var_name)
if val.tag_() in (value_e.MaybeStrArray, value_e.AssocArray):
if var_name in _STRING_AND_ARRAY:
if CheckCompatArray(var_name, self.exec_opts):
# for $BASH_SOURCE, etc.
val = self._BashArrayCompat(val)
val = ResolveCompatArray(val)
else:
e_die("Array %r can't be referred to as a scalar (without @ or *)",
var_name, token=token)
Expand Down
34 changes: 34 additions & 0 deletions spec/ble-features.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -576,3 +576,37 @@ v=tempenv1 f4_unset global,tempenv1
[global,tempenv1,tempenv2,tempenv3] v: (unset) (unset 3)
[global,tempenv1,tempenv2,tempenv3] v: (unset) (unset 4)
## END

#### [compat_array] ${arr} is ${arr[0]}
case ${SH##*/} in (dash|ash) exit 1;; esac # dash/ash does not have arrays
case ${SH##*/} in (osh) shopt -s compat_array;; esac
case ${SH##*/} in (zsh) setopt KSH_ARRAYS;; esac
arr=(foo bar baz)
echo "$arr"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should also test ${arr} since that's a different code path. (as mentioned I want to collapse those)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the test for ${arr} in another PR #733.

## stdout: foo

## N-I dash/ash status: 1
## N-I dash/ash stdout-json: ""

## OK yash stdout: foo bar baz

#### [compat_array] scalar access to arrays
case ${SH##*/} in
(dash|ash) exit 1;; # dash/ash does not have arrays
(osh) shopt -s compat_array;;
(zsh) setopt KSH_ARRAYS;;
esac

a=(1 0 0)
: $(( a++ ))
argv.py "${a[@]}"
## stdout: ['2', '0', '0']

## N-I dash/ash status: 1
## N-I dash/ash stdout-json: ""

## OK yash STDOUT:
# yash does not support scalar access. Instead, it replaces the array
# with a scalar.
['1']
## END
9 changes: 8 additions & 1 deletion spec/introspect.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ ____
## END


#### ${FUNCNAME} with prefix/suffix operators (OSH regression)
#### ${FUNCNAME} with prefix/suffix operators
shopt -s compat_array

check() {
Expand Down Expand Up @@ -181,6 +181,13 @@ argv.py "$FUNCNAME"
## status: 1
## stdout-json: ""

#### $((BASH_LINENO)) (scalar form in arith)
check() {
echo $((BASH_LINENO))
}
check
## stdout: 4

#### ${BASH_SOURCE[@]} with source and function name
argv.py "${BASH_SOURCE[@]}"
source spec/testdata/bash-source-simple.sh
Expand Down