Last active
November 8, 2018 06:22
-
-
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?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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