"local -g" declaration references local var in enclosing scope

2024-03-10 Thread Adrian Ho
[Apologies, an earlier edition of this bug report was sent from the address
a...@lex.03s.net, which can only be replied to from my internal network.
Please ignore that report. Thanks much!]

Configuration Information [Automatically generated, do not change]:
Machine: x86_64
OS: linux-gnu
Compiler: gcc-11
Compilation CFLAGS: -DSSH_SOURCE_BASHRC
uname output: Linux lex 6.5.0-25-generic #25-Ubuntu SMP PREEMPT_DYNAMIC Wed
Feb  7 14:58:39 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
Machine Type: x86_64-pc-linux-gnu

Bash Version: 5.2
Patch Level: 26
Release Status: release

Description:
"local -g var" incorrectly references a local variable in an
enclosing scope, while "local -g var=val" correctly references and modifies
the global variable.

Repeat-By:
Test script:

#!/usr/bin/env bash
echo "Bash version: $BASH_VERSION"

b() {
  local -g a=2
  c
}
c() {
  local -g a
  a=3
}

y() {
  local z=1
  x
}
x() {
  local -g z=2
  w
}
w() {
  local -g z
  echo "w: z=$z"
  z=3
}

b; y
echo "a=$a z=$z"

Expected output:

Bash version: 5.2.26(1)-release
w: z=2
a=3 z=3

Actual output:

Bash version: 5.2.26(1)-release
w: z=1
a=3 z=2


Re: "local -g" declaration references local var in enclosing scope

2024-03-11 Thread Adrian Ho
Thanks much for all the insights, everyone! Indeed, the man page isn't
clear on how to reason about `local -g`. I'm now left with the following
understanding:

1. There is *nothing local* about `local -g`; it operates entirely at the
global level. In particular, it does *not* create a local reference to a
global var, which is the natural assumption given the typical behaviour of
`declare/local`.

2. Because of this, there is no way to *read* a global var if a calling
scope declares a shadowing var, short of unsetting that shadow var.

3. This is NOTABUG.

Have I got that right? If so, can I suggest a documentation enhancement for
`declare`, from:

The `-g` option forces variables to be created or modified at the global
scope, even when `declare` is executed in a shell function.

to:

The `-g` option forces variables to be created or modified at the global
scope, even when `declare` is executed in a shell function, *and no
references to these variables are created in that function's scope*.


Thanks again!

On Mon, Mar 11, 2024 at 12:08 PM Kerin Millar  wrote:

> On Sun, 10 Mar 2024 16:01:10 -0400
> Lawrence Velázquez  wrote:
>
> > On Sun, Mar 10, 2024, at 1:51 PM, Kerin Millar wrote:
> > > Dynamic scoping can be tremendously confusing. The following examples
> > > should help to clarify the present state of affairs.
> > >
> > > $ x() { local a; y; echo "outer: $a"; }
> > > $ y() { local a; a=123; echo "inner: $a"; }
> > > $ x; echo "outermost: $a"
> > > inner: 123
> > > outer:
> > > outermost:
> > >
> > > This is likely as you would expect.
> > >
> > > $ y() { local -g a; a=123; echo "inner: $a"; }
> > > $ x; echo "outermost: $a"
> > > inner: 123
> > > outer: 123
> > > outermost:
> > >
> > > This may not be. There, the effect of the -g option effectively ends
> at
> > > the outermost scope in which the variable, a, was declared. Namely,
> > > that of the x function.
> >
> > This doesn't seem to be accurate; the assignment is performed at
> > the *innermost* declared scope (other than the "local -g" one):
> >
> >   $ x() { local a; y; echo "outer: $a"; }
> >   $ y() { local a; z; echo "inner: $a"; }
> >   $ z() { local -g a; a=123; echo "innermost: $a"; }
> >   $ x; echo "outermost: $a"
> >   innermost: 123
> >   inner: 123
> >   outer:
> >   outermost:
> >
> > Basically, without an assignment, "local -g" does nothing.
>
> It might be tempting to think that the criteria for being "ignored" are
> fulfilled but it is not the case. Below is something that I should also
> have tried before initially posting in this thread.
>
> $ z() { a=123; echo "innermost: $a"; }; unset -v a; x; declare -p a
> innermost: 123
> inner: 123
> outer:
> bash: declare: a: not found
>
> $ z() { local -g a; a=123; echo "innermost: $a"; }; unset -v a; x; declare
> -p a
> innermost: 123
> inner: 123
> outer:
> declare -- a
>
> $ z() { local -g a=456; a=123; echo "innermost: $a"; }; unset -v a; x;
> declare -p a
> innermost: 123
> inner: 123
> outer:
> declare -- a="456"
>
> I think that Greg has it right. The use of the -g option, alone, is
> sufficient to reach into the global scope, though dynamic scoping behaviour
> otherwise remains in effect. That is, one would otherwise still need to pop
> scopes - so to speak - to reach the outermost/bottommost scope.
>
> Speaking of which, to do both of these things has some interesting effects
> ...
>
> $ z() { local -g a; unset -v a; a=123; echo "innermost: $a"; }; unset -v
> a; x; declare -p a
> innermost: 123
> inner: 123
> outer: 123
> declare -- a
>
> $ z() { local -g a; unset -v a; unset -v a; a=123; echo "innermost: $a";
> }; unset -v a; x; declare -p a
> innermost: 123
> inner: 123
> outer: 123
> declare -- a="123"
>
> $ x() { local a; y; local +g a; a=456; echo "outer: $a"; }; unset -v a; x;
> declare -p a
> innermost: 123
> inner: 123
> outer: 456
> declare -- a="123"
>
> I remain somewhat uncertain that the manual conveys enough information to
> be able to perfectly reason with all of this.
>
> --
> Kerin Millar
>


-- 
Regards,
Adrian