Let's say I have an array of n elements. Each element is a string of comma-separated x,y coordinate pairs, e.g. "581,284". There is no set character length to these x,y values.
Say I wanted to subtract 8 from each x value, and 5 from each y value.
What would be the simplest way to modify x and y, independently of each other, without permanently splitting the x and y values apart?
e.g the first array element "581,284" becomes "573,279", the second array element "1013,562" becomes "1005,274", and so forth.
I worked on this problem for a couple of hours (I'm an amateur at bash), and it seemed as if my approach was awfully convoluted.
Please note that the apostrophes above are only added for emphasis, and are not a part of the problem.
Thank you in advance, I've been racking my head over this for a while now!
Edit: The following excerpt is the approach I was taking. I don't know much about bash, as you can tell.
while read value
do
if [[ -z $offset_list ]]
then
offset_list="$value"
else
offset_list="$offset_list,$value"
fi
done < text.txt
new_offset=${offset_list//,/ }
read -a new_array <<< $new_offset
for value in "${new_array[#]}"
do
if [[ $((value%2)) -eq 1 ]]
then
value=$((value-8));
new_array[$counter]=$value
counter=$((counter+1));
elif [[ $((value%2)) -eq 0 ]]
then
value=$((value-5));
new_array[$counter]=$value
counter=$((counter+1));
fi
done
Essentially I had originally read the coordinate pairs, and stripped the commas from them, and then planned on modifying odd/even values which were populated into the new array. At this point I realized that there had to be a more efficient way.
I believe the following should achieve what you are looking for:
#!/bin/bash
input=("581,284" "1013,562")
echo "Initial array ${input[#]}"
for index in ${!input[#]}; do
value=${input[$index]}
x=${value%%,*}
y=${value##*,}
input[$index]="$((x-8)),$((y+5))"
done
echo "Modified array ${input[#]}"
${!input[#]} allows us to loop over the indexes of the bash array.
${value%%,*} and ${value##*,} relies on bash parameter substitution to remove the everything after or before the comma (respectively). This effectively splits your string into two variables.
From there, it's your required math and variable reassignment to mutate the array.
Related
I am pulling my hair out manipulating arrays in bash. I have an array of strings, which contain spaces. I would like an array containing all but the first element of my input array.
input=("first string" "second string" "third string")
echo ${#input[#]}
# len(input)=3
# get slice of all except for first element of input
slice=${input[#]:1}
echo ${#slice[#]}
# expect 2, but get 1
echo $slice
# second string third string
# slice should contain ("second string" "third string"), but instead is "second string third string"
Slicing the array clearly works to eliminate the first element, but the result appears to be a concatenation of all remaining strings, rather than an array. Is there a way to slice an array in bash and get an array as a result?
(sorry, I'm not new to bash, but I've never used it for much before, and I can't find any documentation showing why my slice is flattened)
First off, you should always quote variable expansions. Be very wary of any solution that relies on unquoted expansions. ShellCheck.net is a great tool for catching bugs related to quoting (among many other issues).
To your specific issue, slice=${input[#]:1} does not do what you want. It defines a single scalar variable slice rather than an array, meaning the array expansion (denoted by the [#]) will first be munged into a single string using the current IFS. Here's a demo:
$ arr=(1 2 '3 4')
$ IFS=,
$ var="${arr[#]:1}"
$ echo "$var"
2,3 4
To instead declare and populate an array use the =() notation, like so:
$ var=("${arr[#]:1}")
$ printf '%s\n' "${var[#]}"
2
3 4
Indexes are reset, element 1 is now element 0:
slice=("${input[#]:1}")
Element and index are removed, the first element is now index 1, not index 0:
unset input[0]
${#slice[#]} or ${#input[#]} will now be 1 less than the previous value of ${#input[#]}. Starting out with three elements in slice, the values of "${!slice[#]}" and "${!input[#]}", will be 0 1 and 1 2 respectively (for either the first or second approach)
If you don't quote slice=("${input[#]:1}"), each array element is split on whitespace, creating many more elements.
my-script.awk
#!/env/bin awk
BEGIN {
toggleValues="U+4E00,U+9FFF,U+3400,U+4DBF,U+20000,U+2A6DF,U+2A700,U+2B73F,U+2B740,U+2B81F,U+2B820,U+2CEAF,U+F900,U+FAFF"
split(toggleValues, boundaries, ",")
if ("U+4E00" in boundaries) {print "inside"}
}
Run
echo ''| awk -f my-script.awk
Question
Why I don't see inside printed?
awk stores arrays differently then what you expect. It's a key/value pair with the key (from split() is the integer index starting at 0 and the value is the string that was split() it into that element.
The awk in condition tests keys, not values. So your "U+4E00" in boundaries condition isn't going to pass. Instead you'll need to iterate your array and look for the value.
for (boundary in boundaries) { if(boundaries[boundary] == "U+4E00") { print "inside" }
Either that or you can create a new array based on the existing one, but with the values stored as the key so the in operator will work as is.
for (i in boundaries) {boundaries2[boundaries[i]] = ""}
if ("U+4E00" in boundaries2){print "inside"}
This second method is a little hackey since all your element values are set to "", but it's useful if you are going to iterate through large file and just want to use the in operator to test that a field is in your array (as opposed to iterating the array on each record, which might be more expensive).
There is this typical problem: given a list of values, check if they are present in an array.
In awk, the trick val in array does work pretty well. Hence, the typical idea is to store all the data in an array and then keep doing the check. For example, this will print all lines in which the first column value is present in the array:
awk 'BEGIN {<<initialize the array>>} $1 in array_var' file
However, it is initializing the array takes some time because val in array checks if the index val is in array, and what we normally have stored in array is a set of values.
This becomes more relevant when providing values from command line, where those are the elements that we want to include as indexes of an array. For example, in this basic example (based on a recent answer of mine, which triggered my curiosity):
$ cat file
hello 23
bye 45
adieu 99
$ awk -v values="hello adieu" 'BEGIN {split(values,v); for (i in v) names[v[i]]} $1 in names' file
hello 23
adieu 99
split(values,v) slices the variable values into an array v[1]="hello"; v[2]="adieu"
for (i in v) names[v[i]] initializes another array names[] with names["hello"] and names["adieu"] with empty value. This way, we are ready for
$1 in names that checks if the first column is any of the indexes in names[].
As you see, we slice into a temp variable v to later on initialize the final and useful variable names[].
Is there any faster way to initialize the indexes of an array instead of setting one up and then using its values as indexes of the definitive?
No, that is the fastest (due to hash lookup) and most robust (due to string comparison) way to do what you want.
This:
BEGIN{split(values,v); for (i in v) names[v[i]]}
happens once on startup and will take close to no time while this:
$1 in array_var
which happens once for every line of input (and so is the place that needs to have optimal performance) is a hash lookup and so the fastest way to compare a string value to a set of strings.
not an array solution but one trick is to use pattern matching. To eliminate partial matches wrap the search and array values with the delimiter. For your example,
$ awk -v values="hello adieu" 'FS values FS ~ FS $1 FS' file
hello 23
adieu 99
I'm trying to assign an array of three values to a variable if it hasn't been assigned yet with the line
: ${SEAFILE_MYSQL_DB_NAMES:=(ccnet-db seafile-db seahub-db)}
Unfortunately, echoing ${SEAFILE_MYSQL_DB_NAMES[#]} results in (ccnet-db seafile-db seahub-db) and ${SEAFILE_MYSQL_DB_NAMES[2]} prints nothing. It seems, the value has been interpreted as a string and not as an array. Is there any way I can make my script assign an array this way?
How about doing it in several stages? First declare the fallback array, then check if SEAFILE_MYSQL_DB_NAMES is set, and assign if needed.
DBS=(ccnet-db seafile-db seahub-db)
[[ -v SEAFILE_MYSQL_DB_NAMES ]] || read -ra SEAFILE_MYSQL_DB_NAMES <<< ${DBS[#]}
Based on this answer.
Suppose I have 3 arrays, A, B and C
I want to do the following:
A=("1" "2")
B=("3" "4")
C=("5" "6")
for i in $A $B $C; do
echo ${i[0]} ${i[1]}
#process data etc
done
So, basically i takes the value of the whole array each time and I am able to access the specific data stored in each array.
On the 1st loop, i should take the value of the 1st array, A, on the 2nd loop the value of array B etc.
The above code just iterates with i taking the value of the first element of each array, which clearly isn't what I want to achieve.
So the code only outputs 1, 3 and 5.
You can do this in a fully safe and supportable way, but only in bash 4.3 (which adds namevar support), a feature ported over from ksh:
for array_name in A B C; do
declare -n current_array=$array_name
echo "${current_array[0]}" "${current_array[1]}"
done
That said, there's hackery available elsewhere. For instance, you can use eval (allowing a malicious variable name to execute arbitrary code, but otherwise safe):
for array_name in A B C; do
eval 'current_array=( "${'"$array_name"'[#]}"'
echo "${current_array[0]}" "${current_array[1]}"
done
If the elements of the arrays don't contain spaces or wildcard characters, as in your question, you can do:
for i in "${A[*]}" "${B[*]}" "${C[*]}"
do
iarray=($i)
echo ${iarray[0]} ${iarray[1]}
# process data etc
done
"${A[*]}" expands to a single string containing all the elements of ${A[*]}. Then iarray=($i) splits this on whitespace, turning the string back into an array.