Skip to content

Instantly share code, notes, and snippets.

@greatBigDot
Last active November 8, 2018 06:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save greatBigDot/a3cd1a9febcf7a39a7e0ed6b6534fdde to your computer and use it in GitHub Desktop.
Save greatBigDot/a3cd1a9febcf7a39a7e0ed6b6534fdde to your computer and use it in GitHub Desktop.
The builtin array variable 'FUNCNAME' is considered unset by many expansions, even when set. Bash bug?
#!/bin/bash
set +eu
# # Uncomment this to see what a normal array would do:
# array=( "${FUNCNAME[@]}" )
# unset FUNCNAME
# FUNCNAME=( "${array[@]}" )
# unset array
# # Or uncomment this for the same result in a more direct fashion:
# unset FUNCNAME
# FUNCNAME=( 'main' )
if [ "$(bash --version | head -1)" != 'GNU bash, version 4.4.23(1)-release (x86_64-unknown-linux-gnu)' ]; then
echo 'NOTE: Tested only with:'
echo '$ bash --version'
echo '=> GNU bash, version 4.4.23(1)-release (x86_64-unknown-linux-gnu)...'
fi
echo "Does the array 'FUNCNAME' exist? Depends on which feature you ask..."
################################################################################
# NOTE: In every example I tried, 'FUNCNAME[@]' and 'FUNCNAME[*]' behaved the #
# same, except for occasional predictable differences in number of arguments #
# it expands to. For conciseness, I decided to only use one in this #
# demonstration; I picked the latter due to it always having one argument #
# (which doesn't require me to use an argument-counting function to make the #
# output look nice). #
# #
# NOTE: Many examples try the same feature on FUNCNAME, FUNCAME[0], and #
# FUNCNAME[*], in that order. In those sections' documentations, that same #
# order is used for the "Expected vs. Consistent vs. Actual" summaries. #
################################################################################
################################################################################
# When you actually try to access the variable directly, bash claims that
# FUNCNAME doesn't exist, but that FUNCNAME[0] does, as well as FUNCNAME[*]. For
# every other variable, whether an associative array, linear array, or neither,
# "${var}" and "${var[0]}" are identical.
#
# This incorrect pattern is mirrored for most expansions not included in later
# examples, including "${var-}", "${var:i:n}", "${var[*]:i:n}",
# "${var/str/rep}", "${var^}", and the siblings of such expansions. Deviations
# from this behavior in later examples are noted by listing what it WOULD output
# if bash were *consistently* wrong about FUNCNAME, in addition to the expected
# and actual results.
#
# Expected: main | main | main
# Actual: | main | main
echo
printf -- '%s:\t\t' '"${FUNCNAME}"'
[ -n "${FUNCNAME}" ] && printf -- 'Yes; %s\n' "${FUNCNAME}" || printf -- 'No.\n'
printf -- '%s:\t' '"${FUNCNAME[0]}"'
[ -n "${FUNCNAME[0]}" ] && printf -- 'Yes; %s\n' "${FUNCNAME[0]}" || printf -- 'No.\n'
printf -- '%s:\t' '"${FUNCNAME[*]}"'
[ -n "${FUNCNAME[*]}" ] && printf -- 'Yes; %s\n' "${FUNCNAME[*]}" || printf -- 'No.\n'
################################################################################
# Says that FUNCNAME is unset, but declared (and as a linear array). Should be
# the definitive, canonical source for this information, but it gives the wrong
# answer. (Note that `declare` doesn't accept individual array elements here.)
#
# Oddly, it differs sharply from the "${var@A}" expansion.
#
# Expected: declare -a FUNCNAME=([0]="main")
# Consistent: <Unclear; either <Expected> or 'bash: declare: FUNCNAME: not found'.>
# Actual: declare -a FUNCNAME
echo
printf -- '%s' "declare -p -- FUNCNAME:"$'\t'
declare -p -- 'FUNCNAME' > /dev/null 2>&1 && printf -- 'Yes; ' || printf -- 'No; '
declare -p -- 'FUNCNAME'
################################################################################
# The `test` command's '-v' option. Note that this cannot distinguish between
# empty and unset arrays (it considers both unset); as such, it is unclear
# exactly what `test` believes here.
#
# The behavior is identical to the usual way bash gets FUNCNAME wrong. Since
# it's different enough from most of the other examples (i.e. not an expansion),
# I figured it's worth including anyway.
#
# Expected: 0 | 0 | 0
# Consistent: 1 | 0 | 0
# Actual: 1 | 0 | 0
echo
printf -- '%s:\t' '[ -v FUNCNAME ]'
[ -v FUNCNAME ] && printf -- 'Yes; %s\n' "$?" || printf 'No; %s\n' "$?"
printf -- '%s:\t' "[ -v 'FUNCNAME[0]' ]"
[ -v 'FUNCNAME[0]' ] && printf -- 'Yes; %s\n' "$?" || printf 'No; %s\n' "$?"
printf -- '%s:\t' "[ -v 'FUNCNAME[*]' ]"
[ -v 'FUNCNAME[*]' ] && printf -- 'Yes; %s\n' "$?" || printf 'No; %s\n' "$?"
################################################################################
# Again consistent with the "usual" wrong behavior: indirection is successful
# under FUNCNAME[0] and FUNCNAME[*] and fails under FUNCNAME.
#
# (Somewhat confusingly, you CAN activate indirection expansion with array
# variables, despite the "!" usually triggering subscript expansion instead.
# Namely, you just need to say "${!var[@]-}" or "${!var[*]-}". A careful reading
# of the manual reveals that the ONLY exceptions to the indirection behavior are
# "${!var[@]}", "${!var[*]}", "${!var*}", "${!var@}", and whenever 'var' is
# already a nameref. As such, adding on the "${var-}" expansion to the
# "${!var[@]}" expansion means that indirection still applies. (This fact led to
# quite some befuddlement on my part.))
#
# Expected: 42 | 42 | 42
# Consistent: | 42 | 42
# Actual: | 42 | 42
echo
printf -- '%s\n' '+ main=42'
main=42
printf -- '%s:\t\t' '"${!FUNCNAME}"'
[ -n "${!FUNCNAME}" ] && printf -- 'Yes; %s\n' "${!FUNCNAME}" || printf -- 'No.\n'
printf -- '%s:\t' '"${!FUNCNAME[0]}"'
[ -n "${!FUNCNAME[0]}" ] && printf -- 'Yes; %s\n' "${!FUNCNAME[0]}" || printf -- 'No.\n'
printf -- '%s:\t' '"${!FUNCNAME[*]-}"'
[ -n "${!FUNCNAME[*]-}" ] && printf -- 'Yes; %s\n' "${!FUNCNAME[*]-}" || printf -- 'No.\n'
printf -- '%s\n' '+ unset main'
unset main
################################################################################
# Weirdly, bash thinks that the array "${FUNCNAME[*]}", which it can output just
# fine, has 0 elements. In fact, if you activate set -u, it errors, meaning that
# it thinks FUNCNAME is not just empty, but unset!
#
# (Technically it just might mean bash thinks it isn't even an array variable.
# If var='hello world', then when `set -u` is in play, "${#var[@]}" spits out an
# error. If you then make var an array variable via `declare -a -- var`, then
# "${#var[@]}" goes back to expanding to '1'. This is the only case I know
# about, (arguably other than the bug documented here, where FUNCNAME[0] isn't
# FUNCNAME), where there is a clear difference between arrays with one element
# and regular variables.)
#
# Expected: 1
# Consistent: 1
# Actual: 0
echo
printf -- '%s:\t' '"${#FUNCNAME[*]}"'
(( "${#FUNCNAME[*]}" > 0 )) && printf -- 'Yes; ' || printf -- 'No; '
printf -- '%s\n' "${#FUNCNAME[*]}"
################################################################################
# Bash doesn't think the subscripts of the array exist (or that the array is
# empty), again despite the fact that it usually can output the entire array or
# the element at that subscript just fine. (Even with `set -u`,
# "${!FUNCNAME[*]}" doesn't distinguish between empty and unset arrays in the
# normal case, so we don't know which bash thinks this is.)
#
# Expected: 0
# Consistent: 0
# Actual:
echo
printf -- '%s:\t' '"${!FUNCNAME[*]}"'
[ -n "${!FUNCNAME[*]}" ] && printf -- 'Yes; %s\n' "${!FUNCNAME[*]}" || printf -- 'No.\n'
################################################################################
# And now, despite bash *usually* thinking that FUNCNAME[0] exists and is
# 'main', it also thinks that its 0 characters long! In fact, it errors under
# `set -u`, so it again thinks FUNCNAME doesn't even exist!
#
# Expected: 4
# Consistent: 4
# Actual: 0
echo
printf -- '%s:\t\t' '"${#FUNCNAME}"'
(( "${#FUNCNAME}" > 0 )) && printf -- 'Yes; ' || printf -- 'No; '
printf -- '%s\n' "${#FUNCNAME}"
# Using `set -u ` errors.
printf -- '%s:\t' '"${#FUNCNAME[0]}"'
(( "${#FUNCNAME[0]}" > 0 )) && printf -- 'Yes; ' || printf -- 'No; '
printf -- '%s\n' "${#FUNCNAME[0]}"
################################################################################
# This time, FUNCNAME[0] is back, but FUNCNAME[*] inexplicably stops existing
# again. FUNCNAME still is nowhere to be found. (For whatever reason, even
# normally, bash doesn't distinguish between unset and null variables under
# `set -u` when expanding "${var@Q}".)
#
# Other "parameter transformations" behave the same way.
#
# Expected: 'main' | 'main' | 'main'
# Consistent: | 'main' | 'main
# Actual: | 'main' |
echo
printf -- '%s:\t' '"${FUNCNAME@Q}"'
[ -n "${FUNCNAME@Q}" ] && printf -- 'Yes; %s\n' "${FUNCNAME@Q}" || printf -- 'No.\n'
printf -- '%s:\t' '"${FUNCNAME[0]@Q}"'
[ -n "${FUNCNAME[0]@Q}" ] && printf -- 'Yes; %s\n' "${FUNCNAME[0]@Q}" || printf -- 'No.\n'
printf -- '%s:\t' '"${FUNCNAME[*]@Q}"'
[ -n "${FUNCNAME[*]@Q}" ] && printf -- 'Yes; %s\n' "${FUNCNAME[*]@Q}" || printf -- 'No.\n'
################################################################################
# Same behavior as above, but worth pointing out anyway. (The "${var@A}"
# expansion is quite similar to `declare -p -- var`, though even in the normal
# case there are important differences.) In particular, notice that bash:
#
# * gives us a *correct* `declare` statement for "${FUNCNAME[0]@A}", despite
# not doing so for `declare -p -- FUNCNAME`, while it...
# * doesn't give us *any* `declare` statement for "${FUNCNAME[*]@A}", despite
# doing so for `declare -p -- FUNCNAME`.
#
# (Note also that "${var[sub]@A}" doesn't give what you probably expect for
# normal array variables. If you have `array=(zero one two)`, then
# "${array[1]@A}" yields "declare -a array='one'". In the case of single-element
# arrays where the subscript is 0, like FUNCNAME, this happens to be the correct
# output.)
#
# Expected: declare -a FUNCNAME='main' | declare -a FUNCNAME='main' | declare -a FUNCNAME=([0]='main')
# Consistent: | declare -a FUNCNAME='main' | declare -a FUNCNAME=([0]='main')
# Actual: | declare -a FUNCNAME='main' |
echo
printf -- '%s:\t' '"${FUNCNAME@A}"'
[ -n "${FUNCNAME@A}" ] && printf -- 'Yes; %s\n' "${FUNCNAME@A}" || printf -- 'No.\n'
printf -- '%s:\t' '"${FUNCNAME[0]@A}"'
[ -n "${FUNCNAME[0]@A}" ] && printf -- 'Yes; %s\n' "${FUNCNAME[0]@A}" || printf -- 'No.\n'
printf -- '%s:\t' '"${FUNCNAME[*]@A}"'
[ -n "${FUNCNAME[*]@A}" ] && printf -- 'Yes; %s\n' "${FUNCNAME[*]@A}" || printf -- 'No.\n'
################################################################################
# Another class of examples deviating from the usual pattern. This time, we have
# the "remove substring from start/end" class of expansion, and again, the
# FUNCNAME[*] form joins the FUNCNAME[0] form in apparent nonexistence. (Note
# that these expansions behave basically how'd you expect for normal arrays: the
# expansion gets applied to each element individually before getting
# connected/expanded as usual.)
#
# Other "removal" expansions behave the same way.
#
# Expected: ain | ain | ain
# Consistent: | ain | ain
# Actual: | ain |
echo
printf -- '%s:\t' '"${FUNCNAME#m}"'
[ -n "${FUNCNAME#m}" ] && printf -- 'Yes; %s\n' "${FUNCNAME#m}" || printf -- 'No.\n'
set -u
printf -- '%s:\t' '"${FUNCNAME[0]#m}"'
[ -n "${FUNCNAME[0]#m}" ] && printf -- 'Yes; %s\n' "${FUNCNAME[0]#m}" || printf -- 'No.\n'
printf -- '%s:\t' '"${FUNCNAME[*]#m}"'
[ -n "${FUNCNAME[*]#m}" ] && printf -- 'Yes; %s\n' "${FUNCNAME[*]#m}" || printf -- 'No.\n'
set +u
################################################################################
# Finally, we have "prefix matching" expansion. (This returns all extant
# variables whose names start with a certain (nonempty) string. Variables set to
# the null string or to empty arrays are included; unset but declared variables
# are not.) This expansion fails to find our variable.
#
# (Note that, as written, this test won't be very informative if there are other
# variables in the environment whose names begin with the string 'FUNCNAME'.)
#
# Expected: FUNCNAME
# Consistent: <Unclear.>
# Actual:
echo
printf -- '%s:\t' '"${!FUNCNAME*}"'
[ -n "${!FUNCNAME*}" ] && printf -- 'Yes; %s\n' "${!FUNCNAME*}" || printf -- 'No.\n'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment