BASH copy of array - arrays

I am new to BASH.
I have string with name ARRAY, but i need set ARRAY as array, and this ARRAY as array must include parts of string from ARRAY as string separated by \n (new line)
This is what I have:
ARRAY=$'one\ntwo';
x=$ARRAY;
IFS=$'\n' read -rd '' -a y <<<"$x";
y=(${x//$'\n'/});
IFS=$'\n' y=(${x//$'\n'/ });
IFS=$'\n' y=($x);
unset ARRAY; (i try unset ARRAY)
ARRAY=$y; (this not works correctrly)
echo ${ARRAY[1]}; //result ARRAY[0]="one",ARRAY[1]=""
But if I try echo ${y[1]}; //all is right y[0]="one" y[1]="two"
My problem is that I cannot set ARRAY as copy of y array..

The way you're splitting the string at the newlines is correct:
array=$'one\ntwo'
IFS=$'\n' read -rd '' -a y <<<"$array"
Now, why do you give a different name, if eventually you want the variable array to contain the array? just do:
IFS=$'\n' read -rd '' -a array <<<"$array"
There are no problems if array appears both times here.
Now, if you want to copy an array, you'll do this (assuming the array to copy is called y as in your example):
array=( "${y[#]}" )
Note, that this will not preserve the sparseness of the array (but in your case, y is not sparse so there are no problems with this).
Another comment: when you do IFS=$'\n' read -rd '' -a y <<<"$array", read will return with a return code of 1; while this is not a problem, you may still want to make return happy by using:
IFS=$'\n' read -rd '' -a array < <(printf '%s\0' "$array")
A last comment: instead of using read you can use the builtin mapfile (bash≥4.0 only):
mapfile -t array <<< "$array"

Related

Put lines of a text file in an array in bash

I'm taking over a bash script from a colleague that reads a file, process it and print another file based on the line in the while loop at the moment.
I now need to append some features to it. The one I'm having issues with right now is to read a file and put each line into an array, except the 2nd column of that line can be empty, e.g.:
For a text file with \t as separator:
A\tB\tC
A\t\tC
For a CSV file same but with , as separator:
A,B,C
A,,C
Which should then give
["A","B","C"] or ["A", "", "C"]
The code I took over is as follow:
while IFS=$'\t\r' read -r -a col; do
# Process the array, put that into a file
lp -d $printer $file_to_print
done < $input_file
Which works if B is filled, but B need to be empty now sometimes, so when the input files keeps it empty, the created array and thus the output file to print just skips this empty cell (array is then ["A","C"]).
I tried writing the whole bloc on awk but this brought it's own sets of problems, making it difficult to call the lp command to print.
So my question is, how can I preserve the empty cell from the line into my bash array, so that I can call on it later and use it?
Thank you very much. I know this might be quite confused so please ask and I'll specify.
Edit: After request, here's the awk code I've tried. The issue here is that it only prints the last print request, while I know it loops over the whole file, and the lp command is still in the loop.
awk 'BEGIN {
inputfile="'"${optfile}"'"
outputfile="'"${file_loc}"'"
printer="'"${printer}"'"
while (getline < inputfile){
print "'"${prefix}"'" > outputfile
split($0,ft,"'"${IFSseps}"'");
if (length(ft[2]) == 0){
print "CODEPAGE 1252\nTEXT 465,191,\"ROMAN.TTF\",180,7,7,\""ft[1]"\"" >> outputfile
size_changer = 0
} else {
print "CODEPAGE 1252\nTEXT 465,191,\"ROMAN.TTF\",180,7,7,\""ft[1]"_"ft[2]"\"" >> outputfile
size_changer = 1
}
if ( split($0,ft,"'"${IFSseps}"'") > 6)
maxcounter = 6;
else
maxcounter = split($0,ft,"'"${IFSseps}"'");
for (i = 3; i <= maxcounter; i++){
x=191-(i-2)*33
print "CODEPAGE 1252\nTEXT 465,"x",\"ROMAN.TTF\",180,7,7,\""ft[i]"\"" >> outputfile
}
print "PRINT ""'"${copies}"'"",1" >> outputfile
close(outputfile)
"'"`lp -d ${printer} ${file_loc}`"'"
}
close("'"${file_loc}"'");
}'
EDIT2: Continuing to try to find a solution to it, I tried following code without success. This is weird, as just doing printf without putting it in an array keeps the formatting intact.
$ cat testinput | tr '\t' '>'
A>B>C
A>>C
# Should normally be empty on the second ouput line
$ while read line; do IFS=$'\t' read -ra col < <(printf "$line"); echo ${col[1]}; done < testinput
B
C
For tab, it's complicated.
From 3.5.7 Word Splitting in the manual:
A sequence of IFS whitespace characters is also treated as a delimiter.
Since tab is an "IFS whitespace character", sequences of tabs are treated as a single delimiter
IFS=$'\t' read -ra ary <<<$'A\t\tC'
declare -p ary
declare -a ary=([0]="A" [1]="C")
What you can do is translate tabs to a non-whitespace character, assuming it does not clash with the actual data in the fields:
line=$'A\t\tC'
IFS=, read -ra ary <<<"${line//$'\t'/,}"
declare -p ary
declare -a ary=([0]="A" [1]="" [2]="C")
To avoid the risk of colliding with commas in the data, we can use an unusual ASCII character: FS, octal 034
line=$'A\t\tC'
printf -v FS '\034'
IFS="$FS" read -ra ary <<<"${line//$'\t'/"$FS"}"
# or, without the placeholder variable
IFS=$'\034' read -ra ary <<<"${line//$'\t'/$'\034'}"
declare -p ary
declare -a ary=([0]="A" [1]="" [2]="C")
One bash example using parameter expansion where we convert the delimiter into a \n and let mapfile read in each line as a new array entry ...
For tab-delimited data:
for line in $'A\tB\tC' $'A\t\tC'
do
mapfile -t array <<< "${line//$'\t'/$'\n'}"
echo "############# ${line}"
typeset -p array
done
############# A B C
declare -a array=([0]="A" [1]="B" [2]="C")
############# A C
declare -a array=([0]="A" [1]="" [2]="C")
NOTE: The $'...' construct insures the \t is treated as a single <tab> character as opposed to the two literal characters \ + t.
For comma-delimited data:
for line in 'A,B,C' 'A,,C'
do
mapfile -t array <<< "${line//,/$'\n'}"
echo "############# ${line}"
typeset -p array
done
############# A,B,C
declare -a array=([0]="A" [1]="B" [2]="C")
############# A,,C
declare -a array=([0]="A" [1]="" [2]="C")
NOTE: This obviously (?) assumes the desired data does not contain a comma (,).
It may just be your # Process the array, put that into a file part.
IFS=, read -ra ray <<< "A,,C"
for e in "${ray[#]}"; do o="$o\"$e\","; done
echo "[${o%,}]"
["A","","C"]
See #Glenn's excellent answer regarding tabs.
My simple data file:
$: cat x # tab delimited, empty field 2 of line 2
a b c
d f
My test:
while IFS=$'\001' read -r a b c; do
echo "a:[$a] b:[$b] c:[$c]"
done < <(tr "\t" "\001"<x)
a:[a] b:[b] c:[c]
a:[d] b:[] c:[f]
Note that I used ^A (a 001 byte) but you might be able to use something as simple as a comma or pipe (|) character. Choose based on your data.

Bash - How to pass array to function, with index defined in new function

I have this current function used to create an array in a menu style (associative arrays can't be used in the scenario).
declare -a array1=("host1" "host2" "host3")
declare -a array2=("ip1" "ip2" "ip3")
function menusetup {
iter=0
for ((i=0; i<(count*2); i+=2)); do
menu[$i]=${array1[$iter]}
menu[$i+1]=${array2[$iter]}
((iter++))
done
}
menusetup
which gives me the menu array with {"host1" "ip1" "host2" "ip2" "host3" "ip3"
This is working perfect for what I needed, but now I want to reuse this function to pass any 2 arrays, something like this.
function menusetup {
iter=0
for ((i=0; i<(count*2); i+=2)); do
menu[$i]=${$1[$iter]}
menu[$i+1]=${$2[$iter]}
((iter++))
done
}
menusetup "${array3[#]}" "${array4[#]}"
Edit: I know the second example passes the entire list of elements of the array. I want to know if there is a way to pass and substitute the array
While you can use bash namereferences:
array3=(1 2 3)
array4=(a b c)
menusetup() {
declare -g menu
declare -n count1=$1[#]
count=$(( ${#count1} ))
iter=0
for ((i=0; i<count; i+=2)); do
declare -n el1=$1[$iter]
declare -n el2=$2[$iter]
menu[$i]=$el1
menu[$i+1]=$el2
((iter++))
done
}
menusetup array3 array4
declare -p menu
# will output:
# declare -a menu=([0]="1" [1]="a" [2]="2" [3]="b" [4]="3" [5]="c")
And this is valid and useful programming style, It's way better and easier to think of bash as pipelines. You don't need state, you don't need to "iterate" over arrays.
Think of bash as streams. What you are doing here is you are "zip"-ing two arrays. So first print each array element on newline separated items, then use paste to join two newline separated streams together, specify the paste separator to newline. Then read it to an array, either with readarray < <(...) or IFS=$'\n' menu=( $(...) ).
Assuming there are no newline characters in array values, the whole work your function is doing can be simply done with:
readarray -t menu < <(paste -d$'\n' <(printf "%s\n" "${array3[#]}") <(printf "%s\n" "${array4[#]}") )

Is there a way to split an array of strings into multiple other arrays within Bash?

I have an array my_array in Bash that contains string elements formatted as such
my_array[0] = "username_1:id_1:name_1"
my_array[1] = "username_2:id_2:name_2"
my_array[2] = "username_3:id_3:name_3"
I was curious if there is a convenient way to iterate through this array splitting it on the ":" character and storing the values into three separate arrays. The pseudo-code might look something like this
index=0
for i in "${my_array}"
do
usernames[$index], ids[$index], names[$index]= $(echo $i | tr ":" "\n")
(( index = index + 1 ))
done
This question (How do I split a string on a delimiter in Bash?) is very similar to what I am trying to accomplish, however I am trying to store the values into three different arrays which is my main obstacle. Thanks in advance for any help as I am extremely new to bash programming.
here is what I have attempted:
index=0
for i in "${arrayAdmin[#]}"
do
credentials=$(echo $i | tr ":" "\n" )
accounts[$index]=${credentials[0]}
groups[$index]=${credentials[1]}
names[$index]=${credentials[2]}
(( index = $index + 1))
echo $index
done
Use read, then assign to each array individually.
for i in "${my_array[#]}"
do
IFS=: read -r username id name <<< "$i"
usernames+=("$username")
ids+=("$id")
names+=("$name")
done
You could write
i=0
for rec in "${my_array[#]}"
do
IFS=: read -r usernames[i] ids[i] names[i] <<< "$rec"
((i++))
done
but I find the first loop more readable, in that it doesn't require an explicit loop index for the new arrays.
Or maybe using the same index variable (managed by the for loop) for both the input array and the three output arrays.
for ((i=0;i < ${#my_array[#]}; i++)); do
IFS=: read -r usernames[i] ids[i] names[i] <<< "${my_array[i]}"
done
And finally, if you don't want to assume the array indices are a continuous sequence and you want to preserve the gaps
# For example, my_array=([0]="..." [5]="..." [10]="...")
for i in "${!my_array[#]}"; do
IFS=: read -r usernames[i] ids[i] names[i] <<< "${my_array[i]}"
done

Why can't I append to array?

I'm not sure whats going on here
#!/bin/bash
STRING_PREFIX="foo"
STRING_IDX="1,2,3,4,5"
declare -a STRING_ARRAY
main() {
assemble_strings
for i in "${STRING_ARRAY[#]}"; do
echo "TEST: $i"
done
}
assemble_strings() {
IFS=,
while IFS= read idx; do
STRING_ARRAY+=("${STRING_PREFIX}${idx}")
done < <(echo $STRING_IDX)
}
main
I expect an array of 5 strings each prepended with 'foo'. Instead I get an array of 1 string
TEST: foo1 2 3 4 5
For bonus points, how can I avoid the loop entirely? I can't figure out how to create an array from an expression in bash.
First: Because you put IFS= at the front of your read, the prior IFS=, does nothing (insofar as that read is concerned).
Second: Because you aren't setting -d , in your read, it's using the default -- newline -- value as record terminator. (IFS determines the field separator, not the record terminator; with an empty IFS value, your records have only one field in them anyhow). Thus, when you call read, it reads the whole record -- up to the newline -- so your loop only runs once.
One approach, using read -a to read directly to an array (in this case, treating the entire input stream as a single record, with fields separated by commas):
string_idx=1,2,3,4,5
string_prefix=foo
# use read to directly populate the array
IFS=, read -r -d '' -a string_array <<<"$string_idx"
# go back through and tack on prefixes
for idx in "${!string_array[#]}"; do
string_array[$idx]="${string_prefix}${string_array[$idx]}"
done
# print values
printf ' entry: %s\n' "${string_array[#]}"
Another, making the smallest change to your existing code -- treating the input stream as a series of single-field comma-separated records:
string_idx=1,2,3,4,5
string_prefix=foo
string_array=( )
while IFS= read -r -d , idx; do
string_array+=( "${string_prefix}${idx}" )
done <<<"$string_idx,"

Variable in bash array name

I have this code:
#!/bin/sh
...
for cnumber in `seq $CNUMBER`; do
declare -a CAT$cnumber
let i=0
while IFS=$'\n' read -r line_data; do
CAT$cnumber[i]="${line_data}"
((++i))
done < input_file_$cnumber
done
It works if I use array name without variable $cnumber.
But I want to create multiple arrays (CAT0, CAT1, CAT2 etc.) and to read lines:
from file 'input_file_0' to array 'CAT0'
from file 'input_file_1' to array 'CAT1'
from file 'input_file_2' to array 'CAT2'
etc.
What syntax to use $cnumber variable in array name (CAT1) and in input file_name?
for cnumber in `seq $CNUMBER`; do
declare -a CAT$cnumber
let i=0
while IFS=$'\n' read -r line_data; do
eval CAT$cnumber[i]='"${line_data}"'
((++i))
done < input_file_$cnumber
done
Mainly, that adds the word "eval" which makes bash evaluate the rest of the line. Before that, bash expands variables, thus CAT$number will be something like CAT1 when the line is evaluated. Keep in mind that "${line_date}" would be subject to variable expansion before eval evaluates the line if it would not be protected by single quotes. That might have unexpected effects if the $line_data would contain blank spaces. See this simplified example:
a=b
l="hello date"
eval $a="$l" # executes "date", has no other effect
echo $b # prints an empty line
eval $a='"$l"' # sets b to "hello date"
echo $b # prints that: hello date
In reply to the comment of Etan Reisner below, I add another solution that avoids "eval" and instead uses references, which are available in bash version 4.3 or higher. In that case, using references is preferable for the reason Etan pointed out, and also, for my opinion, because it is more natural:
for cnumber in `seq $CNUMBER`; do
declare -a CAT$cnumber # be sure the array is declared before ...
declare -n ref=CAT$cnumber # ... you declare ref to reference the array
let i=0
while IFS=$'\n' read -r line_data; do
ref[$i]="${line_data}"
((++i))
done < input_file_$cnumber
done
First, your shebang needs to be
#!/bin/bash
since declare is a bash built-in.
Next, use declare to define the array elements
for cnumber in `seq $CNUMBER`; do
declare -a CAT$cnumber
let i=0
while IFS=$'\n' read -r line_data; do
declare "CAT$cnumber[$i]=${line_data}"
((++i))
done < input_file_$cnumber
done

Resources