Bash : Can Here Strings <<< work with multiple variables as input? - arrays

I am trying to initialize an array with multiple variables as below .
StringOne="This is a Test String"
StringTwo="This is a New String"
read -r -a Values <<< "$StringOne" "$StringTwo"
But It seems like array is getting values from only the first variable .ie StringOne
$ echo ${Values[0]}
This
$ echo ${Values[1]}
is
$ echo ${Values[2]}
a
$ echo ${Values[3]}
Test
${Values[4]}
String
$ echo ${Values[5]}
$ echo ${Values[6]}
$
What is wrong with this way of passing variable value for array initialization ? Cant we pass multiple variables with <<< operator ?

What is wrong with this way of passing variable value for array initialization ? Cant we pass multiple variables with <<< operator ?
Yes and no. The <<< operator takes one shell word as its operand, as is presented pretty clearly in its documentation. But you can combine the values of multiple variables in a single shell word by appropriate use of quoting:
StringOne="This is a Test String"
StringTwo="This is a New String"
read -r -a Values <<< "$StringOne $StringTwo"
echo "${Values[#]}"
Output:
This is a Test String This is a New String

Use:
read -r -a Values <<< "$StringOne"" ""$StringTwo"
Or:
read -r -a Values <<< "$StringOne"' '"$StringTwo"
Or:
out="$StringOne $StringTwo"
read -r -a Values <<< "$out"
Or some other variation on this theme.
This works because the string sent via <<< is the result of the expansion of what in on the right of it. We can make both variables act as one long string by concatenating them.

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 - Create arrays from lines of csv file, where first entry is array name

I'm learning to script in Bash.
I have an CSV file, which contains next lines:
numbers,one,two,three,four,five
colors,red,blue,green,yellow,white
custom-1,a,b,c,d,e
custom+2,t,y,w,x,z
Need to create arrays from this, where first entry is array name, eg.
number=(one,two,three,four,five)
colors=(red,blue,green,yellow,white)
custom-1=(a,b,c,d,e)
custom+2=(t,y,w,x,z)
Here is my script:
IFS=","
while read NAME VALUES ; do
declare -a $NAME
arrays+=($NAME)
IFS=',' read -r -a $NAME <<< "${VALUES[0]}"
done < file.csv
When I try with csv file, containing only two first string (numbers and colors), code works well. And if i try to with number, colors, custom-1, custom-2, there is error during reading csv:
./script.sh: line 5: declare: `custom-1': not a valid identifier
./script.sh: line 7: read: `custom+2': not a valid identifier
because bash does not allow special characters in variable names, as far as I understand. Is there any way to avoid this?
As you cannot use the first column of your CSV file as bash array names one option would be to generate valid names using a counter (e.g. arrayN). If you want to access your data using the values of this first column you would also need to store them somewhere with the corresponding counter value. An associative array (declare -A names=()) would be perfect. Last but not least, the namerefs (declare -n arr=..., available starting with bash 4.3) will be convenient to store and access your data. Example:
declare -i cnt=1
declare -A names=()
while IFS=',' read -r -a line; do
names["${line[0]}"]="$cnt"
declare -n arr="array$cnt"
unset line[0]
declare -a arr=( "${line[#]}" )
((cnt++))
done < foo.csv
Now, to access the values corresponding to, let's say, entry custom+2, first get the corresponding counter value, declare a nameref pointing to the corresponding array and voilĂ :
$ cnt="${names[custom+2]}"
$ declare -n arr="array$cnt"
$ echo "${arr[#]}"
t y w x z
Let's declare a function for easier access:
getdata () {
local -i cnt="${names[$1]}"
local -n arr="array$cnt"
[ -z "$2" ] && echo "${arr[#]}" || echo "${arr[$2]}"
}
And then:
$ getdata "custom+2"
t y w x z
$ getdata "colors"
red blue green yellow white
$ getdata "colors" 3
yellow

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

Bash, wihle read line by line, split strings on line divided by ",", store to array

I need to read file line by line, and every line split by ",", and store to array.
File source_file.
usl-coop,/root
usl-dev,/bin
Script.
i=1
while read -r line; do
IFS="," read -ra para_$i <<< $line
echo ${para_$i[#]}
((i++))
done < source_file
Expected output.
para_1[0]=usl-coop
para_1[1]=/root
para_2[0]=usl-dev
para_2[1]=/bin
Script will out error about echo.
./sofimon.sh: line 21: ${para_$i[#]}: bad substitution
When I echo array one by one field, for example
echo para_1[0]
it shows, that variables are stored.
But I need use it with variable within, something like this.
${para_$i[1]}
Is possible to do this?
Thanks.
S.
There is a trick to simulate 2D arrays using associative arrays. It works nice and I think is the most flexible and extensible:
declare -A para
i=1
while IFS=, read -r -a line; do
for j in ${!line[#]}; do
para[$i,$j]="${line[$j]}"
done
((i++)) ||:
done < source_file
declare -p para
will output:
declare -A para=([1,0]="usl-coop" [1,1]="/root" [2,1]="/bin" [2,0]="usl-dev" )
Without modifying your script that much you could use indirect variable expansion. It's sometimes used in simpler scripts:
i=1
while IFS="," read -r -a para_$i; do
n="para_$i[#]"
echo "${!n}"
((i++)) ||:
done < source_file
declare -p ${!para_*}
or basically the same with a nameref a named reference to another variable (side note: see how [#] needs to be part of the variable in indirect expansion, but not in named reference):
i=1
while IFS="," read -r -a para_$i; do
declare -n n
n="para_$i"
echo "${n[#]}"
((i++)) ||:
done < source_file
declare -p ${!para_*}
both scripts above will output the same:
usl-coop /root
usl-dev /bin
declare -a para_1=([0]="usl-coop" [1]="/root")
declare -a para_2=([0]="usl-dev" [1]="/bin")
That said, I think you shouldn't read your file into memory at all. It's just a bad design. Shell and bash is build around passing your files with pipes, streams, fifos, redirections, process substitutions, etc. without ever saving/copying/storing the file. If you have a file to parse, you should stream it to another process, parse and save the result, without ever storing the whole input in memory. If you want some data to find inside a file, use grep or awk.
Here is a short awk script that do the task.
awk 'BEGIN{FS=",";of="para_%d[%d]=%s\n"}{printf(of, NR, 0, $1);printf(of, NR, 1, $2)}' input.txt
Provide the desired output.
Explanation:
BEGIN{
FS=","; # set field seperator to `,`
of="para_%d[%d]=%s\n" # define common printf output format
}
{ # for each input line
printf(of, NR, 0, $1); # output for current line, [0], left field
printf(of, NR, 1, $2) # output for current line, [1], right field
}

bad array subscript error in associative array

I am trying to create a dictionary program in Bash with the following options : 1. Add a word
2. Update meaning
3. Print dictionary
4. Search a word
5. Search by keyword
For the same, I am creating 2 associative arrays, 1 to store the word - meaning and other to store words-keyword.
The problem is I am not able to store values in the array. Everytime I try to do it, it gives me an error
dict[$word]: bad array subscript
Here is the code for part 1
echo
echo -n "Enter a word : "
read $word
echo
echo -n "Enter it's meaning : "
read $meaning
echo
echo -n "Enter some keywords(with space in between) to describe the word : "
read $keyword
dict[$word]=$meaning
keywords[$word]=$keyword
;;
I also tried inserting the following code to remove new line as suggested in some posts but ended up with same result.
word=`echo $word | grep -s '\n'`
keyword=`echo $keyword | grep -s '\n'`
Have also tried the following way :
dict["$word"]="$meaning"
keywords["$word"]="$keyword"
;;
Output :
dict[$word]: bad array subscript
When reading a variable you preface the variable name with a $ (or wrap in $( and )).
When assigning a value to a variable you do not preface the variable name with a $.
In your example your 3x echo/read sessions are attempting to assign values to your variables, but you've prefaced your variables with $, which means your variables are not getting set as you expect; this in turn could be generating your error because $word is not set/defined (depends on version of bash).
You should be able to see what I mean with the following code snippet:
unset word
echo
echo -n "Enter a word : "
read $word
echo ".${word}."
What do you get as ouput? .. ? .<whatever_you_typed_in>. ?
You may also have a problem with your associative arrays (depending on bash version); as George has mentioned, you should play it safe and explicitly declare your associative arrays.
I would suggest editing your input script like such (remove leading $ on your read variables; explicitly declaring your associative arrays):
echo
echo -n "Enter a word : "
read word
echo
echo -n "Enter it's meaning : "
read meaning
echo
echo -n "Enter some keywords(with space in between) to describe the word : "
read keyword
# print some debug messages:
echo "word=.${word}."
echo "meaning=.${meaning}."
echo "keyword=.${keyword}."
# declare arrays as associative
declare -A dict keywords
# assign values to arrays
dict[$word]=$meaning
keywords[$word]=$keyword
# verify array indexes and values
echo "dict index(es) : ${!dict[#]}"
echo "dict value(s) : ${dict[#]}"
echo "keywords index(es): ${!keywords[#]}"
echo "keywords value(s) : ${keywords[#]}"
In my bash 4.4 , this is not raising any error but is not working correctly either:
$ w="one";m="two";d["$w"]="$m";declare -p d
declare -a d=([0]="two")
It is obvious that bash determines array d as a normal array and not as an associative array.
On the contrary, this one works fine:
$ w="one";m="two";declare -A d;d["$w"]="$m";declare -p d
declare -A d=([one]="two" )
According to bash manual, you need first to declare -A an array in order to be used as an associative one.

Resources