EDIT: This has been confirmed to be a bug and will be fixed: https://lists.gnu.org/archive/html/bug-bash/2018-03/msg00055.html
So I'm messing around with bash's indirection feature, namerefs. I thought I had gotten the hang of it, but then I hit upon something that confused me when I was trying to figure out how to make namerefs into regular variables.
Here's a relevant passage from man bash
:
declare -n
Give each name the nameref attribute, making it a name reference to another variable. That other variable is defined by the value of name. All references, assignments, and attribute modifications to name, except those using or changing the -n attribute itself, are performed on the variable referenced by name's value.
So say you wanted to unset a nameref--not the varaible that it points to, but the variable itself. You couldn't just say unset foo
, because that would actually unset whatever foo
points to; instead, you'd have to make it a regular variable, then unset it:
$ declare -p
$ foo=bar; bar='hello world'
$ declare -p
declare -- foo="bar"
declare -- bar="hello world"
$ declare -n foo; declare -p # 'foo' is now a nameref
declare -n foo="bar"
declare -- bar="hello world"
$ declare +n foo; declare -p # 'foo' is no longer a nameref
declare -- foo="bar"
declare -- bar="hello world"
$ unset foo; declare -p # 'foo' is unset, not bar
declare -- bar="hello world"
That all makes sense to me and is consistent with my reading of the above manual passage. What confuses me is what happens upon a minor variation in the above--namely, we leave bar
unset and undeclared:
...
$ declare -p
declare -n foo="bar"
$ echo "${foo}" # These two commands behave as expected--i.e., identically to how namerefs usually behave, just with an unset variable.
-bash: foo: unbound variable
$ echo "${!foo}"
bar
$ declare +n foo; declare -p # Should make 'foo' a regular variable, right? Nope.
declare -n foo="bar" # Still a nameref--wtf?
declare -- bar # And now bar's back--unset still, but declared. Wtf??
$ declare +n foo; declare -p # THIS, however, works like I thought it would--but *why*? In both cases 'bar' is unset...
declare -- foo="bar"
declare -- bar
I apparently misunderstand how namerefs are supposed to work. Based on the passage from man, I would think that unsetting the nameref attribute of foo
should work on foo
, regardless of whether its target, bar
, is undeclared.
Note that it works how I thought it would when bar
is unset, but declared. This is the oddest part to me--I didn't realize there was any significance to a variable being undeclared! test -v
, ${var-_}
, ${var+_}
, and set -u
all only seem to care about whether the variable is set and make no distinction whatsoever between (A) an unset, undeclared variable and (B) an unset, declared variable.
Can someone explain what's going on here, maybe point to the portion of the manual that explains this? Are there any other special cases in the behavior of namerefs I'm going to be confused about? Thanks!
$ bash --version
GNU bash, version 4.4.19(1)-release (x86_64-unknown-linux-gnu)
...
$ echo "$-"
himuBCHs
Note that the behavior persists without set -u
; I just did that to make bash's messages a little clearer.
$$ is a Bash internal variable that contains the Process ID (PID) of the shell running your script. Sometimes the $$ variable gets confused with the variable $BASHPID that contains the PID of the current Bash shell.
'declare' is a bash built-in command that allows you to update attributes applied to variables within the scope of your shell. In addition, it can be used to declare a variable in longhand. Lastly, it allows you to peek into variables.
The only valid beginning character types for bash variables are letters and underscores.
There's a new argument to unset
explicitly for the purpose of undefining a nameref (as opposed to the variable it points to):
unset -n namevar
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With