Bash initialize sparse array - linux

I have an array, index is the hard drive size and value is number of hard drives having same size. So this is what i do.
DRIVE_SIZES[$DRIVE_SIZE]=`expr ${DRIVE_SIZES[$DRIVE_SIZE]} + 1`
I have not initialized the DRIVE_SIZES array to 0. So above line might not work.
I would like to initialize a sparse array in bash script.
Lets say all drives in the host are of the same size, except one. Some 10 drives are of size 468851544 and one drive is of size 268851544. So I cannot initialize all index from 0-468851544 because I dont know the maximum disk size beforehand.
So is there a way to initialize such an sparse array to 0. May be if there is way to declare an integer array in bash that might help. But after some initial research found out I can declare an integer, but not integer array(might be wrong on this). Can someone help me with this ?
I read this, but this might not be the solution to me

Use increment in an arithmetic expression:
(( ++DRIVE_SIZES[DRIVE_SIZE] ))

You can use parameter substitution to put a zero into the expression when the array key has not been defined yet:
DRIVE_SIZES[$DRIVE_SIZE]=`expr ${DRIVE_SIZES[$DRIVE_SIZE]:-0} + 1`
N.B. this is untested, but should be possible.

An array element when unset and used in arithmetic expressions inside (( )) and $(( )) has a default value of 0 so an expression like this would work:
(( DRIVE_SIZES[DRIVE_SIZE] = DRIVE_SIZES[DRIVE_SIZE] + 1 ))
Or
(( DRIVE_SIZES[DRIVE_SIZE] += 1 ))
Or
(( ++DRIVE_SIZES[DRIVE_SIZE] ))
However when used outside (( )) or $(( )), it would still expand to an empty message:
echo ${DRIVE_SIZES[RANDOM]} shows "" if RANDOM turns out to be an index of an element that is unset.
You can however use $(( )) always to get the proper presentation:
echo "$(( DRIVE_SIZES[RANDOM] ))" would return either 0 or the value of an existing element, but not an empty string.
Using -i to declare, typeset or local when declaring arrays or simple variables might also help since it would only allow those parameters to have integral values, and the assignment is always done as if it's being assigned inside (( )) or $(( )).
declare -i VAR
A='1 + 2'
is the same as (( A = 1 + 2 )).
And with (( B = 1 )), A='B + 1' sets A to 2.
For arrays, you can set them as integral types with -a:
declare -a -i ARRAYVAR
So a solution is to just use an empty array variable and that would be enough:
ARRAYVAR=()
Or
declare -a -i ARRAYVAR=()
Just make sure you always use it inside (( )) or $(( )).

Related

Copy a bash associative array using eval statement

Before we get into the question, I know there are answers SIMILAR to this on stack overflow already. However this one is unique in it's use of the eval statement with associative arrays. ( Believe me I've read them all ).
Okay now into the question
I have X number of arrays defined via an eval function similar to this:
for (( i=1;i<=X;i++ ))
do
eval "declare -gA old$i"
eval "old$i[key]=value"
done
This code is in function : makeArrays
Now I have a second function that must loop through these different arrays
old1
old2
.
.
.
oldX
I'll call this function : useArrays
Now, I have a for loop for this useArrays function.
for (( i=0;i<$#;i++ ))
do
// ACCESS OLD1[KEY]
done
My question is, how do I access this array FOR COMPARISONS.
I.E.
if[ old1 -eq 0 ]
then
...
fi
Is there a way I could COPY these associate arrays into a variable I can use for comparisons using eval as little as possible?
Modern versions of bash support namerefs (originally a ksh feature), so you can point a constant name at any variable you choose; this makes eval unnecessary for the purposes to which you're presently placing it.
key="the key you want to test"
for (( i=0;i<$#;i++ )); do
declare -n "oldArray=old$i" # map the name oldArray to old0/old1/old2/...
printf 'The value of %q in old%q is: %q\n' "$key" "$i" "${oldArray[$key]}"
unset -n "oldArray" # remove that mapping
done
You could of course refer to "${!oldArray[#]}" to iterate over its keys; also map a newArray namevar to compare with; etc.

Why do I get this strange output with an associative array in bash when I perform a manual bubble sort?

I have a bash associative array that looks like this
declare -A arraySalary=( [1]=1000 [8]=3000 [2]=2000)
I am learning bash scripting and am trying to implement a bubble sort on the array
with this piece of code
sortedDesc=false
while ! $sortedDesc ;
do
sortedDesc=true
for ((currentIndex=0; currentIndex<$((${#arraySalary[#]} -1)); currentIndex++))
do
if [[ ${arraySalary[$((currentIndex))]} -lt ${arraySalary[$((currentIndex + 1))]} ]]
then
sortedDesc=false
biggerNumber=${arraySalary[((currentIndex - 1))]}
arraySalary[$((currentIndex + 1))]=${arraySalary[$((currentIndex))]}
arraySalary[currentIndex]=${biggerNumber}
echo "swapped"
fi
done
done
echo "Printing new values"
# Print new values
for key in "${!arraySalary[#]}";
do
echo $key "->" ${arraySalary[$key]}
done
but the output I get is
swapped
swapped
Printing new values
0 -> 2000
1 -> 1000
2 ->
Can someone please explain why this is? Thanks
I don't know why you have chosen to use an associative array instead of an ordinary array, but you need to be aware that the index of an associative array is not a numeric context.
If arraySalary had been declared with -a instead of -A, then this would have been fine:
arraySalary[currentIndex]=${biggerNumber}
because the index of an ordinaey array is a numeric context, and in numeric contexts you can use variabke names without $. But since it is associative, what that does is set the element whose key is the string currentIndex. What you want is:
arraySalary[$currentIndex]=${biggerNumber}
It is not necessary to write $((currentIndex)); bash does not distinguish between integers and strings which contain the integer.
Sorting an array just by its value will be easy. In the case that you sort keys
by its value by using an assoc array, you need to introduce another array to hold the order of the elements because an assoc array is orderless.
Let's name the new array index which holds (1 8 2 ..) then the script will look like:
#!/bin/bash
declare -A arraySalary=([1]=1000 [8]=3000 [2]=2000)
declare -a index=("${!arraySalary[#]}")
sortedDesc=false
while ! $sortedDesc ;
do
sortedDesc=true
for ((i=0; i<$((${#index[#]} - 1)); i++))
do
if [[ ${arraySalary[${index[$i]}]} -lt ${arraySalary[${index[$(($i + 1))]}]} ]]
then
sortedDesc=false
biggerIndex=${index[$(($i + 1))]}
index[$(($i + 1))]=${index[$i]}
index[$i]=$biggerIndex
echo "swapped"
fi
done
done
echo "Printing new values"
# Print new values
for key in "${index[#]}";
do
echo $key "->" ${arraySalary[$key]}
done
which yields
swapped
swapped
swapped
Printing new values
8 -> 3000
2 -> 2000
1 -> 1000

Reversing a bash for loop

I have this:
for (( count= "$WP_RANGE_START"; count< "$WP_RANGE_STOP"+1; count=count+1 ));
Where WP_RANGE_STARTis a number like 1 and WP_RANGE_STOPis a number like 10.
Right now this will step though going 1,2,...10
How can I do so that it counts backwards?(10,9,...1)
I guess the mirror image of what you have would be
for (( count="$WP_RANGE_STOP"; count >= "$WP_RANGE_START"; count=count-1 ));
But a less cumbersome way to write it would be
for (( count=WP_RANGE_STOP; count >= WP_RANGE_START; count-- ));
The $ is unnecessary in arithmetic context.
When dealing with literals, bash has a range expansion feature using brace expansion:
for i in {0..10}; # or {10..0} or what have you
But it's cumbersome to use with variables, as the brace expansion happens before parameter expansion. It's usually easier to use arithmetic for loops in those cases.
Your incrementing code can be "simplified" as:
for count in $(eval echo {$WP_RANGE_START..$WP_RANGE_STOP});
So, to decrement you can just reverse the parameters"
for count in $(eval echo {$WP_RANGE_STOP..$WP_RANGE_START});
Assuming you've got a bash version of 3 or higher, you can specify an increment or decrement by appending it to the range, like so:
CHANGE=1
for count in $(eval echo {$WP_RANGE_STOP..$WP_RANGE_START..$CHANGE});
The for loop is your problem.
i=11 ; until [ $((i=i-1)) -lt 1 ] ; do echo $i ; done
OUTPUT
10
9
8
7
6
5
4
3
2
1
You don't need any bashisms at all.

How can I generate new variable names on the fly in a shell script?

I'm trying to generate dynamic var names in a shell script to process a set of files with distinct names in a loop as follows:
#!/bin/bash
SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'
for (( i = 1; i <= 2; i++ ))
do
echo SAMPLE{$i}
done
I would expect the output:
1-first.with.custom.name
2-second.with.custom.name
but i got:
SAMPLE{1}
SAMPLE{2}
Is it possible generate var names in the fly?
You need to utilize Variable Indirection:
SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'
for (( i = 1; i <= 2; i++ ))
do
var="SAMPLE$i"
echo ${!var}
done
From the Bash man page, under 'Parameter Expansion':
"If the first character of parameter is an exclamation point (!), a
level of variable indirection is introduced. Bash uses the value of
the variable formed from the rest of parameter as the name of the
variable; this variable is then expanded and that value is used in the
rest of the substitution, rather than the value of parameter itself.
This is known as indirect expansion."
The Problem
You're using the value of i as if it were an array index. It isn't, because SAMPLE1 and SAMPLE2 are separate variables, not an array.
In addition, when calling echo SAMPLE{$i} you are only appending the value of i to the word "SAMPLE." The only variable you are dereferencing in this statement is $i, which is why you got the results you did.
Ways to Address the Problem
There are two main ways to address this:
Multi-stage dereferencing of an interpolated variable, via the eval builtin or indirect variable expansion.
Iterating over an array, or using i as an index into an array.
Dereferencing with eval
The easiest thing to do in this situation is to use eval:
SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'
for (( i = 1; i <= 2; i++ )); do
eval echo \$SAMPLE${i}
done
This will append the value of i to the end of the variable, and then reprocess the resulting line, expanding the interpolated variable name (e.g. SAMPLE1 or SAMPLE2).
Dereferencing with Indirect Variables
The accepted answer for this question is:
SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'
for (( i = 1; i <= 2; i++ ))
do
var="SAMPLE$i"
echo ${!var}
done
This is technically a three-step process. First, it assigns an interpolated variable name to var, then dereferences the variable name stored in var, and finally expands the result. It looks a little cleaner, and some people are more comfortable with this syntax than with eval, but the result is largely the same.
Iterating Over an Array
You can simplify both the loop and the expansion by iterating over an array instead of using variable interpolation. For example:
SAMPLE=('1-first.with.custom.name' '2-second.with.custom.name')
for i in "${SAMPLE[#]}"; do
echo "$i"
done
This has added benefits over the other methods. Specifically:
You don't need to specify a complex loop test.
You access individual array elements via the $SAMPLE[$i] syntax.
You can get the total number of elements with the ${#SAMPLE} variable expansion.
Practical Equivalency for Original Example
All three methods will work for the example given in the original question, but the array solution provides the most overall flexibility. Choose whichever one works best for the data you have on hand.
You can use eval as shown below:
SAMPLE1='1-first.with.custom.name'
SAMPLE2='2-second.with.custom.name'
for (( i = 1; i <= 2; i++ ))
do
eval echo \$SAMPLE$i
done
Not as far as I know, They way #johnshen64 said. Also, you could solve your problem using an array like so:
SAMPLE[1]='1-first.with.custom.name'
SAMPLE[2]='2-second.with.custom.name'
for (( i = 1; i <= 2; i++ )) do
echo ${SAMPLE[$i]}
done
Note that you don't need to use numbers as indexes SAMPLE[hello] will work just as well
Not a standalone answer, just an addition to Miquel's answer which I couldn't fit well in a comment.
You can populate the array using a loop, the += operator, and a here document as well:
SAMPLE=()
while read; do SAMPLE+=("$REPLY"); done <<EOF
1-first.with.custom.name
2-second.with.custom.name
EOF
In bash 4.0, it's as simple as
readarray SAMPLE <<EOF
1-first.with.custom.name
2-second.with.custom.name
EOF
eval "echo $SAMPLE${i}"

Problem with bash code

function dec_to_bin {
if [ $# != 2 ]
then
return -1
else
declare -a ARRAY[30]
declare -i INDEX=0
declare -i TEMP=$2
declare -i TEMP2=0
while [ $TEMP -gt 0 ]
do
TEMP2="$TEMP%2"
#printf "%d" "$TEMP2"
ARRAY[$INDEX]=$TEMP2
TEMP=$TEMP/2
INDEX=$[ $INDEX + 1 ] #note
done
for (( COUNT=INDEX; COUNT>-1; COUNT--)){
printf "%d" "${ARRAY[$COUNT]}" <<LINE 27
#echo -n ${ARRAY[$COUNT]} <<LINE 28
}
fi
}
why is this code giving this error
q5.sh: line 27: ARRAY[$COUNT]: unbound variable
same error comes with line 28 if uncommented
One more question, I am confused with the difference b/w '' and "" used in bash scripting any link to some nice article will be helpfull.
It works fine for me except that you can't do return -1. The usual error value is 1.
The error message is because you have set -u and you're starting your for loop at INDEX instead of INDEX-1 (${ARRAY[INDEX]} will always be empty because of the way your while loop is written). Since you're using %d in your printf statement, empty variables will print as "0" (if set -u is not in effect).
Also, it's meaningless to declare an array with a size. Arrays in Bash are completely dynamic.
I would code the for loop with a test for 0 (because the -1 looks confusing since it can't be the index of an numerically indexed array):
for (( COUNT=INDEX - 1; COUNT>=0; COUNT--))
This form is deprecated:
INDEX=$[ $INDEX + 1 ]
Use this instead:
INDEX=$(( $INDEX + 1 ))
or this:
((INDEX++))
I also recommend using lower case or mixed case variables as a habit to reduce the chance of variable name collision with shell variables.
You're not using $1 for anything.

Resources