Bash how to parse asscoiative array from argument? - bash

I am trying to read a string into an associative array.
The string is being properly escaped and read into .sh file:
./myScript.sh "[\"el1\"]=\"val1\" [\"el2\"]=\"val2\""
within the script
#!/bin/bash -e
declare -A myArr=( "${1}" ) #it doesn't work either with or without quotes
All I get is:
line 2: myArr: "${1}": must use subscript when assigning associative array
Googling the error only results in "your array is not properly formatted" results.

You could read key/value pairs from variable inputs in series:
$ cat > example.bash <<'EOF'
#!/usr/bin/env bash
declare -A key_values
while true
do
key_values+=(["$1"]="$2")
shift 2
if [[ $# -eq 0 ]]
then
break
fi
done
for key in "${!key_values[#]}"
do
echo "${key} = ${key_values[$key]}"
done
EOF
$ chmod u+x example.bash
$ ./example.bash el1 val1 el2 val2
el2 = val2
el1 = val1
This should be safe no matter what the keys and values are.

Instead of caring about proper double escaping, set the variables on caller side, and use bash declare -p. That way you will always get properly escaped string.
declare -A temp=([el1]=val1 [el2]=val2)
./script.sh "$(declare -p temp)"
Then do:
# ./script.sh
# a safer version of `eval "$1"`
declare -A myarr="${1#*=}"

solution:
./test.sh "( [\"el1\"]=\"val1\" [\"el2\"]=\"val2\" )"
and within the script:
#!/bin/bash -e
declare -A myArr="${1}"
declare -p myArr
for i in "${!myArr[#]}"
do
echo "key : $i"
echo "value: ${myArr[$i]}"
done
Returns:
> ./test.sh "( [\"el1\"]=\"val1\" [\"el2\"]=\"val2\" )"
declare -A myArr=([el2]="val2" [el1]="val1" )
key : el2
value: val2
key : el1
value: val1
I can't explain why this works, or why the order changes though.
Test it yourself here

Related

Pass an array via command-line to be handled with getopts()

I try to pass some parameters to my .bash file.
terminal:
arr=("E1" "E2" "E3")
param1=("foo")
param2=("bar")
now I want to call my execute.bash file.
execute.bash -a ${arr[#]} -p $param1 -c param2
this is my file:
execute.bash:
while getopts ":a:p:c:" opt; do
case $opt in
a) ARRAY=${OPTARG};;
p) PARAM1=${OPTARG};;
c) PARAM2=${OPTARG};;
\?) exit "Invalid option -$OPTARG";;
esac
done
for a in "${ARRAY[#]}"; do
echo "$a"
done
echo "$PARAM1"
echo "$PARAM2"
But my file only prints:
E1
foo
bar
Whats the problem with my script?
Expanding all values in the array using ${arr[#]} expands each value as a separate command-line argument, so getopt only sees the first value as the parameter to the "-a" option.
If you expand using ${arr[*]} then all of the array values are expanded into a single command-line argument, so getopt can see all of the values in the array as a single argument to the "-a" option.
There are a couple of other issues: you need to quote the values on the command line:
< execute.bash -a ${arr[#]} -p $param1 -c param2
> execute.bash -a "${arr[*]}" -p $param1 -c $param2
and use braces around ${OPTARG} in the getopt processing to make it an array assignment:
< a) ARRAY=${OPTARG};;
> a) ARRAY=(${OPTARG});;
after making these changes, I get this output:
E1
E2
E3
foo
bar
which I think is what you are expecting.
You have a problem with passing the array as one of the parameter for the -a flag. Arrays in bash get expanded in command line before the actual script is invoked. The "${array[#]}" expansions outputs words separated by white-space
So your script is passed as
-a "E1" "E2" "E3" -p foo -c bar
So with the getopts() call to the argument OPTARG for -a won't be populated with not more than the first value, i.e. only E1. One would way to achieve this is to use the array expansion of type "${array[*]}" which concatenates the string with the default IFS (white-space), so that -a now sees one string with the words of the array concatenated, i.e. as if passed as
-a "E1 E2 E3" -p foo -c bar
I've emphasized the quote to show arg for -a will be received in getopts()
#!/usr/bin/env bash
while getopts ":a:p:c:" opt; do
case $opt in
a) ARRAY="${OPTARG}";;
p) PARAM1="${OPTARG}";;
c) PARAM2="${OPTARG}";;
\?) exit "Invalid option -$OPTARG";;
esac
done
# From the received string ARRAY we are basically re-constructing another
# array splitting on the default IFS character which can be iterated over
# as in your input example
read -r -a splitArray <<<"$ARRAY"
for a in "${splitArray[#]}"; do
echo "$a"
done
echo "$PARAM1"
echo "$PARAM2"
and now call the script with args as. Note that you are using param1 and param2 are variables but your definition seems to show it as an array. Your initialization should just look like
arr=("E1" "E2" "E3")
param1="foo"
param2="bar"
and invoked as
-a "${arr[*]}" -p "$param1" -c "$param2"
A word of caution would be to ensure that the words in the array arr don't already contain words that contain spaces. Reading them back as above in that case would have a problem of having those words split because the nature of IFS handling in bash. In that case though use a different de-limiter say |, # while passing the array expansion.
If I want to export the array MY_ARRAY, I use at caller side:
[[ $MY_ARRAY ]] && export A_MY_ARRAY=$(declare -p MY_ARRAY)
... and at at sub script side:
[[ $A_MY_ARRAY =~ ^declare ]] && eval $A_MY_ARRAY
The concept works for parameters too. At caller side:
SUB_SCRIPT "$(declare -p MY_ARRAY)"
... and at at sub script side:
[[ $1 =~ ^declare ]] && eval $1
The only issue of both solution is, that the variable names are the same at both sides. This can be changed if replacing the variable name before expanding it.

In Bash test if associative array is declared

How can I test if an associative array is declared in Bash? I can test for a variable like:
[ -z $FOO ] && echo nope
but I doesn't seem to work for associative arrays:
$ unset FOO
$ declare -A FOO
$ [ -z $FOO ] && echo nope
nope
$ FOO=([1]=foo)
$ [ -z $FOO ] && echo nope
nope
$ echo ${FOO[#]}
foo
EDIT:
Thank you for your answers, both seem to work so I let the speed decide:
$ cat test1.sh
#!/bin/bash
for i in {1..100000}; do
size=${#array[#]}
[ "$size" -lt 1 ] && :
done
$ time bash test1.sh #best of five
real 0m1.377s
user 0m1.357s
sys 0m0.020s
and the other:
$ cat test2.sh
#!/bin/bash
for i in {1..100000}; do
declare -p FOO >/dev/null 2>&1 && :
done
$ time bash test2.sh #again, the best of five
real 0m2.214s
user 0m1.587s
sys 0m0.617s
EDIT 2:
Let's speed compare Chepner's solution against the previous ones:
#!/bin/bash
for i in {1..100000}; do
[[ -v FOO[#] ]] && :
done
$ time bash test3.sh #again, the best of five
real 0m0.409s
user 0m0.383s
sys 0m0.023s
Well that was fast.
Thanks again, guys.
In bash 4.2 or later, you can use the -v option:
[[ -v FOO[#] ]] && echo "FOO set"
Note that in any version, using
declare -A FOO
doesn't actually create an associative array immediately; it just sets an attribute on the name FOO which allows you to assign to the name as an associative array. The array itself doesn't exist until the first assignment.
You can use declare -p to check if a variable has been declared:
declare -p FOO >/dev/null 2>&1 && echo "exists" || echo "nope"
And to check specifically associative array:
[[ "$(declare -p FOO 2>/dev/null)" == "declare -A"* ]] &&
echo "array exists" || echo "nope"
This is a Community Wiki version of an excellent answer by #user15483624 on a question which is now closed as duplicate. Should that user choose to add their own answer here, this should be deleted in favor of the one with their name on it.
The prior answers on this question should be used only when compatibility with bash 4.x and prior is required. With bash 5.0 and later, an expansion to check variable type is directly available; its use is far more efficient than parsing the output of declare -p, and it avoids some of the unintended side effects of other proposals as well..
The following can be used to test whether a bash variable is an associative array.
[[ ${x#a} = A ]]
${x#a} can be used to test whether it is a variable and an array as well.
$ declare x; echo "${x#a}"
$ declare -a y; echo "${y#a}"
a
$ declare -A z; echo "${z#a}"
A
One of the easiest ways is to the check the size of the array:
size=${#array[#]}
[ "$size" -lt 1 ] && echo "array is empty or undeclared"
You can easily test this on the command line:
$ declare -A ar=( [key1]=val1 [key2]=val2 ); echo "szar: ${#ar[#]}"
szar: 2
This method allow you to test whether the array is declared and empty or undeclared altogether. Both the empty array and undeclared array will return 0 size.

Why are "declare -f" and "declare -a" needed in bash scripts?

Sorry for the innocent question - I'm just trying to understand...
For example - I have:
$ cat test.sh
#!/bin/bash
declare -f testfunct
testfunct () {
echo "I'm function"
}
testfunct
declare -a testarr
testarr=([1]=arr1 [2]=arr2 [3]=arr3)
echo ${testarr[#]}
And when I run it I get:
$ ./test.sh
I'm function
arr1 arr2 arr3
So here is a question - why do I have to (if I have to ...) insert declare here?
With it - or without it it works the same...
I can understand for example declare -i var or declare -r var. But for what is -f (declare function) and -a (declare array)?
declare -f functionname is used to output the definition of the function functionname, if it exists, and absolutely not to declare that functionname is/will be a function. Look:
$ unset -f a # unsetting the function a, if it existed
$ declare -f a
$ # nothing output and look at the exit code:
$ echo $?
1
$ # that was an "error" because the function didn't exist
$ a() { echo 'Hello, world!'; }
$ declare -f a
a ()
{
echo 'Hello, world!'
}
$ # ok? and look at the exit code:
$ echo $?
0
$ # cool :)
So in your case, declare -f testfunct will do nothing, except possibly if testfunct exists, it will output its definition on stdout.
As far as I know, the -a option alone does not have any practical relevance, but I think it's a plus for readability when declaring arrays. It becomes more interesting when it is combined with other options to generate arrays of a special type.
For example:
# Declare an array of integers
declare -ai int_array
int_array=(1 2 3)
# Setting a string as array value fails
int_array[0]="I am a string"
# Convert array values to lower case (or upper case with -u)
declare -al lowercase_array
lowercase_array[0]="I AM A STRING"
lowercase_array[1]="ANOTHER STRING"
echo "${lowercase_array[0]}"
echo "${lowercase_array[1]}"
# Make a read only array
declare -ar readonly_array=(42 "A String")
# Setting a new value fails
readonly_array[0]=23
declare -f allows you to list all defined functions (or sourced) and their contents.
Example of use:
[ ~]$ cat test.sh
#!/bin/bash
f(){
echo "Hello world"
}
# print 0 if is defined (success)
# print 1 if isn't defined (failure)
isDefined(){
declare -f "$1" >/dev/null && echo 0 || echo 1
}
isDefined f
isDefined g
[ ~]$ ./test.sh
0
1
[ ~]$ declare -f
existFunction ()
{
declare -f "$1" > /dev/null && echo 0 || echo 1
}
f ()
{
echo "Hello world"
}
However as smartly said gniourf_gniourf below : it's better to use declare -F to test the existence of a function.

easy way to cleanly dump contents of associative array in bash?

In zsh I can easily dump the contents of an associative array with a single command:
zsh% typeset -A foo
zsh% foo=(a 1 b 2)
zsh% typeset foo
foo=(a 1 b 2 )
However despite searching high and low, the best I could find was declare -p, whose output contains declare -A:
bash$ typeset -A foo
bash$ foo=([a]=1 [b]=2)
bash$ declare -p foo
declare -A foo='([a]="1" [b]="2" )'
Is there a clean way to obtain something like the zsh output (ideally foo=(a 1 b 2 ) or foo='([a]="1" [b]="2" )'), preferably without resorting to string manipulation?
It seems there is no way to do this other than string manipulation. But at least we can avoid forking a sed process each time, e.g.:
dump_assoc_arrays () {
for var in "$#"; do
read debug < <(declare -p $var)
echo "${debug#declare -A }"
done
}
the declare -A is redundant
Good sir, the declare -A is not redundant.
$ foo=([a]="1" [b]="2")
$ echo ${foo[a]}
2
$ declare -A bar=([a]="1" [b]="2")
$ echo ${bar[a]}
1

Loading variables from a text file into bash script

Is it possible to load new lines from a text file to variables in bash?
Text file looks like?
EXAMPLEfoo
EXAMPLEbar
EXAMPLE1
EXAMPLE2
EXAMPLE3
EXAMPLE4
Variables become
$1 = EXAMPLEfoo
$2 = EXAMPLEbar
ans so on?
$ s=$(<file)
$ set -- $s
$ echo $1
EXAMPLEfoo
$ echo $2
EXAMPLEbar
$ echo $#
EXAMPLEfoo EXAMPLEbar EXAMPLE1 EXAMPLE2 EXAMPLE3 EXAMPLE4
I would improve the above by getting rid of temporary variable s:
$ set -- $(<file)
And if you have as input a file like this
variable1 = value
variable2 = value
You can use following construct to get named variables.
input=`cat filename|grep -v "^#"|grep "\c"`
set -- $input
while [ $1 ]
do
eval $1=$3
shift 3
done
cat somefile.txt| xargs bash_command.sh
bash_command.sh will receive these lines as arguments
saveIFS="$IFS"
IFS=$'\n'
array=($(<file))
IFS="$saveIFS"
echo ${array[0]} # output: EXAMPLEfoo
echo ${array[1]} # output: EXAMPLEbar
for i in "${array[#]}"; do echo "$i"; done # iterate over the array
Edit:
The loop in your pastebin has a few problems. Here it is as you've posted it:
for i in "${array[#]}"; do echo " "AD"$count = "$i""; $((count=count+1)); done
Here it is as it should be:
for i in "${array[#]}"; do declare AD$count="$i"; ((count=count+1)); done
or
for i in "${array[#]}"; do declare AD$count="$i"; ((count++)); done
But why not use the array directly? You could call it AD instead of array and instead of accessing a variable called "AD4" you'd access an array element "${AD[4]}".
echo "${AD[4]}"
if [[ ${AD[9]} == "EXAMPLE value" ]]; then do_something; fi
This can be done be with an array if you don't require these variables as inputs to a script. push() function lifted from the Advanced Scripting Guide
push() # Push item on stack.
{
if [ -z "$1" ] # Nothing to push?
then
return
fi
let "SP += 1" # Bump stack pointer.
stack[$SP]=$1
return
}
The contents of /tmp/test
[root#x~]# cat /tmp/test
EXAMPLEfoo
EXAMPLEbar
EXAMPLE1
EXAMPLE2
EXAMPLE3
EXAMPLE4
SP=0; for i in `cat /tmp/test`; do push $i ; done
Then
[root#x~]# echo ${stack[3]}
EXAMPLE1
None of the above will work, if your values are quoted with spaces.
However, not everythinf is lost.
Try this:
eval "$(VBoxManage showvminfo "$VMname" --details --machinereadable | egrep "^(name|UUID|CfgFile|VMState)")"
echo "$name {$UUID} $VMState ($VMStateChangeTime) CfgFile=$CfgFile"
P.S.
Nothing will ever work, if your names are quoted or contain dashes.
If you have something like that, as is the case with VBoxManage output ("IDE-1-0"="emptydrive" and so on), either egrep only specific values, as shown in my example, or silence the errors.
However, silencing erors is always dangerous. You never know, when the next value will have unquoted "*" in it, thus you must treat values loaded this way very careful, with all due precaution.

Resources