How to print out individual numbers after ordering an array in Bash? - bash

I'm new to bash and having some issues printing out individual numbers after sorting an array.
I have the following....
for x in ${array[#]}
do
echo $x
done| sort
This is the only way I was able to print out the entire array in order.
Now I'm trying to print out a single item after it's been ordered
so I tried....
for x in ${array[#]}
do
echo ${array[2]}
exit
done| sort
But it prints the third item in the unordered array array instead.
Any help?

You are printing the variable and then trying to sort the ONLY variable that you have printed (in your case ${array[2]})
Try this:
sorted=($(printf '%s\n' "${array[#]}"|sort))
echo ${sorted[2]}
This sorts the array and stores it in another array sorted

Related

Why is my for loop only storing all desired items as one element?

I am trying to figure out why all items are being stored as one element:
filedates=($dirPath/*.csv)
filebasenames=()
filedates2=()
for file in ${filedates[#]}; do
filebasenames+="${file##*/} "
done
for i in ${filebasenames[#]}; do
filedates2+="$(echo $i | cut -c6-13) "
done
for i in ${filedates2[#]}; do
echo $i
done
echo test here ${filebasenames[0]}
echo test here ${filebasenames[1]}
Im confused because the third for loop prints each element in a new line so I assumed that there is more than one element in the array but when I echo the "test here" line it shows me all the elements in one line indicating that there is only one large string. I verify that with the second echo test here 2 line
You aren't appending to the array; you are only appending to the first element of the array. Appending to an array requires parentheses. (Notice, too, that I've dropped the space from the new array element.)
for file in ${filedates[#]}; do
filebasenames+=("${file##*/}")
done
That said, you don't need a loop at all; you can apply the ## to all the elements of the array in one operation.
filebasenames=("${filedates[#]##*/}")
The other array is probably still best populated using a loop.
for i in "${filebasenames[#]}"; do
filedates2+=("$(echo "$i" | cut -c6-13)")
done

Bash. Associative array iteration (ordered and without duplicates)

I have two problems handling associative arrays. First one is that I can't keep a custom order on it.
#!/bin/bash
#First part, I just want to print it ordered in the custom created order (non-alphabetical)
declare -gA array
array["PREFIX_THIS","value"]="true"
array["PREFIX_IS","value"]="false"
array["PREFIX_AN","value"]="true"
array["PREFIX_ORDERED","value"]="true"
array["PREFIX_ARRAY","value"]="true"
for item in "${!array[#]}"; do
echo "${item}"
done
Desired output is:
PREFIX_THIS,value
PREFIX_IS,value
PREFIX_AN,value
PREFIX_ORDERED,value
PREFIX_ARRAY,value
But I'm obtaining this:
PREFIX_IS,value
PREFIX_ORDERED,value
PREFIX_THIS,value
PREFIX_AN,value
PREFIX_ARRAY,value
Until here the first problem. For the second problem, the order is not important. I added more stuff to the associative array and I just want to loop on it without duplicates. Adding this:
array["PREFIX_THIS","text"]="Text for the var"
array["PREFIX_IS","text"]="Another text"
array["PREFIX_AN","text"]="Text doesn't really matter"
array["PREFIX_ORDERED","text"]="Whatever"
array["PREFIX_ARRAY","text"]="More text"
I just want to loop over "PREFIX_THIS", "PREFIX_IS", "PREFIX_AN", etc... printing each one only once. I just want to print doing an "echo" on loop (order is not important for this part, just to print each one only once). Desired output:
PREFIX_ORDERED
PREFIX_AN
PREFIX_ARRAY
PREFIX_IS
PREFIX_THIS
I achieved it doing "dirty" stuff. But there must be a more elegant way. This is my working but not too much elegant approach:
already_set=""
var_name=""
for item in "${!array[#]}"; do
var_name="${item%,*}"
if [[ ! ${already_set} =~ "${var_name}" ]]; then
echo "${var_name}"
already_set+="${item}"
fi
done
Any help? Thanks.
Iteration Order
As Inian pointed out in the comments, you cannot fix the order in which "${!array[#]}" expands for associative arrays. However, you can store all keys inside a normal array that you can order manually.
keysInCustomOrder=(PREFIX_{THIS,IS,AN,ORDERED,ARRAY})
for key in "${keysInCustomOrder[#]}"; do
echo "do something with ${array[$key,value]}"
done
Unique Prefixes of Keys
For your second problem: a["key1","key2"] is the same as a["key1,key2"]. In bash, arrays are always 1D therefore there is no perfect solution. However, you can use the following one-liner as long as , is never part of key1.
$ declare -A array=([a,1]=x [a,2]=y [b,1]=z [c,1]=u [c,2]=v)
$ printf %s\\n "${!array[#]}" | cut -d, -f1 | sort -u
a
b
c
When your keys may also contain linebreaks delemit each key by null \0.
printf %s\\0 "${!array[#]}" | cut -zd, -f1 | sort -zu
Alternatively you could use reference variables to simulate 2D-arrays, however I would advice against using them.

Loop over two associative arrays in Bash

Say I have two associative arrays in Bash
declare -A a
declare -A b
a[xz]=1
b[xz]=2
a[zx]=3
b[zx]=4
I want to do something like this
for arr in ${a[#]} ${b[#]}; do echo ${arr[zx]}; done
and get 3 and 4 in output
but I get
$ for arr in ${a[#]} ${b[#]}; do echo ${arr[zx]}; done
1
3
2
4
Is there a way to do this in Bash?
You don't want to iterate over the contents; you want to iterate over the names of the arrays, then use indirect expansion to get the desired value of the fixed key from each array.
for arr in a b; do
t=$arr[zx] # first a[zx], then b[zx]
printf '%s\n' "${!t}"
done
Here, the variable "name" for use in indirect expansion is the name of the array along with the desired index.
Assuming the keys in both the arrays match(a major assumption), you can use one array as reference and loop over the keys and print in each array.
for key in "${!a[#]}"; do
printf "Array-1(%s) %s Array-2(%s) %s\n" "$key" "${a[$key]}" "$key" "${b[$key]}"
done
which produces an output as below. You can of-course remove the fancy debug words(Array-1, Array-2) which was added just for an understanding purpose.
Array-1(xz) 1 Array-2(xz) 2
Array-1(zx) 3 Array-2(zx) 4
One general good practice is always quote (for key in "${!a[#]}") your array expansions in bash, so that the elements are not subjected to word-splitting by the shell.

what does the ! mean in this expression: ${!mylist[#]}

I'm trying to understand a shell script written by a previous group member. there is this for loop. I can understand it's looping through a list ${!mylist[#]} but I've only seen ${mylist[#]} before, not ${!mylist[#]}.
What does the exclamation mark do here?
for i in ${!mylist[#]};
do
echo ${mylist[i]}
....
done
${!mylist[#]} returns the keys (or indices) to an an array. This differs from ${mylist[#]} which returns the values in the array.
As an example, let's consider this array:
$ arr=(abc def ghi)
In order to get its keys (or indices in this case):
$ echo "${!arr[#]}"
0 1 2
In order to get its values:
$ echo "${arr[#]}"
abc def ghi
From man bash:
It is possible to obtain the keys (indices) of an array as well
as the values. ${!name[#]} and ${!name[*]} expand to the indices
assigned in array variable name. The treatment when in double quotes
is similar to the expansion of the special parameters # and * within
double quotes.
Example using associative arrays
To show that the same applies to associative arrays:
$ declare -A Arr=([a]=one [b]=two)
$ echo "${!Arr[#]}"
a b
$ echo "${Arr[#]}"
one two

Element matching within the different lists

As usually :) I have two sets of input files with the same names but different extensions.
Using bash I made simple script which create 2 lists with identical elements names while looping 2 sets of the files from 2 dirs and using only it names w/o extensions as the elements of those lists:
#!/bin/bash
workdir=/data2/Gleb/TEST/claire+7d4+md/water_analysis/MD1
traj_all=${workdir}/tr_all
top_all=${workdir}/top_all
#make 2 lists for both file types
Trajectories=('');
Topologies=('');
#looping of 1st input files
echo "Trr has been found in ${traj_all}:"
for tr in ${traj_all}/*; do # ????
tr_n_full=$(basename "${tr}")
tr_n="${tr_n_full%.*}"
Trajectories=("${Trajectories[#]}" "${tr_n}");
done
#sort elements within ${Trajectories[#]} lists!! >> HERE I NEED HELP!
#looping of 2nd files
echo "Top has been found in ${top_all}:"
for top in ${top_all}/*; do # ????
top_n_full=$(basename "${top}")
top_n="${top_n_full%.*}"
Topologies=("${Topologies[#]}" "${top_n}");
done
#sort elements within ${Topologies[#] lists!! >> HERE I NEED HELP!
#make input.in file for some program- matching of elements from both lists >> HERE I NEED HELP!
for i in $(seq 1 ${#Topologies[#]}); do
printf "parm $top_all/${Topologies[i]}.top \ntrajin $traj_all/${Trajectories[i]}.mdcrd\nwatershell ${Area} ${output}/watershell_${Topologies[i]}_${Area}.dat > output.in
done
I'd thankful if someone provide me with good possibility how to improve this script:
1) I need to sort elements in both lists in the similar pattern after last elements have been added in each of them;
2) I need to add some test on the LAST step of the script which will create final output.in file only in case if elements are the same (in principle in this case it always should be the same!) in both lists which are matched during this operation by printf.
Thanks for the help,
Gleb
Here's a simpler way to create the arrays:
# Create an empty array
Trajectories=();
for tr in "${traj_all}"/*; do
# Remove the string of directories
tr_base=${tr##*/}
# Append the name without extension to the array
Trajectories+="${tr_base%.*}"
done
In bash, this will normally result in a sorted list, because the expansion of * in the glob is sorted. But you can sort it with sort; it is simplest if you are certain there are no newlines in the filenames:
mapfile -t sorted_traj < <(printf %s\\n "${Trajectories[#]}" | sort)
To compare two sorted arrays, you could use join:
# convenience function; could have helped above, too.
lines() { printf %s\\n "$#"; }
# Some examples:
# 1. compare a with b and print the lines which are only in a
join -v 1 -t '' <(lines "${a[#]}") <(lines "${b[#]}")
# 2. create c as an array with the lines which are only in b
mapfile -t c < <( join -v 2 -t '' <(lines "${a[#]}") <(lines "${b[#]}") )
If you create both difference lists, then the two arrays are equal if both lists are empty. If you are expecting both arrays to be the same, though, and if this is time-critical (probably not), you could do a simple precheck:
if [[ "${a[*]}" = "${b[*]}" ]]; then
# the arrays are the same
else
# the arrays differ; do some more work to see how.
fi

Resources