Skip to content

Instantly share code, notes, and snippets.

@dimo414
Last active November 28, 2022 07:25
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dimo414/2fb052d230654cc0c25e9e41a9651ebe to your computer and use it in GitHub Desktop.
Save dimo414/2fb052d230654cc0c25e9e41a9651ebe to your computer and use it in GitHub Desktop.
Bash array expansion patterns for use with -u
#!/bin/bash
echo "Bash Version: $BASH_VERSION"
pass_fail() {
#printf '%s:%s' "$1" "$2"; return # diagnostic
if (( $1 == $2 )); then
printf '\e[32m%s\e[0m' "✓"
else
printf '\e[31m%s\e[0m' "✗"
fi
}
test_expansion() (
readonly arr=("$@")
set -u
printf '$#:%s' "$#"
copy=( "${@:+"$@"}" ); printf '\t%s' "$(pass_fail "${#copy[@]}" "$#")"
copy=( "${@:+$@}" ); printf '\t%s' "$(pass_fail "${#copy[@]}" "$#")"
copy=( "${@+"$@"}" ); printf '\t%s' "$(pass_fail "${#copy[@]}" "$#")"
copy=( "${@+$@}" ); printf '\t%s' "$(pass_fail "${#copy[@]}" "$#")"
copy=( ${@:+"$@"} ); printf '\t%s' "$(pass_fail "${#copy[@]}" "$#")"
copy=( ${@:+$@} ); printf '\t%s' "$(pass_fail "${#copy[@]}" "$#")"
copy=( ${@+"$@"} ); printf '\t%s' "$(pass_fail "${#copy[@]}" "$#")"
copy=( ${@+$@} ); printf '\t%s' "$(pass_fail "${#copy[@]}" "$#")"
copy=( "${@:1}" ); printf '\t%s' "$(pass_fail "${#copy[@]}" "$#")"
copy=( ${@:1} ); printf '\t%s' "$(pass_fail "${#copy[@]}" "$#")"
printf '\n'
printf '#arr:%s' "${#arr[@]}"
copy=( "${arr[@]:+"${arr[@]}"}" ); printf '\t%s' "$(pass_fail "${#copy[@]}" "${#arr[@]}")"
copy=( "${arr[@]:+${arr[@]}}" ); printf '\t%s' "$(pass_fail "${#copy[@]}" "${#arr[@]}")"
copy=( "${arr[@]+"${arr[@]}"}" ); printf '\t%s' "$(pass_fail "${#copy[@]}" "${#arr[@]}")"
copy=( "${arr[@]+${arr[@]}}" ); printf '\t%s' "$(pass_fail "${#copy[@]}" "${#arr[@]}")"
copy=( ${arr[@]:+"${arr[@]}"} ); printf '\t%s' "$(pass_fail "${#copy[@]}" "${#arr[@]}")"
copy=( ${arr[@]:+${arr[@]}} ); printf '\t%s' "$(pass_fail "${#copy[@]}" "${#arr[@]}")"
copy=( ${arr[@]+"${arr[@]}"} ); printf '\t%s' "$(pass_fail "${#copy[@]}" "${#arr[@]}")"
copy=( ${arr[@]+${arr[@]}} ); printf '\t%s' "$(pass_fail "${#copy[@]}" "${#arr[@]}")"
( copy=( "${arr[@]:0}" ); printf '\t%s' "$(pass_fail "${#copy[@]}" "${#arr[@]}")" ) 2>/dev/null || printf '\t\e[1;33m%s\e[0m' "!"
( copy=( ${arr[@]:0} ); printf '\t%s' "$(pass_fail "${#copy[@]}" "${#arr[@]}")" ) 2>/dev/null || printf '\t\e[1;33m%s\e[0m' "!"
printf '\n'
)
printf '\t%s' '":+"' '":+' '"+"' '"+' ':+"' ':+' '+"' '+' '":0/1' ':0/1'
printf '\n'
test_expansion
test_expansion ''
test_expansion '' ''
test_expansion a 'b c'
@dimo414
Copy link
Author

dimo414 commented May 1, 2020

If the eval-magic feels a bit too handwavy for you, an earlier version of the script doesn't use it.

@quinncomendant
Copy link

Don't worry, ${arr[@]+"${arr[@]}"} will not have word-splitting applied, even though it is not surrounded by " … " (as you would normally do with "${arr[@]}", and as is done with the inferior "${arr[@]+"${arr[@]}"}").

Here's a test:

arr=(a 'b c');
for a in ${arr[@]+"${arr[@]}"}; do
    echo ">$a<";
done

The output is:

>a<
>b c<

@dimo414
Copy link
Author

dimo414 commented Apr 22, 2022

@quinncomendant yes, that's exactly what this post is demonstrating. But ${arr[@]+"${arr[@]}"} is quoted, which is why word splitting doesn't occur. "${arr[@]+"${arr[@]}"}" ("+" in the table above) does not work correctly in some versions of bash, namely 4.2 and 4.3.

@andrewgdotcom
Copy link

@dimo414 Great stuff! But isn't the reference to ${@+"$@"} excessive? I've modified your script with '"$@"' included as a test case and it reports no issue on any of the bash versions listed. When would the longer form be required?

@dimo414
Copy link
Author

dimo414 commented Jun 29, 2022

@andrewgdotcom can you share a more complete example of what you tried? As written, '"$@"' with enclosing single quotes would be a single string of the literal characters "$@" rather than an array expansion.

@andrewgdotcom
Copy link

@dimo414 I updated your script to read:

#!/bin/bash

echo "Bash Version: $BASH_VERSION"

expansion() {
  local incantation=$1 expected=$2 actual
  shift 2
  actual=$(
    set -u
    eval "$(printf 'copy=( %s ); printf "${#copy[@]}"' "$incantation")" 2>/dev/null
  ) || { printf '\t\e[1;33m%s\e[0m' "!"; return 0; }
  if (( actual == expected )); then
    printf '\t\e[32m%s\e[0m' "✓"
  else
    printf '\t\e[31m%s\e[0m' "✗"
  fi
}

test_expansion() {
  local arr=("$@")

  printf '$#:%s' "$#"
  for incantation in \
    '$@' '"$@"' \
    '"${@:+"$@"}"' '"${@:+$@}"' '"${@+"$@"}"' '"${@+$@}"' '${@:+"$@"}' '${@:+$@}' \
    '${@+"$@"}' '${@+$@}' '"${@:1}"' '${@:1}'
  do
    expansion "$incantation" "$#" "$@"
  done
  printf '\n'

  printf '#arr:%s' "${#arr[@]}"
  for incantation in \
    '${arr[@]}' '"${arr[@]}"' \
    '"${arr[@]:+"${arr[@]}"}"' '"${arr[@]:+${arr[@]}}"' '"${arr[@]+"${arr[@]}"}"' \
    '"${arr[@]+${arr[@]}}"' '${arr[@]:+"${arr[@]}"}' '${arr[@]:+${arr[@]}}' \
    '${arr[@]+"${arr[@]}"}' '${arr[@]+${arr[@]}}' '"${arr[@]:0}"' '${arr[@]:0}'
  do
    expansion "$incantation" "$#"
  done
  printf '\n'
}

printf '\t%s' '.' '"' '":+"' '":+' '"+"' '"+' ':+"' ':+' '+"' '+' '":0/1' ':0/1'
printf '\n'
test_expansion
test_expansion ''
test_expansion '' ''
test_expansion a 'b c'

and the output was:

Bash Version: 3.1.23(1)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
#arr:0	!	!	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
#arr:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 3.2.57(1)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
#arr:0	!	!	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
#arr:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 4.0.44(1)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
#arr:0	!	!	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
#arr:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 4.1.17(2)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
#arr:0	!	!	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
#arr:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 4.2.53(2)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
#arr:0	!	!	✓	✓	✓	✓	✓	✓	✓	✓	!	!
$#:1	✗	✓	✗	✗	✗	✗	✗	✗	✓	✗	✓	✗
#arr:1	✗	✓	✗	✗	✗	✗	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 4.3.48(1)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
#arr:0	!	!	✓	✓	✓	✓	✓	✓	✓	✓	!	!
$#:1	✗	✓	✗	✗	✗	✗	✗	✗	✓	✗	✓	✗
#arr:1	✗	✓	✗	✗	✗	✗	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 4.4.23(1)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
#arr:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✗	✗	✓	✓	✗	✗	✓	✗	✓	✗
#arr:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 5.0.18(1)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
#arr:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✗	✗	✓	✓	✗	✗	✓	✗	✓	✗
#arr:1	✗	✓	✗	✗	✓	✓	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
#arr:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗

Second column, odd lines represent "$@" - no issues reported?

@andrewgdotcom
Copy link

It's easier to see if you comment out the #arr tests entirely, to leave only the $# ones:

Bash Version: 3.1.23(1)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 3.2.57(1)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 4.0.44(1)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 4.1.17(2)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✓	✓	✓	✓	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 4.2.53(2)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✗	✗	✗	✗	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 4.3.48(1)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✗	✗	✗	✗	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 4.4.23(1)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✗	✗	✓	✓	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
Bash Version: 5.0.18(1)-release
	.	"	":+"	":+	"+"	"+	:+"	:+	+"	+	":0/1	:0/1
$#:0	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓	✓
$#:1	✗	✓	✗	✗	✓	✓	✗	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗
$#:2	✗	✓	✓	✓	✓	✓	✓	✗	✓	✗	✓	✗

@dimo414
Copy link
Author

dimo414 commented Jun 30, 2022

Ok yes I follow you now. You're right that $@ is not affected by the same set -u issue as other arrays, I believe I just included it in the table for completeness - as you've found $@ expansion sometimes behaves differently than ${arr[@]} expansion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment