shellcheck icon indicating copy to clipboard operation
shellcheck copied to clipboard

SC2124: false alarm with single-element array expansion

Open GfEW opened this issue 6 months ago • 2 comments

The Problem

Shellcheck warns with SC2124 even though this never extracts more than one element:

$ shellcheck -- - <<'EOF'
#!/usr/bin/bash
t() (p="${@: -1}"; declare -p p)
EOF

In - line 2:
t() (p="${@: -1}"; declare -p p)
       ^--------^ SC[2124](tel:2124) (warning): Assigning an array to a string! Assign as array, or use * instead of @ to concatenate.

For more information:
https://www.shellcheck.net/wiki/SC2124 -- Assigning an array to a string! A...

I guess that SC2124 either ignores array slicing entirely or disregards it as no remedy to the risks associated with mistreating arrays as plain variables. However, ${@: -1} either fails or gives the last argument, and never an array. If bash allowed syntaxes like ${@[-1]} or ${-1}, the single-element outcome would be obvious, but since it doesn't, ${@: -1} or ${*: -1} is all we have. If you, like me, consistently use the '@' variant unless you expressly wish to concatenate multiple arguments, the '*' form would only introduce confusion - for no technical benefit.

This issue also applies to other single (or zero) element expansions, e. g. ${@:3:1} or ${@: -2:1}.

The Solution

Ideally, when SC2124 encounters any of

  • ${@: -1}
  • ${@: -1: anylength}
  • ${@: $#-1} (*)
  • ${@: $#-1: anylength} (*)
  • ${@: anyoffset: 1}
  • ${array[@]: -1} (**)
  • ${array[@]: -1: anylength} (**)
  • ${array[@]: $#-1} (*) (**)
  • ${array[@]: $#-1: anylength} (*) (**)
  • ${array[@]: anyoffset: 1} (**)

or, more generally,

  • ${@: offset: length} or
  • ${array[@]: offset: length}(**),

where

  • offset is (or unconditionally evaluates to) -1 or $#-1(*), or
  • length is (or unconditionally evaluates to) 1,

it should conclude that never more than one element can be returned, and waive the warning.

(*) Whilst the basic cases that only use explicit numbers are most important to address, the ones involving $#-1 (or any syntax variant thereof, like $# -1, ${#}-1 etc.) would be nice to have. (**) On a side note, single-element expansions of @-subscribed arrays (i. e. ${array[@]: offset: length} etc.) by means of substring expansion could be worth a separate, dedicated info, along the lines of: "Note that to extract a single array element, ${array[offset]} is more straightforward."

GfEW avatar Jun 21 '25 17:06 GfEW

OP's code:

$ p="${@: -1}"

Shellcheck's response:

"SC2124 (warning): Assigning an array to a string! Assign as array, or use * instead of @ to concatenate."

Issue: if creation of an array is desired, then, in bash, it's necessary to use the proper syntax.

This example creates a scalar variable:

$ foo=bar

This example creates an indexed array:

$ foo=( quux )

This example creates an associative array:

$ declare -A baz; baz=( [corge]="grault" )

These are the three types of parameters in bash. In addition, parameters can have "attributes," such as 'readonly' or 'trace'.

The "type" of parameter created, in bash, is determined by the syntax of the assignment statement, and not by the type of the data assigned to the new parameter, as in some other languages (IIGC C#, Go, Rust...).

On Sat, Jun 21, 2025, 10:37 GfEW @.***> wrote:

GfEW created an issue (koalaman/shellcheck#3228) https://github.com/koalaman/shellcheck/issues/3228 The Problem

Shellcheck warns with SC2124 https://github.com/koalaman/shellcheck/wiki/SC2124 even though this never extracts more than one element:

$ shellcheck -- - <<'EOF' #!/usr/bin/bash t() (p="${@: -1}"; declare -p p) EOF

In - line 2: t() (p="${@: -1}"; declare -p p) ^--------^ SC2124 (warning): Assigning an array to a string! Assign as array, or use * instead of @ to concatenate.

For more information:https://www.shellcheck.net/wiki/SC2124 -- Assigning an array to a string! A...

I guess that SC2124 either ignores slicing entirely or disregards it as no remedy to the risks associated with mistreating arrays as plain variables. However, ${@: -1} either fails or gives the last argument, and never an array. If bash allowed syntaxes like @.***} or ${-1}, the single-element outcome would be even clearer, but since it doesn't, ${@: -1} or ${ : -1} is what we have. If you, like me, consistently use the '@' variant unless you expressly want to concatenate some arguments, the '' form would only introduce confusion - for no technical benefit.

This problem also applies to other single (or zero) element extractions like e. g. ${@: -2:1} or ${@: 3:1}. The Solution

Ideally, when SC2124 encounters substring expansion of arrays, it should check if

  • (i) offset is -1, or
  • (ii) length is 1

(provided only exlicit position numbers, or maybe very simple arithmetics like $#-1, are used), and in that case, conclude that never more than one element can be returned, hence no warning shall be raised.

— Reply to this email directly, view it on GitHub https://github.com/koalaman/shellcheck/issues/3228, or unsubscribe https://github.com/notifications/unsubscribe-auth/AUF2F27HGYEU3UBVLZEIMZT3EWJ6PAVCNFSM6AAAAAB72LDPFGVHI2DSMVQWIX3LMV43ASLTON2WKOZTGE3DKMRWGY4TONI . You are receiving this because you are subscribed to this thread.Message ID: @.***>

wileyhy avatar Jul 12 '25 02:07 wileyhy

@wileyhy I appreciate your comprehensive replies (particularly so over there at #3118). However in this case, I don't get your point.

Issue: if creation of an array is desired, then, in bash, it's necessary to use the proper syntax.

At no time was p meant to be an array. Whether it is declared a scalar variable implicitly by assigning it (while originally unset) the scalar value of the last argument, or explicitly, as in

$ shellcheck -- - <<'EOF'
#!/usr/bin/bash
t() (unset -- p; declare -- p=''; p="${@: -1}"; declare -p p)
EOF

-- SC2124 raises the same, moot warning.

Do you mean to say that "${@: -1}" expands to an array, hence types mismatch? According to man bash,

'When the expansion occurs within double quotes, each parameter expands to a separate word. That is, "$@" is equivalent to "$1" "$2" ... '

In my understanding, constraining the expansion to a single array element gives a single scalar value (i. e. "word") just as e. g. "$1" does, with the sole point of difference being the lack of compact parameter syntax (e. g. ${-1} or the like for the last argument) in bash. Therefore, all of

t1() (unset -- foo; foo=bar; declare -p foo); t1
t2() (unset -- foo; foo="${1}"; declare -p foo); t2 bar ar r
t3() (unset -- foo; foo="${@: -1}"; declare -p foo); t3 b ba bar

or even

t4() (unset -- arr foo; arr=("${@}"); foo="${arr[-1]}"; declare -p foo); t4 b ba bar
t5() (unset -- arr foo; arr=("${@}"); foo="${arr[@]: -1}"; declare -p foo); t4 b ba bar

should assign the scalar value (i. e. string) "bar" to the scalar variable foo. I've never encountered an exception to this expected behavior, and SC2124 already treats the t1, t2 and t4 cases correctly by not raising its 'Assigning an array to a string!' warning there.

Am I missing something in the t3 and t5 cases, do they bear any risk of expanding to anything other than a single scalar value? Could you give an example?

I think that if there is no such risk, SC2124 shouldn't warn about it, either.

GfEW avatar Jul 12 '25 13:07 GfEW