in Bash Script, how to read a file and split all lines into a two-dimensional array - linux

Content of the file:
Class_one 23
Class_two 17
Class-three 22
..
How to read the file and split all lines into a two-dimensional array? like java. Like:
arr[0][0] = Class_one arr[0][1] = 23
arr[1][0] = Class_two arr[1][1] = 17
thanks.

GNU bash has no two-dimensional array. A workaround is an associative array.
#!/bin/bash
declare -A arr # declare an associative array
declare -i c=0
# read from stdin (from file)
while read -r label number; do
arr[$c,0]="$label"; arr[$c,1]="$number"
c=c+1
done < file
# print array arr
for ((i=0;i<${#arr[#]}/2;i++)); do
echo "${arr[$i,0]} ${arr[$i,1]}"
done
See: help declare and man bash

#Cyrus's approach involves associative arrays which is noticeably only in bash 4.0 and up. Below is what works for bash sub-4.0. Note that nowadays Mac's are still shipped w/ bash 3.x.
#!/bin/bash
l=0; while read -a a$l; do
let l++;
done < ${data_file_name}
## now everything is stored in the 2D array ${a};
## $(($l+1)) is #rows, and ${#a0[#]} is #cols;
## elements can be accessed in the form of "ai[j]";
## e.g., a0[0] is the element at (0,0);
## but to access "ai[j]" using var ${i} and ${j}
## as indexes can be a just little tricky
echo "#rows: $((l+1))"
echo "#cols: ${#a0[#]}"$'\n'
echo "element at (0, 0): ${a0[0]}"
## the following shows how to access an element at (i,j)
i=1; j=1
tmp_a="a${i}[${j}]"; echo "element at ($i, $j): ${!tmp_a}"$'\n'
## the following shows how to iterate through the 2D array
echo "all elements printed from top left to bottom right:"
for i in `eval echo {0..$l}`; do
for j in `eval echo {0.."$((${#a0[#]}-1))"}`; do
tmp_a="a${i}[${j}]"; echo ${!tmp_a}
done
done

Related

how can i make the lines variable in a file? [duplicate]

I'm trying to read from a file, that has multiple lines, each with 3 informations I want to assign to the variables and work with.
I figured out, how to simply display them each on the terminal, but can't figure out how to actually assign them to variables.
while read i
do
for j in $i
do
echo $j
done
done < ./test.txt
test.txt:
1 2 3
a b c
So I want to read the line in the outer loop, then assign the 3 variables and then work with them, before going to the next line.
I'm guessing I have to read the values of the lines without an inside loop, but I can't figure it out right now.
Hope someone can point me in the right direction.
I think all you're looking for is to read multiple variables per line: the read command can assign words to variables by itself.
while read -r first second third; do
do_stuff_with "$first"
do_stuff_with "$second"
do_stuff_with "$third"
done < ./test.txt
The below assumes that your desired result is the set of assignments a=1, b=2, and c=3, taking the values from the first line and the keys from the second.
The easy way to do this is to read your keys and values into two separate arrays. Then you can iterate only once, referring to the items at each position within those arrays.
#!/usr/bin/env bash
case $BASH_VERSION in
''|[123].*) echo "ERROR: This script requires bash 4.0 or newer" >&2; exit 1;;
esac
input_file=${1:-test.txt}
# create an associative array in which to store your variables read from a file
declare -A vars=( )
{
read -r -a vals # read first line into array "vals"
read -r -a keys # read second line into array "keys"
for idx in "${!keys[#]}"; do # iterate over array indexes (starting at 0)
key=${keys[$idx]} # extract key at that index
val=${vals[$idx]} # extract value at that index
vars[$key]=$val # assign the value to the key inside the associative array
done
} < "$input_file"
# print for debugging
declare -p vars >&2
echo "Value of variable a is ${vars[a]}"
See:
BashFAQ #6 - How can I use variable variables (indirect variables, pointers, references) or associative arrays?
The bash-hackers page on the read builtin, documenting use of -a to read words into an array.

How To Parameter Array To The Function In Bash [duplicate]

As we know, in bash programming the way to pass arguments is $1, ..., $N. However, I found it not easy to pass an array as an argument to a function which receives more than one argument. Here is one example:
f(){
x=($1)
y=$2
for i in "${x[#]}"
do
echo $i
done
....
}
a=("jfaldsj jflajds" "LAST")
b=NOEFLDJF
f "${a[#]}" $b
f "${a[*]}" $b
As described, function freceives two arguments: the first is assigned to x which is an array, the second to y.
f can be called in two ways. The first way use the "${a[#]}" as the first argument, and the result is:
jfaldsj
jflajds
The second way use the "${a[*]}" as the first argument, and the result is:
jfaldsj
jflajds
LAST
Neither result is as I wished. So, is there anyone having any idea about how to pass array between functions correctly?
You cannot pass an array, you can only pass its elements (i.e. the expanded array).
#!/bin/bash
function f() {
a=("$#")
((last_idx=${#a[#]} - 1))
b=${a[last_idx]}
unset a[last_idx]
for i in "${a[#]}" ; do
echo "$i"
done
echo "b: $b"
}
x=("one two" "LAST")
b='even more'
f "${x[#]}" "$b"
echo ===============
f "${x[*]}" "$b"
The other possibility would be to pass the array by name:
#!/bin/bash
function f() {
name=$1[#]
b=$2
a=("${!name}")
for i in "${a[#]}" ; do
echo "$i"
done
echo "b: $b"
}
x=("one two" "LAST")
b='even more'
f x "$b"
You can pass an array by name reference to a function in bash (since version 4.3+), by setting the -n attribute:
show_value () # array index
{
local -n myarray=$1
local idx=$2
echo "${myarray[$idx]}"
}
This works for indexed arrays:
$ shadock=(ga bu zo meu)
$ show_value shadock 2
zo
It also works for associative arrays:
$ declare -A days=([monday]=eggs [tuesday]=bread [sunday]=jam)
$ show_value days sunday
jam
See also nameref or declare -n in the man page.
You could pass the "scalar" value first. That would simplify things:
f(){
b=$1
shift
a=("$#")
for i in "${a[#]}"
do
echo $i
done
....
}
a=("jfaldsj jflajds" "LAST")
b=NOEFLDJF
f "$b" "${a[#]}"
At this point, you might as well use the array-ish positional params directly
f(){
b=$1
shift
for i in "$#" # or simply "for i; do"
do
echo $i
done
....
}
f "$b" "${a[#]}"
This will solve the issue of passing array to function:
#!/bin/bash
foo() {
string=$1
array=($#)
echo "array is ${array[#]}"
echo "array is ${array[1]}"
return
}
array=( one two three )
foo ${array[#]}
colors=( red green blue )
foo ${colors[#]}
Try like this
function parseArray {
array=("$#")
for data in "${array[#]}"
do
echo ${data}
done
}
array=("value" "value1")
parseArray "${array[#]}"
Pass the array as a function
array() {
echo "apple pear"
}
printArray() {
local argArray="${1}"
local array=($($argArray)) # where the magic happens. careful of the surrounding brackets.
for arrElement in "${array[#]}"; do
echo "${arrElement}"
done
}
printArray array
Here is an example where I receive 2 bash arrays into a function, as well as additional arguments after them. This pattern can be continued indefinitely for any number of bash arrays and any number of additional arguments, accommodating any input argument order, so long as the length of each bash array comes just before the elements of that array.
Function definition for print_two_arrays_plus_extra_args:
# Print all elements of a bash array.
# General form:
# print_one_array array1
# Example usage:
# print_one_array "${array1[#]}"
print_one_array() {
for element in "$#"; do
printf " %s\n" "$element"
done
}
# Print all elements of two bash arrays, plus two extra args at the end.
# General form (notice length MUST come before the array in order
# to be able to parse the args!):
# print_two_arrays_plus_extra_args array1_len array1 array2_len array2 \
# extra_arg1 extra_arg2
# Example usage:
# print_two_arrays_plus_extra_args "${#array1[#]}" "${array1[#]}" \
# "${#array2[#]}" "${array2[#]}" "hello" "world"
print_two_arrays_plus_extra_args() {
i=1
# Read array1_len into a variable
array1_len="${#:$i:1}"
((i++))
# Read array1 into a new array
array1=("${#:$i:$array1_len}")
((i += $array1_len))
# Read array2_len into a variable
array2_len="${#:$i:1}"
((i++))
# Read array2 into a new array
array2=("${#:$i:$array2_len}")
((i += $array2_len))
# You can now read the extra arguments all at once and gather them into a
# new array like this:
extra_args_array=("${#:$i}")
# OR you can read the extra arguments individually into their own variables
# one-by-one like this
extra_arg1="${#:$i:1}"
((i++))
extra_arg2="${#:$i:1}"
((i++))
# Print the output
echo "array1:"
print_one_array "${array1[#]}"
echo "array2:"
print_one_array "${array2[#]}"
echo "extra_arg1 = $extra_arg1"
echo "extra_arg2 = $extra_arg2"
echo "extra_args_array:"
print_one_array "${extra_args_array[#]}"
}
Example usage:
array1=()
array1+=("one")
array1+=("two")
array1+=("three")
array2=("four" "five" "six" "seven" "eight")
echo "Printing array1 and array2 plus some extra args"
# Note that `"${#array1[#]}"` is the array length (number of elements
# in the array), and `"${array1[#]}"` is the array (all of the elements
# in the array)
print_two_arrays_plus_extra_args "${#array1[#]}" "${array1[#]}" \
"${#array2[#]}" "${array2[#]}" "hello" "world"
Example Output:
Printing array1 and array2 plus some extra args
array1:
one
two
three
array2:
four
five
six
seven
eight
extra_arg1 = hello
extra_arg2 = world
extra_args_array:
hello
world
For further examples and detailed explanations of how this works, see my longer answer on this topic here: Passing arrays as parameters in bash
You can also create a json file with an array, and then parse that json file with jq
For example:
my-array.json:
{
"array": ["item1","item2"]
}
script.sh:
ARRAY=$(jq -r '."array"' $1 | tr -d '[],"')
And then call the script like:
script.sh ./path-to-json/my-array.json

Bash shell how to remove string from array with -= operator [duplicate]

I need to remove an element from an array in bash shell.
Generally I'd simply do:
array=("${(#)array:#<element to remove>}")
Unfortunately the element I want to remove is a variable so I can't use the previous command.
Down here an example:
array+=(pluto)
array+=(pippo)
delete=(pluto)
array( ${array[#]/$delete} ) -> but clearly doesn't work because of {}
Any idea?
The following works as you would like in bash and zsh:
$ array=(pluto pippo)
$ delete=pluto
$ echo ${array[#]/$delete}
pippo
$ array=( "${array[#]/$delete}" ) #Quotes when working with strings
If need to delete more than one element:
...
$ delete=(pluto pippo)
for del in ${delete[#]}
do
array=("${array[#]/$del}") #Quotes when working with strings
done
Caveat
This technique actually removes prefixes matching $delete from the elements, not necessarily whole elements.
Update
To really remove an exact item, you need to walk through the array, comparing the target to each element, and using unset to delete an exact match.
array=(pluto pippo bob)
delete=(pippo)
for target in "${delete[#]}"; do
for i in "${!array[#]}"; do
if [[ ${array[i]} = $target ]]; then
unset 'array[i]'
fi
done
done
Note that if you do this, and one or more elements is removed, the indices will no longer be a continuous sequence of integers.
$ declare -p array
declare -a array=([0]="pluto" [2]="bob")
The simple fact is, arrays were not designed for use as mutable data structures. They are primarily used for storing lists of items in a single variable without needing to waste a character as a delimiter (e.g., to store a list of strings which can contain whitespace).
If gaps are a problem, then you need to rebuild the array to fill the gaps:
for i in "${!array[#]}"; do
new_array+=( "${array[i]}" )
done
array=("${new_array[#]}")
unset new_array
You could build up a new array without the undesired element, then assign it back to the old array. This works in bash:
array=(pluto pippo)
new_array=()
for value in "${array[#]}"
do
[[ $value != pluto ]] && new_array+=($value)
done
array=("${new_array[#]}")
unset new_array
This yields:
echo "${array[#]}"
pippo
This is the most direct way to unset a value if you know it's position.
$ array=(one two three)
$ echo ${#array[#]}
3
$ unset 'array[1]'
$ echo ${array[#]}
one three
$ echo ${#array[#]}
2
This answer is specific to the case of deleting multiple values from large arrays, where performance is important.
The most voted solutions are (1) pattern substitution on an array, or (2) iterating over the array elements. The first is fast, but can only deal with elements that have distinct prefix, the second has O(n*k), n=array size, k=elements to remove. Associative array are relative new feature, and might not have been common when the question was originally posted.
For the exact match case, with large n and k, possible to improve performance from O(nk) to O(n+klog(k)). In practice, O(n) assuming k much lower than n. Most of the speed up is based on using associative array to identify items to be removed.
Performance (n-array size, k-values to delete). Performance measure seconds of user time
N K New(seconds) Current(seconds) Speedup
1000 10 0.005 0.033 6X
10000 10 0.070 0.348 5X
10000 20 0.070 0.656 9X
10000 1 0.043 0.050 -7%
As expected, the current solution is linear to N*K, and the fast solution is practically linear to K, with much lower constant. The fast solution is slightly slower vs the current solution when k=1, due to additional setup.
The 'Fast' solution: array=list of input, delete=list of values to remove.
declare -A delk
for del in "${delete[#]}" ; do delk[$del]=1 ; done
# Tag items to remove, based on
for k in "${!array[#]}" ; do
[ "${delk[${array[$k]}]-}" ] && unset 'array[k]'
done
# Compaction
array=("${array[#]}")
Benchmarked against current solution, from the most-voted answer.
for target in "${delete[#]}"; do
for i in "${!array[#]}"; do
if [[ ${array[i]} = $target ]]; then
unset 'array[i]'
fi
done
done
array=("${array[#]}")
Here's a one-line solution with mapfile:
$ mapfile -d $'\0' -t arr < <(printf '%s\0' "${arr[#]}" | grep -Pzv "<regexp>")
Example:
$ arr=("Adam" "Bob" "Claire"$'\n'"Smith" "David" "Eve" "Fred")
$ echo "Size: ${#arr[*]} Contents: ${arr[*]}"
Size: 6 Contents: Adam Bob Claire
Smith David Eve Fred
$ mapfile -d $'\0' -t arr < <(printf '%s\0' "${arr[#]}" | grep -Pzv "^Claire\nSmith$")
$ echo "Size: ${#arr[*]} Contents: ${arr[*]}"
Size: 5 Contents: Adam Bob David Eve Fred
This method allows for great flexibility by modifying/exchanging the grep command and doesn't leave any empty strings in the array.
Partial answer only
To delete the first item in the array
unset 'array[0]'
To delete the last item in the array
unset 'array[-1]'
To expand on the above answers, the following can be used to remove multiple elements from an array, without partial matching:
ARRAY=(one two onetwo three four threefour "one six")
TO_REMOVE=(one four)
TEMP_ARRAY=()
for pkg in "${ARRAY[#]}"; do
for remove in "${TO_REMOVE[#]}"; do
KEEP=true
if [[ ${pkg} == ${remove} ]]; then
KEEP=false
break
fi
done
if ${KEEP}; then
TEMP_ARRAY+=(${pkg})
fi
done
ARRAY=("${TEMP_ARRAY[#]}")
unset TEMP_ARRAY
This will result in an array containing:
(two onetwo three threefour "one six")
Here's a (probably very bash-specific) little function involving bash variable indirection and unset; it's a general solution that does not involve text substitution or discarding empty elements and has no problems with quoting/whitespace etc.
delete_ary_elmt() {
local word=$1 # the element to search for & delete
local aryref="$2[#]" # a necessary step since '${!$2[#]}' is a syntax error
local arycopy=("${!aryref}") # create a copy of the input array
local status=1
for (( i = ${#arycopy[#]} - 1; i >= 0; i-- )); do # iterate over indices backwards
elmt=${arycopy[$i]}
[[ $elmt == $word ]] && unset "$2[$i]" && status=0 # unset matching elmts in orig. ary
done
return $status # return 0 if something was deleted; 1 if not
}
array=(a 0 0 b 0 0 0 c 0 d e 0 0 0)
delete_ary_elmt 0 array
for e in "${array[#]}"; do
echo "$e"
done
# prints "a" "b" "c" "d" in lines
Use it like delete_ary_elmt ELEMENT ARRAYNAME without any $ sigil. Switch the == $word for == $word* for prefix matches; use ${elmt,,} == ${word,,} for case-insensitive matches; etc., whatever bash [[ supports.
It works by determining the indices of the input array and iterating over them backwards (so deleting elements doesn't screw up iteration order). To get the indices you need to access the input array by name, which can be done via bash variable indirection x=1; varname=x; echo ${!varname} # prints "1".
You can't access arrays by name like aryname=a; echo "${$aryname[#]}, this gives you an error. You can't do aryname=a; echo "${!aryname[#]}", this gives you the indices of the variable aryname (although it is not an array). What DOES work is aryref="a[#]"; echo "${!aryref}", which will print the elements of the array a, preserving shell-word quoting and whitespace exactly like echo "${a[#]}". But this only works for printing the elements of an array, not for printing its length or indices (aryref="!a[#]" or aryref="#a[#]" or "${!!aryref}" or "${#!aryref}", they all fail).
So I copy the original array by its name via bash indirection and get the indices from the copy. To iterate over the indices in reverse I use a C-style for loop. I could also do it by accessing the indices via ${!arycopy[#]} and reversing them with tac, which is a cat that turns around the input line order.
A function solution without variable indirection would probably have to involve eval, which may or may not be safe to use in that situation (I can't tell).
Using unset
To remove an element at particular index, we can use unset and then do copy to another array. Only just unset is not required in this case. Because unset does not remove the element it just sets null string to the particular index in array.
declare -a arr=('aa' 'bb' 'cc' 'dd' 'ee')
unset 'arr[1]'
declare -a arr2=()
i=0
for element in "${arr[#]}"
do
arr2[$i]=$element
((++i))
done
echo "${arr[#]}"
echo "1st val is ${arr[1]}, 2nd val is ${arr[2]}"
echo "${arr2[#]}"
echo "1st val is ${arr2[1]}, 2nd val is ${arr2[2]}"
Output is
aa cc dd ee
1st val is , 2nd val is cc
aa cc dd ee
1st val is cc, 2nd val is dd
Using :<idx>
We can remove some set of elements using :<idx> also. For example if we want to remove 1st element we can use :1 as mentioned below.
declare -a arr=('aa' 'bb' 'cc' 'dd' 'ee')
arr2=("${arr[#]:1}")
echo "${arr2[#]}"
echo "1st val is ${arr2[1]}, 2nd val is ${arr2[2]}"
Output is
bb cc dd ee
1st val is cc, 2nd val is dd
http://wiki.bash-hackers.org/syntax/pe#substring_removal
${PARAMETER#PATTERN} # remove from beginning
${PARAMETER##PATTERN} # remove from the beginning, greedy match
${PARAMETER%PATTERN} # remove from the end
${PARAMETER%%PATTERN} # remove from the end, greedy match
In order to do a full remove element, you have to do an unset command with an if statement. If you don't care about removing prefixes from other variables or about supporting whitespace in the array, then you can just drop the quotes and forget about for loops.
See example below for a few different ways to clean up an array.
options=("foo" "bar" "foo" "foobar" "foo bar" "bars" "bar")
# remove bar from the start of each element
options=("${options[#]/#"bar"}")
# options=("foo" "" "foo" "foobar" "foo bar" "s" "")
# remove the complete string "foo" in a for loop
count=${#options[#]}
for ((i = 0; i < count; i++)); do
if [ "${options[i]}" = "foo" ] ; then
unset 'options[i]'
fi
done
# options=( "" "foobar" "foo bar" "s" "")
# remove empty options
# note the count variable can't be recalculated easily on a sparse array
for ((i = 0; i < count; i++)); do
# echo "Element $i: '${options[i]}'"
if [ -z "${options[i]}" ] ; then
unset 'options[i]'
fi
done
# options=("foobar" "foo bar" "s")
# list them with select
echo "Choose an option:"
PS3='Option? '
select i in "${options[#]}" Quit
do
case $i in
Quit) break ;;
*) echo "You selected \"$i\"" ;;
esac
done
Output
Choose an option:
1) foobar
2) foo bar
3) s
4) Quit
Option?
Hope that helps.
There is also this syntax, e.g. if you want to delete the 2nd element :
array=("${array[#]:0:1}" "${array[#]:2}")
which is in fact the concatenation of 2 tabs. The first from the index 0 to the index 1 (exclusive) and the 2nd from the index 2 to the end.
POSIX shell script does not have arrays.
So most probably you are using a specific dialect such as bash, korn shells or zsh.
Therefore, your question as of now cannot be answered.
Maybe this works for you:
unset array[$delete]
What I do is:
array="$(echo $array | tr ' ' '\n' | sed "/itemtodelete/d")"
BAM, that item is removed.
This is a quick-and-dirty solution that will work in simple cases but will break if (a) there are regex special characters in $delete, or (b) there are any spaces at all in any items. Starting with:
array+=(pluto)
array+=(pippo)
delete=(pluto)
Delete all entries exactly matching $delete:
array=(`echo $array | fmt -1 | grep -v "^${delete}$" | fmt -999999`)
resulting in
echo $array -> pippo, and making sure it's an array:
echo $array[1] -> pippo
fmt is a little obscure: fmt -1 wraps at the first column (to put each item on its own line. That's where the problem arises with items in spaces.) fmt -999999 unwraps it back to one line, putting back the spaces between items. There are other ways to do that, such as xargs.
Addendum: If you want to delete just the first match, use sed, as described here:
array=(`echo $array | fmt -1 | sed "0,/^${delete}$/{//d;}" | fmt -999999`)
Actually, I just noticed that the shell syntax somewhat has a behavior built-in that allows for easy reconstruction of the array when, as posed in the question, an item should be removed.
# let's set up an array of items to consume:
x=()
for (( i=0; i<10; i++ )); do
x+=("$i")
done
# here, we consume that array:
while (( ${#x[#]} )); do
i=$(( $RANDOM % ${#x[#]} ))
echo "${x[i]} / ${x[#]}"
x=("${x[#]:0:i}" "${x[#]:i+1}")
done
Notice how we constructed the array using bash's x+=() syntax?
You could actually add more than one item with that, the content of a whole other array at once.
In ZSH this is dead easy (note this uses more bash compatible syntax than necessary where possible for ease of understanding):
# I always include an edge case to make sure each element
# is not being word split.
start=(one two three 'four 4' five)
work=(${(#)start})
idx=2
val=${work[idx]}
# How to remove a single element easily.
# Also works for associative arrays (at least in zsh)
work[$idx]=()
echo "Array size went down by one: "
[[ $#work -eq $(($#start - 1)) ]] && echo "OK"
echo "Array item "$val" is now gone: "
[[ -z ${work[(r)$val]} ]] && echo OK
echo "Array contents are as expected: "
wanted=("${start[#]:0:1}" "${start[#]:2}")
[[ "${(j.:.)wanted[#]}" == "${(j.:.)work[#]}" ]] && echo "OK"
echo "-- array contents: start --"
print -l -r -- "-- $#start elements" ${(#)start}
echo "-- array contents: work --"
print -l -r -- "-- $#work elements" "${work[#]}"
Results:
Array size went down by one:
OK
Array item two is now gone:
OK
Array contents are as expected:
OK
-- array contents: start --
-- 5 elements
one
two
three
four 4
five
-- array contents: work --
-- 4 elements
one
three
four 4
five
To avoid conflicts with array index using unset - see https://stackoverflow.com/a/49626928/3223785 and https://stackoverflow.com/a/47798640/3223785 for more information - reassign the array to itself: ARRAY_VAR=(${ARRAY_VAR[#]}).
#!/bin/bash
ARRAY_VAR=(0 1 2 3 4 5 6 7 8 9)
unset ARRAY_VAR[5]
unset ARRAY_VAR[4]
ARRAY_VAR=(${ARRAY_VAR[#]})
echo ${ARRAY_VAR[#]}
A_LENGTH=${#ARRAY_VAR[*]}
for (( i=0; i<=$(( $A_LENGTH -1 )); i++ )) ; do
echo ""
echo "INDEX - $i"
echo "VALUE - ${ARRAY_VAR[$i]}"
done
exit 0
[Ref.: https://tecadmin.net/working-with-array-bash-script/ ]
How about something like:
array=(one two three)
array_t=" ${array[#]} "
delete=one
array=(${array_t// $delete / })
unset array_t
#/bin/bash
echo "# define array with six elements"
arr=(zero one two three 'four 4' five)
echo "# unset by index: 0"
unset -v 'arr[0]'
for i in ${!arr[*]}; do echo "arr[$i]=${arr[$i]}"; done
arr_delete_by_content() { # value to delete
for i in ${!arr[*]}; do
[ "${arr[$i]}" = "$1" ] && unset -v 'arr[$i]'
done
}
echo "# unset in global variable where value: three"
arr_delete_by_content three
for i in ${!arr[*]}; do echo "arr[$i]=${arr[$i]}"; done
echo "# rearrange indices"
arr=( "${arr[#]}" )
for i in ${!arr[*]}; do echo "arr[$i]=${arr[$i]}"; done
delete_value() { # value arrayelements..., returns array decl.
local e val=$1; new=(); shift
for e in "${#}"; do [ "$val" != "$e" ] && new+=("$e"); done
declare -p new|sed 's,^[^=]*=,,'
}
echo "# new array without value: two"
declare -a arr="$(delete_value two "${arr[#]}")"
for i in ${!arr[*]}; do echo "arr[$i]=${arr[$i]}"; done
delete_values() { # arraydecl values..., returns array decl. (keeps indices)
declare -a arr="$1"; local i v; shift
for v in "${#}"; do
for i in ${!arr[*]}; do
[ "$v" = "${arr[$i]}" ] && unset -v 'arr[$i]'
done
done
declare -p arr|sed 's,^[^=]*=,,'
}
echo "# new array without values: one five (keep indices)"
declare -a arr="$(delete_values "$(declare -p arr|sed 's,^[^=]*=,,')" one five)"
for i in ${!arr[*]}; do echo "arr[$i]=${arr[$i]}"; done
# new array without multiple values and rearranged indices is left to the reader

How to extract key value pairs from a file when values span multiple lines?

I'm a few weeks into bash scripting and I haven't advanced enough yet to get my head wrapped around this problem. Any help would be appreciated!
I have a "script.conf" file that contains the following:
key1=value1
key2=${HOME}/Folder
key3=( "k3v1" "k3 v2" "k3v3")
key4=( "k4v1"
"k4 v2"
"k4v3"
)
key5=value5
#key6="Do Not Include Me"
In a bash script, I want to read the contents of this script.conf file into an array. I've learned how to handle the scenarios for keys 1, 2, 3, and 5, but the key4 scenario throws a wrench into it with it spanning across multiple lines.
I've been exploring the use of sed -n '/=\s*[(]/,/[)]/{/' which does capture key4 and its value, but I can't figure out how to mix this so that the other keys are also captured in the matches. The range syntax is also new to me, so I haven't figured out how to separate the key/value. I feel like there is an easy regex that would accomplish what I want... in plain-text: "find and group the pattern ^(.*)= (for the key), then group everything after the '=' char until another ^(.*)= match is found, rinse and repeat". I guess if I do this, I need to change the while read line to not handle the key/value separation for me (I'll be looking into this while I'm waiting for a response). BTW, I think a solution where the value of key4 is flattened (new lines removed) would be acceptable; I know for key3 I have to store the value as a string and then convert it to an array later when I want to iterate over it since an array element apparently can't contain a list.
Am I on the right path with sed or is this a job for awk or some other tool? (I haven't ventured into awk yet). Is there an easier approach that I'm missing because I'm too deep into the forest (like changing the while read line in the LoadConfigFile function)?
Here is the code that I have so far in script.sh for processing and capturing the other pairs into the $config array:
__AppDir=$(dirname $0)
__AppName=${__ScriptName%.*}
typeset -A config #init config array
config=( #Setting Default Config values
[key1]="defaultValue1"
[key2]="${HOME}/defaultFolder"
[QuietMode]=0
[Verbose]=0 #Ex. Usage: [[ "${config[Verbose]}" -gt 0 ]] && echo ">>>Debug print"
)
function LoadConfigFile() {
local cfgFile="${1}"
shopt -s extglob #Needed to remove trailing spaces
if [ -f ${cfgFile} ]; then
while IFS='=' read -r key value; do
if [[ "${key:0:1}" == "#" ]]; then
#echo "Skipping Comment line: ${key}"
elif [ "${key:-EMPTY}" != "EMPTY" ]; then
value="${value%%\#*}" # Delete in-line, right comments
value="${value%%*( )}" # Delete trailing spaces
value="${value%%( )*}" # Delete leading spaces
#value="${value%\"*}" # Delete opening string quotes
#value="${value#\"*}" # Delete closing string quotes
#Manipulate any variables included in the value so that they can be expanded correctly
# - value must be stored in the format: "${var1}". `backticks`, "$var2", and "doubleQuotes" are left as is
value="${value//\"/\\\"}" # Escape double quotes for eval
value="${value//\`/\\\`}" # Escape backticks for eval
value="${value//\$/\\\$}" # Escape ALL '$' for eval
value="${value//\\\${/\${}" # Undo the protection of '$' if it was followed by a '{'
value=$(eval "printf '%s\n' \"${value}\"")
config[${key}]=${value} #Store the value into the config array at the specified key
echo " >>>DBG: Key = ${key}, Value = ${value}"
#else
# echo "Skipped Empty Key"
fi
done < "${cfgFile}"
fi
}
CONFIG_FILE=${__AppDir}/${__AppName}.conf
echo "Config File # ${CONFIG_FILE}"
LoadConfigFile ${CONFIG_FILE}
#Print elements of $config
echo "Script Config Values:"
echo "----------------------------"
for key in "${!config[#]}"; do #The '!' char gets an array of the keys, without it, we would get an array of the values
printf " %-20s = %s\n" "${key}" "${config[${key}]}"
done
echo "------ End Script Config ------"
#To convert to an array...
declare -a valAsArray=${config[RequiredAppPackages]} #Convert the value from a string to an array
echo "Count = ${#valAsArray[#]}"
for itemCfg in "${valAsArray[#]}"; do
echo " item = ${itemCfg}"
done
As I mentioned before, I'm just starting to learn bash and Linux scripting in general, so if you see that I'm doing some taboo things in other areas of my code too, please feel free to provide feedback in the comments... I don't want to start bad habits early on :-).
*If it matters, the OS is Ubuntu 14.04.
EDIT:
As requested, after reading the script.conf file, I would like for the elements in $config[#] to be equivalent to the following:
typeset -A config #init config array
config=(
[key1]="value1"
[key2]="${HOME}/Folder"
[key3]="( \"k3v1\" \"k3 v2\" \"k3v3\" )"
[key4]="( \"k4v1\" \"k4 v2\" \"k4v3\" )"
[key5]="value5"
)
I want to be able to convert the values of elements 'key4' and 'key3' into an array and iterated over them the same way in the following code:
declare -a keyValAsArray=${config[keyN]} #Convert the value from a string to an array
echo "Count = ${#keyValAsArray[#]}"
for item in "${keyValAsArray[#]}"; do
echo " item = ${item}"
done
I don't think it matters if \n is preserved for key4's value or not... that depends on if declare has a problem with it.
A shell is an environment from which to call tools with a language to sequence those calls. It is NOT a tool to manipulate text. The standard UNIX tool to manipulate text is awk. Trying to manipulate text in shell IS a bad habit, see why-is-using-a-shell-loop-to-process-text-considered-bad-pr‌​actice for SOME of the reasons why
You still didn't post the expected result of populating the config array so I'm not sure but I think this is what you wanted:
$ cat tst.sh
declare -A config="( $(awk '
{ gsub(/^[[:space:]]+|([[:space:]]+|#.*)$/,"") }
!NF { next }
/^[^="]+=/ {
name = gensub(/=.*/,"",1)
value = gensub(/^[^=]+=/,"",1)
n2v[name] = value
next
}
{ n2v[name] = n2v[name] OFS $0 }
END {
for (name in n2v) {
value = gensub(/"/,"\\\\&","g",n2v[name])
printf "[%s]=\"%s\"\n", name, value
}
}
' script.conf
) )"
declare -p config
$ ./tst.sh
declare -A config='([key5]="value5" [key4]="( \"k4v1\" \"k4 v2\" \"k4v3\" )" [key3]="( \"k3v1\" \"k3 v2\" \"k3v3\")" [key2]="/home/Ed/Folder" [key1]="value1" )'
The above uses GNU awk for gensub(), with other awks you'd use [g]sub() instead.

bash: put list files into a variable and but size of array is 1

I am listing the files in a directory and looping through them okay, BUT I need to know how many there are too. ${#dirlist[#]} is always 1, but for loop works?
#!/bin/bash
prefix="xxx"; # as example
len=${#prefix}; # string length
dirlist=`ls ${prefix}*.text`;
qty=${#dirlist[#]}; # sizeof array is always 1
for filelist in $dirlist
do
substring="${filelist:$len:-5}";
echo "${substring}/${qty}";
done
I have files xxx001.text upto xxx013.text
but all I get is 001/1 002/1 003/1
This:
dirlist=`ls ${prefix}*.text`
doesn't make an array. It only makes a string with space separated file names.
You have to do
dirlist=(`ls ${prefix}*.text`)
to make it an array.
Then $dirlist will reference only the first element, so you have to use
${dirlist[*]}
to reference all of them in the loop.
Declare an array of files:
arr=(~/myDir/*)
Iterate through the array using a counter:
for ((i=0; i < ${#arr[#]}; i++)); do
# [do something to each element of array]
echo "${arr[$i]}"
done
You're not creating an array unless you surround it with ( ):
dirlist=(`ls ${prefix}*.text`)
dir=/tmp
file_count=`ls -B "$dir" | wc -l`
echo File count: $file_count
The array syntax in bash is simple, using parentheses ( and ):
# string
var=name
# NOT array of 3 elements
# delimiter is space ' ' not ,
arr=(one,two,three)
echo ${#arr[#]}
1
# with space
arr=(one two three)
# or ' ',
arr=(one, two, three)
echo ${#arr[#]}
3
# brace expansion works as well
# 10 elements
arr=({0..9})
echo ${#arr[#]}
10
# advanced one
curly_flags=(--{ftp,ssl,dns,http,email,fc,fmp,fr,fl,dc,domain,help});
echo ${curly_flags[#]}
--ftp --ssl --dns --http --email --fc --fmp --fr --fl --dc --domain --help
echo ${#curly_flags[#]}
12
if you want to run a command and store the output
# a string of output
arr=$(ls)
echo ${#arr[#]}
1
# wrapping with parentheses
arr=($(ls))
echo ${#arr[#]}
256
A more advanced / handy way is by using built-in bash commands mapfile or readarray and process substitution. here is is an example of using mapfile:
# read the output of ls, save it in the array name: my_arr
# -t Remove a trailing DELIM from each line read (default newline)
mapfile -t my_arr < <(ls)
echo ${#my_arr[#]}
256

Resources