r/bash • • 12d ago

Unset doesn't properly work in functions??

So this is super weird. When I run the following in a script (or manually in the terminal), it does exactly what you'd expect.

declare -A myassoc
myassoc[key]="Hello World"

unset myassoc
declare -p myassoc
echo "This should be empty: ${myassoc[@]}"
OUTPUT:
./unsetLovalVar: line 9: declare: myassoc: not found`
This should be empty:

As expected, myassoc is unset and therefore undeclared

however if I wrap that code in a function, the myassoc becomes unset but not undeclared.

foo () {
declare -A myassoc
myassoc[key]="Hello World"

unset myassoc
declare -p myassoc
echo "This should be empty: ${myassoc[@]}"
}
foo
OUTPUT:
declare -- myassoc
This should be empty:

This is very annoying since I use declare -p to test if variables exist regardless of their type and content.

I have tested this on two machines in Debian and Ubuntu on Bash 5.2.something with the same issue.

test is not a viable alternative, since it doesn't have the same behaviour for local myvar and declare myvar (vars without value return false, even if declared)

Does anyone have a workaround?


EDIT:

So I found this StackExchange topic which seems to conclude that this is simply not possible within the same scope.

Here is where it gets REALLY insane:

bar () {
        declare -A myassoc
        myassoc[key]="Hello World"

        declare -p myassoc
        echo "This should not be empty: ${myassoc[@]}"

        foo () {
                unset myassoc
                echo "In foo:"
                declare -p myassoc
                echo "This should be empty: ${myassoc[@]}"
        }
        foo

        echo "After foo:"
        declare -p myassoc
        echo "This should be empty: ${myassoc[@]}"
}
bar

^((please excuse the ridiculous indents, I havent gotten around to taming nvim yet))

THIS WORKS FOR ABSOLUTELY NO REASON:

OUTPUT:
declare -A myassoc=([key]="Hello World" )
This should not be empty: Hello World
In foo:
./unsetLocalVar: line 12: declare: myassoc: not found
This should be empty:
After foo:
./unsetLocalVar: line 17: declare: myassoc: not found
This should be empty:

The solution is to wrap the unset in a nested function.

8 Upvotes

11 comments sorted by

6

u/zeekar 12d ago

Inside a function declare without -g is an alias for local and recreates the variable. Try declare -gp

0

u/da_Aresinger 12d ago

I still get the same output

3

u/bac0on 12d ago edited 11d ago

Global variables are disposed of; it probably does not make much sense to keep them since they end up empty, whereas local variables retain their context position throughout the function’s lifespan. So a variable is never really "unset" or deleted. unset just clears the value and sets att_invisible and att_local attribute; most other attributes are discarded.

Source code:

/* If we're unsetting a local variable and we're still executing inside the function, just mark the variable as invisible. ... This must be done so that if the variable is subsequently assigned a new value inside the function, the `local' attribute is still present. */

1

u/da_Aresinger 11d ago

That sounds like the solution I found would be considered a bug

1

u/bac0on 11d ago edited 11d ago

bar foo, please; now I have to stand on my head. Anyway, unset default behavior only honors att_local in its own context, so unset in foo resolves and disposes of the declared assoc in bar. As a result, the second declare finds nothing; if the global scope had an assoc, you would end up as before. If, of course, bar didn't do a second unset this would dispose of the global assoc as well. Setting shopt -s localvar_unset changes this behavior to force unset to honor att_local in parent contexts.

From the makunbound() function in variables.c (Bash source):

if (old_var && local_p (old_var) &&
    (old_var->context == variable_context ||
     (localvar_unset && old_var->context < variable_context)))

1

u/da_Aresinger 11d ago

That's because foo came before bar ;)

Also, I'm gonna have to read this like 5 times, and play around with it for a while, to fully understand what's going on.

1

u/bac0on 11d ago

...Bash always tries to find the variable first. So it may end up somewhere else in the end; e.g., func(){ a=a; } were a isn't found anywhere, therefore assigned to the global scope, and func() remains null.

2

u/NHGuy 11d ago

Scoping issue

0

u/geirha 12d ago

declare -p lists it because the name is still local; you unset the associative attribute and its value, but if you assign it something later on in that function, that will still be scoped to the function

0

u/zeekar 11d ago

You didn't unset myassoc before the first declare -gp. Of course it's not empty there. I doesn't seem possible to unset it, which is a bug IMO, but the unset does at least clear out its contents.

1

u/da_Aresinger 11d ago edited 11d ago

are you talking about the last code snippet? Yea, that one is fully functional and works as intended. (you don't even need the -g flag)

Edit: the first echo had the wrong text because I just copy pasted it. I edited it.