Bash Array value in default value assignment - arrays

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.

Related

bash 5.1.4 associative array with period in index works for one statement but not in another

As the title says, my index with a period in the name is properly set in one call to the function but not when referencing the index to assign a value. I only found one other question and the answer was to declare the array as associative. (Which I already had.)
The relevant part of the script:
#!/usr/bin/env bash
declare -Ag root_dict sub_dict1 sub_dict2
object=0.7.0-rc1 #parsed from file
_set_key $object
# called from another part of script
_set_key() {
if [[ -z $last_obj ]]; then # first object parsed.
root_dict_key="$object"
last_obj="$object"
last_key="$object"
object=""
root_dict[$root_dict_key]="" # i haven't parsed the value yet
last_dict=root_dict
last_key=root_dict_key
return
elif [[ -n $sub_dict ]]; then
# parsed another object to get the array value.
# value is also a key, to reference another array.
object=files # argument sent with caller
# indirect referencing because the referenced array \
dict_key="${!last_dict}" # is dynamic
dict_key[$last_key]="$object"
last_obj="$object"
last_val="$object"
object=""
return
fi
}
The first if statement properly sets the key. root_dict[0.7.0-rc1]=""
When I go to set the value for that key in the next call to _set_key and the elif statement I get:
line 136: 0.7.0-rc2: syntax error: invalid arithmetic operator (error token is ".7.0-rc2")
I know variable names can't have punctuation except for the underscore, but the other answer suggested that associative array indices are string literals. Why would the index get properly set in the first call but return the error in the second call? The caller is the same in both instances first sending object=0.7.0-rc1 and then object=files.
Looking at your code maybe you wanted to write
dict_key="${!last_dict}"
root_dict_key[$last_key]="$object"
... instead of ...
dict_key="${!last_dict}"
dict_key[$last_key]="$object"
dict_key was not declare as an associative array. When bash sees the assignment dict_key[$last_key]="$object" (note the […]) then the non-associative dict_key is interpreted as a regular array.
The braces of regular arrays open an arithmetic context. Bash arithmetic contexts cannot interpret floating point numbers, hence the error message.

Simplest way to modify comma-separated values in an array?

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.

How to test value is inside array with awk

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).

How to slice a variable into array indexes?

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

Access 'cat' output as an array in 'bash'

I am trying to access some numeric values that a regular 'cat' outputs in an array.
If I do: cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies
I get: 51000 102000 204000 312000 ...
So I scripted as below to get all elements in an array and I tried to get the number of elements.
vAvailableFrequencies=$(sudo cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies)
nAvailableFrequencies=${#vAvailableFrequencies[#]}
The problem is that nAvailableFrequencies is equal to the number of characters in the array, not the number of elements.
The idea is to be able to access each element as:
for (( i=0;i<$nAvailableFrequencies;i++)); do
element=${vAvailableFrequencies[$i]
done
Is this possible in bash without doing something like a sequential read and inserting elements in the array one by one?
You can use array like this:
arr=($(</sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies))
nAvailableFrequencies=${#arrr}
$(<file) reads and outputs a file content while (...) creates an array.
You just need another set of brackets around the vAvailableFrequencies assignment:
vAvailableFrequencies=($(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies))
nAvailableFrequencies=${#vAvailableFrequencies[#]}
Now you can access within your for loop, or individually with ${vAvailableFrequencies[i]} where i is the number of an element
If you are using bash 4 or later, use
readfile -t vAvailableFrequencies < /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies
(or, if you need to use sudo,
readfile -t vAvailableFrequencies < <(sudo cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies)
)

Resources