Skip to content

Commit

Permalink
expand: improve support for array keys and values
Browse files Browse the repository at this point in the history
That is, add support for "${array[@]}" for associative arrays,
as well as "{!array[@]}" for indexed and associative arrays.

We also add support for positional parameters in indirections,
so that ${!@}, ${!*}, and ${!1} work the same as Bash.

As usual, with plenty of tests.

Fixes #884.
  • Loading branch information
mvdan committed Jul 3, 2022
1 parent 678ce51 commit b37fb54
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 12 deletions.
44 changes: 35 additions & 9 deletions expand/expand.go
Expand Up @@ -661,31 +661,57 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) {
}

// quotedElemFields returns the list of elements resulting from a quoted
// parameter expansion if it was in the form of ${*}, ${@}, ${foo[*], ${foo[@]},
// or ${!foo@}.
// parameter expansion that should be treated especially, like "${foo[@]}".
func (cfg *Config) quotedElemFields(pe *syntax.ParamExp) []string {
if pe == nil || pe.Length || pe.Width {
return nil
}
name := pe.Param.Value
if pe.Excl {
if pe.Names == syntax.NamesPrefixWords {
switch pe.Names {
case syntax.NamesPrefixWords: // "${!prefix@}"
return cfg.namesByPrefix(pe.Param.Value)
case syntax.NamesPrefix: // "${!prefix*}"
return nil
}
switch nodeLit(pe.Index) {
case "@": // "${!name[@]}"
switch vr := cfg.Env.Get(name); vr.Kind {
case Indexed:
keys := make([]string, 0, len(vr.Map))
for key := range vr.List {
keys = append(keys, strconv.Itoa(key))
}
return keys
case Associative:
keys := make([]string, 0, len(vr.Map))
for key := range vr.Map {
keys = append(keys, key)
}
return keys
}
}
return nil
}
name := pe.Param.Value
switch name {
case "*":
case "*": // "${*}"
return []string{cfg.ifsJoin(cfg.Env.Get(name).List)}
case "@":
case "@": // "${@}"
return cfg.Env.Get(name).List
}
switch nodeLit(pe.Index) {
case "@":
if vr := cfg.Env.Get(name); vr.Kind == Indexed {
case "@": // "${name[@]}"
switch vr := cfg.Env.Get(name); vr.Kind {
case Indexed:
return vr.List
case Associative:
elems := make([]string, 0, len(vr.Map))
for _, elem := range vr.Map {
elems = append(elems, elem)
}
return elems
}
case "*":
case "*": // "${name[*]}"
if vr := cfg.Env.Get(name); vr.Kind == Indexed {
return []string{cfg.ifsJoin(vr.List)}
}
Expand Down
8 changes: 5 additions & 3 deletions expand/param.go
Expand Up @@ -160,18 +160,20 @@ func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) {
strs = cfg.namesByPrefix(pe.Param.Value)
case orig.Kind == NameRef:
strs = append(strs, orig.Str)
case vr.Kind == Indexed:
case pe.Index != nil && vr.Kind == Indexed:
for i, e := range vr.List {
if e != "" {
strs = append(strs, strconv.Itoa(i))
}
}
case vr.Kind == Associative:
case pe.Index != nil && vr.Kind == Associative:
for k := range vr.Map {
strs = append(strs, k)
}
case !syntax.ValidName(str):
case vr.Kind == Unset:
return "", fmt.Errorf("invalid indirect expansion")
case str == "":
return "", nil
default:
vr = cfg.Env.Get(str)
strs = append(strs, vr.String())
Expand Down
37 changes: 37 additions & 0 deletions interp/interp_test.go
Expand Up @@ -2310,6 +2310,23 @@ set +o pipefail
`a=(b); echo ${a[-2]}`,
"negative array index\nexit status 1 #JUSTERR",
},
// TODO: also test with gaps in arrays.
{
`a=([0]=' x ' [1]=' y '); for v in "${a[@]}"; do echo "$v"; done`,
" x \n y \n",
},
{
`a=([0]=' x ' [1]=' y '); for v in "${a[*]}"; do echo "$v"; done`,
" x y \n",
},
{
`a=([0]=' x ' [1]=' y '); for v in "${!a[@]}"; do echo "$v"; done`,
"0\n1\n",
},
{
`a=([0]=' x ' [1]=' y '); for v in "${!a[*]}"; do echo "$v"; done`,
"0 1\n",
},

// associative arrays
{
Expand Down Expand Up @@ -2353,6 +2370,22 @@ set +o pipefail
`a=(['x']=b); echo ${a['y']}`,
"\n #IGNORE bash requires -A",
},
{
`declare -A a=(['a 1']=' x ' ['b 2']=' y '); for v in "${a[@]}"; do echo "$v"; done | sort`,
" x \n y \n",
},
{
`declare -A a=(['a 1']=' x ' ['b 2']=' y '); for v in "${a[*]}"; do echo "$v"; done | sort`,
" x y \n",
},
{
`declare -A a=(['a 1']=' x ' ['b 2']=' y '); for v in "${!a[@]}"; do echo "$v"; done | sort`,
"a 1\nb 2\n",
},
{
`declare -A a=(['a 1']=' x ' ['b 2']=' y '); for v in "${!a[*]}"; do echo "$v"; done | sort`,
"a 1 b 2\n",
},

// weird assignments
{"a=b; a=(c d); echo ${a[@]}", "c d\n"},
Expand Down Expand Up @@ -2500,6 +2533,10 @@ set +o pipefail
// "declare -n foo_interp_missing=bar_interp_missing bar_interp_missing=baz; foo_interp_missing=xxx; echo $foo_interp_missing $bar_interp_missing; echo $baz",
// "xxx xxx\nxxx\n",
//},
{
"echo ${!@}-${!*}-${!1}; set -- foo_interp_missing; echo ${!@}-${!*}-${!1}; foo_interp_missing=value; echo ${!@}-${!*}-${!1}",
"--\n--\nvalue-value-value\n",
},

// read-only vars
{"declare -r foo_interp_missing=bar_interp_missing; echo $foo_interp_missing", "bar_interp_missing\n"},
Expand Down

0 comments on commit b37fb54

Please sign in to comment.