9

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.


Part I understand

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"

Part I don't understand

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!


Potentially relevant information:

$ 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.

greatBigDot
  • 505
  • 3
  • 9
  • 2
    It's probably worth pointing this out on the `bash-bug` mailing list; subtle semantic points like this tend not to be defined until they come up, at which point the developers try to hash out whether the observed behavior is justifiable/correct, or should be defined in some other way. It's also worth nothing that `typeset +n foo` does appear to make `foo` a regular variable in `ksh`, the shell that `bash` borrowed namerefs from. – chepner Mar 08 '18 at 18:07
  • @chepner Thanks, I'll try that (if no one explains it here, that is). Gah, why does bash have to be *so* very unpredictable around edge cases? – greatBigDot Mar 08 '18 at 18:14
  • ...wait, "make them into regular variables", *not* unset them entirely (after which you can assign a new regular variable with the same name)? Why would you want to do that? – Charles Duffy Mar 08 '18 at 18:15
  • Like I said, I was just screwing around with namerefs to try to understand them. Trying to unset namerefs is what led me to this, but that's not the behavior I was confused by. I can't really think of a scenario where you'd want to make a nameref a normal variable for any purpose *other* than unsetting them, though. – greatBigDot Mar 08 '18 at 18:20
  • @chepner [Thanks for the tip; it is a bug](https://lists.gnu.org/archive/html/bug-bash/2018-03/msg00055.html) – greatBigDot Mar 11 '18 at 01:45

1 Answers1

7

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
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Awesome, thanks! That solves the problem. Still, is there any good reason for `declare +n` to act like it does? (Not that it really matters--`unset -n` makes it irrelevant.) – greatBigDot Mar 08 '18 at 18:24
  • Reading the comments in the source, it's... interesting in there. See http://git.savannah.gnu.org/gitweb/?p=bash.git;a=blob;f=builtins/declare.def;h=f4819b42377ee04da7b5253886b8f428dcd9fb9c;hb=refs/heads/master#l547 and surrounding area. I'd need to ruminate on it a little more (and maybe look at the history of the changes / bug reports that drove them) to make a pronouncement. – Charles Duffy Mar 08 '18 at 18:53
  • 1
    It's like the code is trying to convince itself that it's right. – chepner Mar 08 '18 at 21:02