why bash throws unbound variable warning when I declare a local array in function whose name is shadowing a global one? - bash

In this example declaring local variable with different name from that of global scope produces no error but when name is the same as global I get:
line 5: !1: unbound variable
code:
set -u
function get_arr {
local myArr2=("${!1}")
echo ${myArr2[*]}
local myArr=("${!1}")
echo ${myArr[*]}
}
myArr=(one two three)
get_arr myArr[#]

Just to make sure we are on the same sheet of paper, here is the version working on Bash 3.2 (works fine quoted or unquoted). You must either have an environment setting or stray characters in your file, of something unrelated to your script causing issues:
#!/bin/bash
set -u
function get_arr {
local myArr2=("${!1}")
echo ${myArr2[*]}
local myArr=("${!1}")
echo ${myArr[*]}
}
myArr=(one two three)
get_arr "myArr[#]"
exit 0
Version
$ bash --version
GNU bash, version 3.2.39(1)-release (i586-suse-linux-gnu)
Copyright (C) 2007 Free Software Foundation, Inc.
Output
$ bash array_indirect_ref.sh
one two three
one two three
Execution
$ bash -x array_indirect_ref.sh
+ set -u
+ myArr=(one two three)
+ get_arr 'myArr[#]'
+ myArr2=("${!1}")
+ local myArr2
+ echo one two three
one two three
+ myArr=("${!1}")
+ local myArr
+ echo one two three
one two three

Update: it appears that how you declare the passed array inside your function affects whether or not shadowed names will work, even in new bash versions.
I have some bash code that used to work, as of last week, but now fails after I updated cygwin to its current code.
~~~~~~~~~~
My cygwin bash version is now 4.3.39:
$ bash --version
GNU bash, version 4.3.39(2)-release (i686-pc-cygwin)
which is the latest.
~~~~~~~~~~
Consider this bash code:
#!/bin/bash
set -e # exit on first failed command
set -u # exit if encounter never set variable
testArrayArg1() {
declare -a argArray=("${!1}")
echo "testArrayArg1: ${argArray[#]}"
}
testArrayArg2() {
declare -a anArray=("${!1}")
echo "testArrayArg2: ${anArray[#]}"
}
anArray=("a" "b" "c")
testArrayArg1 anArray[#]
testArrayArg2 anArray[#]
Note that testArrayArg2 function uses an array name (anArray) which shadows the subsequent variable name in the script.
Also note that the way I pass the array to the function (anArray[#]) and the way that I declare the array in the function (declare -a anArray=("${!1}")) are taken from Ken Bertelson's answer here.
Both functions above used to always work.
Now, after my cygwin/bash update, testArrayArg1 still works but testArrayArg2 which uses a shadowed array name fails:
$ bash t.sh
testArrayArg1: a b c
t.sh: line 11: !1: unbound variable
Anyone know what changed recently in bash to cause this?
~~~~~~~~~~
I can fix this if I change how I declare the array inside the function from declare -a anArray=("${!1}") to your "local" idiom of local anArray=("${!1}").
So, this code
testArrayArg3() {
local anArray=("${!1}")
echo "testArrayArg3: ${anArray[#]}"
}
testArrayArg3 anArray[#]
works:
testArrayArg3: a b c
~~~~~~~~~~
OK, so the local anArray=("${!1}") function array arg declaration idiom seems to work.
This idiom is mentioned in that SO link that I mentioned above in a hidden comment under Ken Bertelson's answer. To see it, click on the "show 3 more" link and check out Mike Q's comment.
Is it every bit as good as the declare -a anArray=("${!1}") idiom, or does it have drawbacks of its own?
I have some critical code that depends on passing arrays to bash functions, so I really need to get this straight.

Related

backport "declare -n" from BASH 4.4 to 4.2

With Bash 4.4, I wrote scripts using "declare -n" but today I learned that when I give those to RedHat 7 users, the scripts fail because their BASH is 4.2.
Here's a small example of the problem, I wonder if you can advise me on good method to backport this to BASH 4.2:
#!/bin/bash
pwd=`pwd`
declare -A parms
parms[engine]=\"Sweave\"
parms[verbose]=FALSE
parms[tangle]=TRUE
## builds $parmstring by concatenating key=value pairs
catarr() {
declare -n __p="$1"
for k in "${!__p[#]}"
do parmstring+=", $k=${__p[$k]}"
done
}
parmstring=""
catarr parms
echo ${parmstring[*]}
The output is supposed to be like this:
$ bash bashmre.sh
, engine="Sweave", verbose=FALSE, tangle=TRUE
But on old BASH, RedHat says declare does not allow "-n".
I think what you're looking for can be achieved using eval:
#!/bin/bash
pwd=`pwd`
declare -A parms
parms[engine]=\"Sweave\"
parms[verbose]=FALSE
parms[tangle]=TRUE
## builds $parmstring by concatenating key=value pairs
catarr() {
eval keys=(\"\${!$1[#]}\")
for k in "${keys[#]}"
do
eval val=\${$1[$k]}
parmstring+=", $k=$val"
done
}
parmstring=""
catarr parms
echo ${parmstring[*]}
I tested this on bash 4.2.37 and got the desired output
It's not at all clean, but you can generate and then execute code. To try to keep the generation safe, use printf %q for substituted values.
catarr() {
local eval_str
printf -v eval_str '
for k in "${!%q[#]}"; do
parmstring+=", $k=${%q[$k]}"
done
' "$1" "$1"
eval "$eval_str"
}
I gave +1 to the answers by #Charles Duffy and #shay, but in the end I did not use either answer.
I took the path that Charles suggested in a comment to my original post. I re-wrote the thing so there is one separate function for each of the arrays that needs to be concatenated. In the actual application were we are using this, there are two arrays, "parms" and "myopts" and so I end up with functions named catparms and catmyopts
catparms(){
for k in "${!parms[#]}"
do parmstring+=", $k=${parms[$k]}"
done
}
catmyopts(){
for k in "${!myopts[#]}"
do optstring+=", $k=${myopts[$k]}"
done
}
If I had a lot of arrays floating around, that method would become tedious.
If that happens, I am pretty sure I would go back to the method I used relying on Bash 4.4 and tell the people with old Bash they have to upgrade or forget about it. I would need to learn how to add code in the script that would detect the current Bash version and terminate if it is not 4.3 or higher. I've not done it, but could.

Why doesn't this custom sourcing function make my declared variable globally available?

I'm facing a very weird issue. I know I'm missing something basic but for the life of me I can't quite figure out what.
Consider these declarations in a file tmp.sh:
declare -A aa
aa[1]=hello
aa[2]=world
myfunc() {
echo exists
}
myvar=exists
I source the script as source tmp.sh and run:
myfunc
echo $myvar
echo ${aa[#]}
The output is:
exists
exists
hello world
Now I do the same thing but put the source statement in a function:
mysource() {
filename="$1"
source "$filename"
}
This time the output is:
exists
exists
What's going on here?
Add the -g option to declare. [1]
From the manual
-g create global variables when used in a shell function; otherwise ignored (by default, declare declares local scope variables when used in shell functions)
Also useful to mention from chepner's comment below
source works by executing the contents of the file exactly as if you replaced the source command with contents of the file. Even though the declare statements are not in a function in your file, they are part of the function that calls source.
[1] The -g option requires Bash 4.2 or above.
To complement 123's helpful answer:
By default, declare creates a local variable when used in a function (to put it differently: inside a function, declare by default behaves the same as local).
To create a global variable from inside a function:
Bash 4.2+:
use declare -g (e.g., declare -g foo='bar')
Older Bash versions, including 3.x:
simply assign a value to the variable (e.g., foo='bar'), do not use declare.
As an aside:
Your sample code uses declare -A to declare an associative array, which requires Bash 4.0.
Associative arrays are the only types of (non-environment) variables that strictly need a declare statement for their creation - you cannot create an associative array without declare -A, whereas you can create (non-integer-typed) scalars and arrays implicitly by simple assignment.
Thus, given that declare -g requires Bash 4.2, there is no solution to your problem if you happen to be stuck on 4.0 or 4.1.
3.x versions of Bash don't face this problem, because they don't support declare -A altogether.

Is there file scope in bash programming?

I want to make this variable
local to="$HOME/root_install/grunt"
be available to the entire file
makeGrunt(){
# set paths
local to="$HOME/root_install/grunt"
cd $to
sudo npm install -g grunt-init
sudo git clone https://github.com/gruntjs/grunt-init-gruntfile.git ~/.grunt-init/gruntfile
sudo grunt-init gruntfile
}
In POSIX-like shells - unless you use nonstandard constructs such as local, typeset, or declare - variables created implicitly through
assignment have global scope in the shell at hand.
Thus, to="$HOME/root_install/grunt" will make variable $to available anywhere in the current shell - unless you're inside a function and that variable was explicitly marked as local.
andlrc's helpful answer demonstrates the pitfalls associated with subshells - subshells are child processes that are clones of the original shell - they see the same state, but cannot modify the original shell's environment.
Bash shells use dynamic
scopes
which means that all variables are available for all called functions, commands,
etc. Consider this:
var=1
a() {
local var=2
b
}
b() {
echo "$var"
}
a # 2
b # 1
a # 2
When using the local keyword a variable will be available for the function, in
where it's defined, but also within all functions called from that function.
The same applies when a variable is created without the local keyword. With
that exception that it will also be available outside the function.
One more thing to be aware of is that whenever a subshell is created a variable
will not be able to "leave" it, i.e. when a pipe is involved. Consider this:
sum=0
seq 3 | while read -r num; do
sum=$((sum + num))
echo "$sum" # will print 1, 3 and 6
done
echo "$sum" # 0 huh? 1 + 2 + 3 = 0?

Strange behaviour of declare/typeset with "options" array in zsh

I attempted to declare a variable named options in a script for zsh. Turned out that it's some reserved name and zsh stores an associative array under it.
function mcve() {
options='';
}
$ mcve
mcve:1: options: attempt to set slice of associative array
Tried to look at its contents with declare and encountered a strange behaviour. The output is different before and after first occurence of the above error.
$ zsh
$ declare options
options
$ zsh
$ mcve
mcve:1: options: attempt to set slice of associative array
$ declare options
options=(autolist on printexitvalue off...<20 more lines>)
What's happening? Why is output different? Is the options array declared at the moment of first attempt to use it?
I've heard that typeset should be used instead of declare, but my man zshbuiltins says they're perfectly equal.
Also, this runs without failure:
function mcve() {
declare options;
options='';
echo ok;
}
$ zsh
$ mcve
ok
Why is this different?
The options associative array is documented in man zshmodules, under ZSH/PARAMETERS. I can't explain the behavior of declare options, but I will note that print $options[#] will output a list of on/off values even when declare options shows nothing.
In your last example, declare options inside a function definition always declares a new local variable, whether or not a global by the same name already exists.

Associative arrays are local by default

Associative arrays seem to be local by default when declared inside a function body, where they should be global. The following code
#!/bin/bash
f() {
declare -A map
map[x]=a
map[y]=b
}
f
echo x: ${map[x]} y: ${map[y]}
produces the output:
x: y:
while this
#!/bin/bash
declare -A map
f() {
map[x]=a
map[y]=b
}
f
echo x: ${map[x]} y: ${map[y]}
produces the output:
x: a y: b
Is it possible to declare a global associative array within a function?
Or what work-around can be used?
From: Greg Wooledge
Sent: Tue, 23 Aug 2011 06:53:27 -0700
Subject: Re: YAQAGV (Yet Another Question About Global Variables)
bash 4.2 adds "declare -g" to create global variables from within a
function.
Thank you Greg! However Debian Squeeze still has Bash 4.1.5
Fine, 4.2 adds "declare -g" but it's buggy for associative arrays so it doesn't (yet) answer the question. Here's my bug report and Chet's confirmation that there's a fix scheduled for the next release.
http://lists.gnu.org/archive/html/bug-bash/2013-09/msg00025.html
But I've serendipitously found a workaround, instead of declaring the array and assigning an initial value to it at the same time, first declare the array and then do the assignment. That is, don't do this:
declare -gA a=([x]=1 [y]=2)
but this instead:
declare -gA a; a=([x]=1 [y]=2)
You have already answered your own question with declare -g. The workaround on bash versions < 4.2 is to declare the array outside of the function.
f() {
map[y] = foo
}
declare -A map
foo
echo "${map[y]}"
This example declares a global associative array variable inside a function, in bash.
set -euf +x -o pipefail # There is no place for implicit errors in this script.
function init_arrays(){
# FYI. Multiple array declarations are not a problem. You can invoke it multiple times.
# The "-gA" switch is the trick for the global array declaration inside a function.
declare -gA my_var
}
function do_work(){
init_arrays
my_var[$1]=OPPA
}
do_work aa
echo ${my_var[aa]}
echo It is expected to get OPPA value printed above
Tested on GNU bash, version 4.4...
Important notes.
The declare -A command doesn't actually create an associative array immediately; it just sets an attribute on a variable name which allows you to assign to the name as an associative array. The array itself doesn't exist until the first assignment (!!!).
(I wanted to see a complete working example in this thread, sorry.)
For those who are stuck with Bash version < 4.2 and are not comfortable with proposed workarounds I share my custom implementation of global associative arrays. It does not have the full power of bash associative arrays and you need to be careful about special characters in array index, but gets job done.
get_array(){
local arr_name="$1"
local arr_key="$2"
arr_namekey_var="ASSOCARRAY__${arr_name}__${arr_key}"
echo "${!arr_namekey_var:=}"
}
set_array(){
local arr_name="$1"
local arr_key="$2"
local arr_value="$3"
arr_namekey_var="ASSOCARRAY__${arr_name}__${arr_key}"
if [[ -z "${arr_value}" ]]; then
eval ${arr_namekey_var}=
else
printf -v "${arr_namekey_var}" "${arr_value}"
fi
}
Few notes:
Array name and array key could be combined into a single value, but split proved convenient in practice.
__ as a separator can by hacked by malicious or careless use -- to be on the safe side use only single-underscore values in array name and key, on top of only using alphanumeric values. Of course the composition of the internal variable (separators, prefix, suffix...) can be adjusted to application and developer needs.
The default value expansion guarantees that undefined array key (and also array name!) will expand to null string.
Once you move to version of bash where you are comfortable with builtin associative arrays, these two procedures can be used as wrappers for actual associative arrays without having to refactor whole code base.

Resources