How to modify 2d array in shell script - arrays

I have the following sample code for my shell script:
#!/bin/bash
x[1,1]=0
x[2,1]=1
echo "x[1,1]=${x[1,1]}"
echo "x[2,1]=${x[2,1]}"
for i in {1..2}; do
x[$i,1]=${i}
echo "loop$i x[$i,1]=${i}"
done
echo "x[1,1]=${x[1,1]}"
echo "x[2,1]=${x[2,1]}"
and I am expecting for x[1,1] to have the value of 1 and x[2,2] to have the value of 2.
But when I run the script the result is:
$ ./test3.sh
x[1,1]=1
x[2,1]=1
loop1 x[1,1]=1
loop2 x[2,1]=2
x[1,1]=2
x[2,1]=2
I expect x[1,1] to retain the value of 1 but it happens to be 2 now. Is there something wrong with my script?

Bash does not have 2-D arrays. The best you can do is emulate them with associative arrays.
Add the following line to the beginning of your script:
declare -A x
This makes x into an associative array. When that is done, the script produces the output that you expect:
$ bash script
x[1,1]=0
x[2,1]=1
loop1 x[1,1]=1
loop2 x[2,1]=2
x[1,1]=1
x[2,1]=2
Bash indexed arrays
Unless declare -A is used, a bash array is just an indexed array. Let's define y as an indexed array:
$ y=()
Now, let's assign two values:
$ y[2,3]=1
$ y[22,3]=2
Now, let's use declare -p to find out what the contents of the array really are:
$ declare -p y
declare -a y='([3]="2")'
As you can see, there is only y[3]. The reason is that the index in an indexed array is subject to arithmetic expansion and, when given a list of comma-separated values, arithmetic expansion returns just the last one.
In other words, as far as bash is concerned, assignments to y[2,3] and y[22,3] are both just assignments to y[3]. The second assignment overwrites the first.
We can see this directly if we echo the results of arithmetic expansion:
$ echo $((3))
3
$ echo $((2,3))
3
$ echo $((22,3))
3
When given a list of comma-separated values, arithmetic expansion returns the last one. This is true even if the comma-separated list is a long one:
$ echo $((1+2,3*4,5,6,7,8))
8
It is always the last value which is returned.
Bash associative arrays
Let's examine what happens with associative arrays. Let's define z as an associative array and assign some values to it:
$ declare -A z
$ z[1,2]=1
$ z[3,4]=2
$ z["Jim Bob"]=3
Now, let's see what was stored in z:
$ declare -p z
declare -A z='([3,4]="2" ["Jim Bob"]="3" [1,2]="1" )'
This seems to be what you need.

Related

Bash shift changes expected value of associative array

I made a simple script, that matches the positional arguments starting with dash, and saves them into an associative array.
declare -A opts
for i; do
[[ "$i" =~ - ]] && opts[$i]=1
done
shift "${#opts[*]}"
echo "opts: ${opts[*]}"
echo "!opts: ${!opts[-d]}"
echo "Query: $*"
For the call ./script -d hello world the output is:
opts: 1
!opts: hello
Query: hello world
Which is unexpected, since the key of ${!opts[-d]} is supposed to be -d itself if defined. This behavior happens because of the shift command, when it is removed from the code, the output is as expected:
opts: 1
!opts: -d
Query: -d hello world
Why does shift interfere with the created associative array?
The associative array isn't being changed; you can verify this by putting a declare -p opts after the shift, which will give
declare -A opts=([-d]="1" )
The problem: ${!opts[-d]} doesn't do what you seem to expect. First, opts[-d] is looked up, which is the value 1. Then the ! means that's used as the name of a variable to substitute - so it's effectively the same as $1, which, after the shift, is hello.
If you want to print out all the keys of the associative array, use ${!opts[#]}. The leading ! has two different meanings in bash paramater expansion depending on if used with an array with # or * in the brackets, or an index/normal variables.

Bash Array not sorting correctly

I need to order these 2 arrays, I don't care about the format of the output, I only need it to be ordered in order to compare them but this doesn't seem to work, although it works with simpler text. I also tried removing the --field-separator='"'
DIG_1=("sampletext""zzz""ms=ms91608007""asdas")
DIG_2=("zzz""ms=ms91608007""sampletext""asdas")
echo "unsorted:"
echo ${DIG_1[*]}
echo ${DIG_2[*]}
IFS=$'\n' sorted=($(sort --field-separator='"' <<<"${DIG_1[*]}")); unset IFS
IFS=$'\n' sorted2=($(sort --field-separator='"' <<<"${DIG_2[*]}")); unset IFS
echo "sorted:"
echo ${sorted[*]}
echo ${sorted2[*]}
And the output I get is:
unsorted:
sampletextzzzms=ms91608007asdas
zzzms=ms91608007sampletextasdas
sorted:
sampletextzzzms=ms91608007asdas
zzzms=ms91608007sampletextasdas
How Can I fix this? I want it to be, for example:
unsorted:
sampletextzzzms=ms91608007asdas
zzzms=ms91608007sampletextasdas
sorted:
asdasms=ms91608007sampletextzzz
asdasms=ms91608007sampletextzzz
There's no reason to use an array to store one element.
Since you need to keep the double quotes, you need to make efforts to preserve them:
DIG_1='"sampletext""zzz""ms=ms91608007""asdas"'
Otherwise the double quotes will be removed by the shell: 3.5.9 Quote Removal
When you use VAR=value some_command, that variable is only
set for the duration of some_command -- bash puts that variable in the environment for the command, not into the shell's own catalog of variables. Subsequently unsetting the
variable is not required -- unsetting the IFS variable is potentially harmful
for the rest of the program
sort won't sort the fields within a record, it's for sorting records against each other.
To accomplish what you want, this will do:
sorted_1=$(grep -Po '(?<=").*?(?=")' <<<"$DIG_1" | sort | paste -s -d "")
As anubhava mentioned in the comments, the current code is creating arrays of single values, ie:
$ DIG_1=("sampletext""zzz""ms=ms91608007""asdas")
$ typeset -p DIG_1
declare -a DIG_1=([0]="sampletextzzzms=ms91608007asdas")
$ DIG_2=("zzz""ms=ms91608007""sampletext""asdas")
$ typeset -p DIG_2
declare -a DIG_2=([0]="zzzms=ms91608007sampletextasdas")
Assuming the OP really does want an array, and that the array elements will be utilized in later code, we need a way to delimit the items of the array, and the easiest way to do this is with some white space, eg:
$ DIG_1=("sample text" "zzz" "ms=ms91608007" "asdas")
$ typeset -p DIG_1
declare -a DIG_1=([0]="sample text" [1]="zzz" [2]="ms=ms91608007" [3]="asdas")
$ DIG_2=("zzz" "ms=ms91608007" "sample text" "asdas")
$ typeset -p DIG_2
declare -a DIG_2=([0]="zzz" [1]="ms=ms91608007" [2]="sample text" [3]="asdas")
NOTE: I've added a single space to change "sampletext" to "sample text" so that we can see how a space is treated a) as part of the data vs b) as a delimiter.
NOTE: Assuming OPs code is generating the questionable array assignments (eg, DIG_1=("sampletext""zzz""ms=ms91608007""asdas")), it may make more sense to look into ways to 'fix' the array generator than to complicate the code by trying to figure out how to treat these single strings as a 4-part array definition.
Also, since the sample output (current vs desired) shows no double quotes I'm guessing this means the double quotes are not part of the actual data but rather just delimiters.
Now that we have an actual array of elements we can look at sorting the arrays and storing the results into additional (sorted) arrays, eg:
$ IFS=$'\n' sorted=($(printf "%s\n" "${DIG_1[#]}" | sort))
$ typeset -p sorted
declare -a sorted=([0]="asdas" [1]="ms=ms91608007" [2]="sample text" [3]="zzz")
$ IFS=$'\n' sorted2=($(printf "%s\n" "${DIG_2[#]}" | sort))
$ typeset -p sorted2
declare -a sorted2=([0]="asdas" [1]="ms=ms91608007" [2]="sample text" [3]="zzz")
At this point we now have 2 sets of arrays ... 1) original data (DIG_1[#] and DIG_2[#]) and 2) sorted (sorted[#] and sorted2[#]).
The OP can then slice-n-dice the data as desired, as well as print the contents of the arrays in any desired format, eg:
# print array elements on a single line with no delimiters, storing the results
# in variables for later use/comparison/display
$ printf -v srt "%s" "${sorted[#]}"
$ typeset -p srt
declare -- srt="asdasms=ms91608007sample textzzz"
$ echo "${srt}"
asdasms=ms91608007sample textzzz
$ printf -v srt2 "%s" "${sorted2[#]}"
$ typeset -p srt2
declare -- srt2="asdasms=ms91608007sample textzzz"
$ echo "${srt2}"
asdasms=ms91608007sample textzzz

Copy elements from an array to another preserving embedded spaces

Consider having an array which is as follows:
array=("BMW E46" "Ford Mustang" "Toyota GT86")
Running the following command proves that it has 3 elements:
echo ${#array[#]} # outputs 3
Now I will remove an element from the initial array:
unset array[0]
While this will remove the element from my array, the indices will remain the same:
echo ${!array[#]} # output: 1 2
My way of dealing with this problem (normally) is to just type array=(${array[#]}) but in this case, running the command will result in my elements being parsed as ('Ford' 'Mustang' ...).
Is there a way of fixing the indices without messing up the array elements?
Enclose ${array[#]} in double-quotes so that each element will be retained as a separate field.
$ array=("BMW E46" "Ford Mustang" "Toyota GT86")
$ unset array[0]
$ array=("${array[#]}")
$ declare -p array
declare -a array=([0]="Ford Mustang" [1]="Toyota GT86")

How can I see which values exists in Bash array?

Given an array (in Bash), is there a command that prints the contents of the array according to indices?
Something like that: arr[0]=... , arr[1]=... ,...
I know that I can print it in a for loop, but I am looking for a command that does it.
Given an array with contiguous indices starting at 0:
$ arr=(one two three)
And non-contiguous indices:
$ declare -a arr2='([0]="one" [2]="two" [5]="three")'
You can print the values:
$ echo ${arr[*]} # same with arr2
one two three
Or, use a C style loop for arr:
$ for (( i=0;i<${#arr[#]};i++ )); do echo "arr[$i]=${arr[$i]}"; done
arr[0]=one
arr[1]=two
arr[2]=three
But that won't work for arr2.
So, you can expand the indices (contiguous or not) and print index, value like so:
$ for i in "${!arr2[#]}"; do echo "arr2[$i]=${arr2[$i]}"; done
arr2[0]=one
arr2[2]=two
arr2[5]=three
Or inspect it with declare -p:
$ declare -p arr
declare -a arr='([0]="one" [1]="two" [2]="three")'
Which also works if the array has non-contiguous indices (where the C loop would break):
$ declare -p arr2
declare -a arr2='([0]="one" [2]="two" [5]="three")'
Note: A common mistake is to use the sigil $ and thinking you are addressing that array or named value. It is the unadorned name that is used since the sigil will dereference and tell details of the name contained instead of that name:
$ k=arr
$ declare -p $k
declare -a arr='([0]="one" [1]="two" [2]="three")' # note this is 'arr' , not 'k'
$ declare -p k
declare -- k="arr"
Since declare with no arguments will print the entire Bash environment at that moment, you can also use utilities such as sed grep or awk against that output:
$ declare | grep 'arr'
arr=([0]="one" [1]="two" [2]="three")
k=arr

Bash array saving

The think is that I want to save something to an array in bash. The point is that I want one name of file for one array. So I don't know how much arrays I will have.
#!/bin/bash
declare -A NAMES
index=0
for a in recursive.o timeout.o print_recursive.o recfun.o
do
NAMES[$index]=$a
index=$((index+1))
echo ${NAMES[ $index ]}
done
When I run script with -x I can see that NAMES[$index], the index is not there represented as number so the whole thing doesn't work.
The error is at lines 7 and 8. Swap them and it will work.
When index have value 0 you set NAMES[0]=recursive.o, then increment index and print NAMES[1] which not set. And same thing for another elements. Because that there is no output.
Your loop should looks like this:
for a in recursive.o timeout.o print_recursive.o recfun.o
do
NAMES[$index]=$a
echo ${NAMES[$index]}
index=$((index+1))
done
The problem is in the following:
declare -A NAMES
This makes an associative array NAMES. Quoting from help declare:
Options which set attributes:
-a to make NAMEs indexed arrays (if supported)
-A to make NAMEs associative arrays (if supported)
You needed to say:
declare -a NAMES
May be you are trying to do this:
#!/bin/bash
declare -a NAMES
for a in recursive.o timeout.o print_recursive.o recfun.o; do
NAMES+=( "$a" )
done
for (( x=0; x<${#NAMES[#]}; x++ )); do
echo "Index:$x has Value:${NAMES[x]}"
done
Output:
Index:0 has Value:recursive.o
Index:1 has Value:timeout.o
Index:2 has Value:print_recursive.o
Index:3 has Value:recfun.o
Accessing the index which is not set is throwing it off.
NAMES[$index]=$a #Setting up an array with index 0
index=$((index+1)) #Incrementing the index to 1
echo ${NAMES[ $index ]} #Accessing value of index 1 which is not yet set

Resources