Variable handling and scoping in shell and especially bash can be very obscure and unintuitive (and sometimes buggy).
ksh had typeset for a similar feature. ksh, zsh, yash have typeset. bash has typeset as an alias of declare for compatibility with ksh, zsh has declare as an alias of typeset for compatibility with bash. Most shells have export, readonly and local that implement part of what typeset does.
One of the reasons why bash authors chose declare over typeset may be because typeset does not only set the type, it also declares a variable: introduce it in a given scope, possibly with a type, attributes and/or value.
In bash, variables can be:
- unknown (like when they never have been set or declared)
- declared (after
declare) - set (when given a value, possibly empty).
They can be of different types:
- scalar
- array
- associative array
and have several attributes:
- integer
- exported
- readonly
- all lower-case/upper-case
- named references
(though the distinction between type and attribute can be quite blurry).
Not all combinations of types and attributes are supported or effective.
Now, declare declares the variable in the current scope. bash, even though it implements dynamic scoping treats the outer-most scope specially. It calls it the global scope.
declare behaves very differently when it's called in that global scope and when in a function (I'm not talking of the kind of separate scope that is introduced by subshells or associated with the environment).
When you do declare var inside a function and provided that same variable hadn't been declared in that same scope, it declares a new variable which is initially unset and that shadows a potential var variable that would have been present in the parent scope (the caller of the function).
That's dynamic scoping implemented via some sort of stack. When the function exits, the status, type, attributes and value of the variable as it was when the function was invoked is restored (popped from the stack).
Outside of any function however (in the global scope), declare does declare a variable, but does not initialise it as unset if it was set before (same as when using declare a second time within the same function scope). If a type is specified, the value of the variable may be converted, though not all conversion paths are allowed (only scalar to array/hash), and attributes may be added or removed.
In bash, within a function declare -g acts on the variable at the bottom of the stack, in the outer-most ("global") scope.
declare -g was inspired from ksh93's typeset -g. But ksh93 implements static scoping where the global scope is different and separate from each function scope. Doing the same with dynamic scoping makes little sense. In all other shells that have typeset -g (mksh, zsh, yash), typeset -g is used to change some attribute of a variable without instantiating a new local one.
In bash, people generally use it for the same purpose, but because it affects the variable of the outer-most scope instead of the current variable, it doesn't always work.
For instance:
integer() { typeset -gi "$1"; }
To make a variable an integer works in mksh/yash/zsh. It works in bash only on variables that have not been declared local by the caller:
$ bash -c 'f() { declare a; integer a; a=1+1; echo "$a"; }; integer() { typeset -gi "$1"; }; f' 1+1 $ bash -c 'f() { integer a; a=1+1; echo "$a"; }; integer() { typeset -gi "$1"; }; f' 2
Note that export var is neither typeset -x var nor typeset -gx var. It adds the export attribute without declaring a new variable if the variable already existed. Same for readonly vs typeset -r.
Also note that unset in bash only unsets a variable if it has been declared in the current scope (leaves it declared though except in the global scope; it removes attributes and values and the variable is no longer array or hash; also note that on namerefs, it unsets the referenced variable). Otherwise, it just pops one variable layer from the stack mentioned above. With bash 5.0 or above, that can be fixed by setting the localvar_unset option.
So to sum up:
declare var
When called in a function and if var has not been declared before in that same function, declares a variable of type scalar with no attributes and that is initially unset.
If called outside of any function or if var had already been declared in the same function, it has no effect as we're not specifying any new type or attribute.
declare -g var
Wherever it's called would declare a var in the outer-most ("global") scope: make it declared, of type scalar, no attribute, no value if it was previously unknown in that scope (which for all intent and purpose is the same as an unknown variable except that it would show in the output of typeset -p), or do nothing otherwise.
In any case, you might not be able to access that variable in the context you're running that command:
f() { local a; g; }; g() { typeset -g a=123; echo "$a"; }; f
outputs nothing.