Iterate array in bash v.3 - arrays

I have file.properties, where are keys and values defined:
key1=value1 value2 value3
key2=value4 value5 value6
key3=value7 value8 value9
I want to load lines from the file to array. I cannot use declare -A to create associative array because of bash 3.
In bash 4 my code is:
conf="./file.properties"
init() {
local configiration=$1
if [ -f "$conf" ]
then
while IFS='=' read -r key value
do
configuration[ ${key} ]=$value
done < "$conf"
fi}
declare -A configuration
init configuration
KEY="key value from file"
#KEY="key1" or KEY="key2" or KEY="key3"
SERVERS=${configuration[ $KEY ]}
I want to do the same job in bash 3.
conf="./file.properties"
init () {
key=()
value=()
if [ -f "$conf" ]
then
while IFS='=' read -r key value
do
key+=("$key")
value+=("$value")
done < "$conf"
fi}
init
KEY="key value from file"
#KEY="key1" or KEY="key2" or KEY="key3"
for ((i = 0; i < ${#key[#]}; i++))
do
if [ "${key[$i]}" == "$KEY" ];
then
echo "values: ${value[i]}"
fi
done
In both cases I get the same output, e.g.:
value1 value2 value3
Is my code for bash 3 correct or something has to be revised?

This looks correct to me, as you use an appropriate way to iterate through your arrays considering you just created them prior and know that the arrays' keys will be in 0,1,...,n-1 for your arrays of size n. The array+=(elements) operator always places elements into array starting at array's last index + 1.
The only case where I would iterate through an array differently is if it were not guaranteed that the indices are in [0,n). For example:
declare -ai roots=();
declare -i x y;
for ((x=0; x<4; roots[x*x]=x,++x)) do :; done;
for y in "${!roots[#]}"; do
printf 'sqrt(%d) = %d\n' $y ${roots[y]};
done;
By expanding the indices of roots using "${!roots[#]}"to iterate across it, we get the following output:
sqrt(0) = 0
sqrt(1) = 1
sqrt(4) = 2
sqrt(9) = 3
In cases like this, we have to avoid the arithmetic for loop to avoid something like the following from occurring:
$ for((i=0;i<${#roots[#]};++i)); do echo "${roots[i]}"; done;
0
1
 
 
The only change I would make is to use [[ instead of [ to perform string comparison, as it is a bash builtin which is safer and more efficient to use in general. Also, I believe you need either a newline or a ; before your } to close your init function.

Related

Loop over indices of sliced array in bash

I want to loop over the indices of an array starting on the second index. How can I do this?
I have tried:
myarray=( "test1" "test2" "test3" "test4")
for i in ${!myarray[#]:1}
do
# I only print the indices to simplify the example
echo $i
done
But doesn't work.
Obviously this works:
myarray=( "test1" "test2" "test3" "test4")
myindices=("${!myarray[#]}")
for i in ${myindices[#]:1}
do
echo $i
done
But I would like to combine everything in the for loop statement if possible.
Use the # parameter length expansion:
myarray=( "test1" "test2" "test3" "test4")
for (( i=1; i < ${#myarray[#]}; i++ ))
do
# only print the indices to simplify the example
echo $i
done
Note that the ! indirect expansion operator is evidently not compatible with substring expansion since:
echo "${!myarray[#]:2}"
Produces an error code 1 and outputs to STDERR:
bash: test1 test2 test3 test4: bad substitution
At least for current versions of bash, v.4.4 and earlier. Unfortunately man bash doesn't make it sufficiently clear that substring expansion doesn't work with indirect expansion.
I'd do it this way:
#!/usr/bin/env bash
myarray=('a' 'b' 'c' 'd')
start_index=2
# generate a null delimited list of indexes
printf '%s\0' "${!myarray[#]}" |
# slice the indexes list 2nd entry to last
cut --zero-terminated --delimiter='' --fields="${start_index}-" |
# iterate the sliced indexes list
while read -r -d '' i; do
echo "$i"
done
Output does not list first index 0 as expected:
1
2
3
Works as well with an associative array:
#!/usr/bin/env bash
typeset -A myassocarray=(["foo"]='a' ["bar"]='b' ["baz"]='c' ["qux"]='d')
start_index=2
# generate a null delimited list of indexes
printf '%s\0' "${!myassocarray[#]}" |
# slice the indexes list 2nd entry to last
cut --zero-terminated --delimiter='' --fields="${start_index}-" |
# iterate the sliced indexes list
while read -r -d '' k; do
echo "$k"
done
Output:
bar
baz
qux

How do you unset all empty array elements in bash? [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

Remove elements from bash array by element property

In bash, if I have an array of file names which I retrieve from a file like this:
readarray files < $MY_DIR/my_file_list.cfg
How can i remove any elements in the array files which are say shorter than four characters long?
Loop about every element of your array and check its element length and remove element if necessary.
for ((i=0;i<=${#files[#]};i++)); do
[[ ${#files[$i]} -lt 4 ]] && unset files[$i]
done
declare -p
${#files[#]}: number of elements in array files
${#files[$i]}: length of element $i
-lt: arithmetic test less-than
Test code, using bash substring expansion:
printf "abc\nabcdefg\na\nabcd\n" | while read x ; do [ "${x:4}" ] && echo $x ; done
Output:
abcdefg
abcd
Therefore:
Pure bash code:
readarray files < <(while read x ; do [ "${x:4}" ] && echo $x ; done < \
$MY_DIR/my_file_list.cfg )
Simpler grep aided code:
readarray files < <(grep '^....' $MY_DIR/my_file_list.cfg)
These methods waste no array memory on too-short entries. The worst case would be if my_file_list.cfg contained only one long name, but billions of short names. (Billions of short names might exhaust bash's memory, or at least lead to swapping, or thrashing.)

Remove (not just unset) multiple strings from an array without knowing their positions

Say I have arrays
a1=(cats,cats.in,catses,dogs,dogs.in,dogses)
a2=(cats.in,dogs.in)
I want to remove everything from a1 that matches the strings in a2 after removing ".in" , in addition to the ones that match completely(including ".in").
So from a1, I want to remove cats, cats.in, dogs, dogs.in, but not catses or dogses.
I think I'll have to do this in 2 steps. I found how to cut the ".in" away:
for elem in "${a2[#]}" ; do
var="${elem}"
len="${#var}"
pref=${var:0:len-3}
done
^ this gives me "cats" and "dogs"
What command do I need to add to the loop remove each elem from a1?
Seems to me that the easiest way to solve this is with nested for loops:
#!/usr/bin/env bash
a1=(cats cats.in catses dogs dogs.in dogses)
a2=(cats.in dogs.in)
for x in "${!a1[#]}"; do # step through a1 by index
for y in "${a2[#]}"; do # step through a2 by content
if [[ "${a1[x]}" = "$y" || "${a1[x]}" = "${y%.in}" ]]; then
unset a1[x]
fi
done
done
declare -p a1
But depending on your actual data, the following might be better, using two separate for loops instead of nesting.
#!/usr/bin/env bash
a1=(cats cats.in catses dogs dogs.in dogses)
a2=(cats.in dogs.in)
# Flip "a2" array to "b", stripping ".in" as we go...
declare -A b=()
for x in "${!a2[#]}"; do
b[${a2[x]%.in}]="$x"
done
# Check for the existence of the stripped version of the array content
# as an index of the associative array we created above.
for x in "${!a1[#]}"; do
[[ -n "${b[${a1[x]%.in}]}" ]] && unset a1[$x] a1[${x%.in}]
done
declare -p a1
The advantage here would be that instead of looping through all of a2 for each item in a1, you just loop once over each array. Down sides might depend on your data. For example, if contents of a2 are very large, you might hit memory limits. Of course, I can't know that from what you included in your question; this solution works with the data you provided.
NOTE: this solution also depends on an associative array, which is a feature introduced to bash in version 4. If you're running an old version of bash, now might be a good time to upgrade. :)
This is the solution I went with:
for elem in "${a2[#]}" ; do
var="${elem}"
len="${#var}"
pref=${var:0:len-3}
#set 'cats' and 'dogs' to ' '
for i in ${!a1[#]} ; do
if [ "${a1[$i]}" = "$pref" ] ; then
a1[$i]=''
fi
#set 'cats.in' and 'dogs.in' to ' '
if [ "${a1[$i]}" = "$var" ] ; then
a1[$i]=''
fi
done
done
Then I created a new array from a1 without the ' ' elements
a1new=( )
for filename in "${a1[#]}" ; do
if [[ $a1 != '' ]] ; then
a1new+=("${filename}")
fi
done
A naive approach would be:
#!/bin/bash
# Checkes whether a value is in an array.
# Usage: "$value" "${array[#]}"
inarray () {
local n=$1 h
shift
for h in "$#";do
[[ $n = "$h" ]] && return
done
return 1
}
a1=(cats cats.in catses dogs dogs.in dogses)
a2=(cats.in dogs.in)
result=()
for i in "${a1[#]}";do
if ! inarray "$i" "${a2[#]}" && ! inarray "$i" "${a2[#]%.in}"; then
result+=("$i")
fi
done
# Checking.
printf '%s\n' "${result[#]}"
If you only want to print the values to stdout, you might instead want to use comm:
comm -23 <(printf '%s\n' "${a1[#]}"|sort -u) <(printf '%s\n' "${a2[#]%.in}" "${a2[#]}"|sort -u)

How to allocate array which is passed to a function by name

I want to create a function which iterates through an array and inserts the given value, if its not yet included. I am using this function on two different parts of my code and so I have to use different arrays. Therefore I am delivering the array names to the function. Now I can't figure out how to allocate a slot of the array to store the element at this place.
Here is my code:
name=("hello" "world")
function f {
array_name=$2[#]
array=("${!array_name}")
length=${#array_name[#]}
for (( i = 0; i <= $length; i++ )); do
if [[ "${array[i]}" = "$1" ]];
break;
fi
if [[ "$i" = "$length" ]]; then
${2[$length+1]=$1};
fi
done
}
f "test" "name"
Edit: I want the array to append the given value so something like this
for i in ${name[#]}
do
echo $i
done
would have this output:
hello
world
test
but apparently "${2[$length+1]=$1}" is not working.
(Idea for passing the array is from here: bash how to pass array as an argument to a function)
If I understand correctly you want to append a value to an array if this value is not yet in the array, but the tricky part is that the array name is passed to the function.
Method 1
One possibility is to see the problem from a different viewpoint: you can pass to the function the value to be inserted and the full array, and the function will set a global variable that you will recover after its execution. For our test example we'll use:
array=( hello world "how are you today?" )
and we'll try to insert test and hello (so that hello will not be inserted):
f() {
# This function will set the global variable _f_out to an array obtained from
# the positional parameters starting from position 2, with $1 appended if and
# only if $1 doesn't appear in the subsequent positional parameters
local v=$1 i
shift
_f_out=( "$#" )
for i; do [[ $i = $v ]] && return; done
_f_out+=( "$v" )
}
Let's use it:
$ array=( hello world "how are you today?" )
$ f test "${array[#]}"
$ array=( "${_f_out[#]}" )
$ printf '%s\n' "${array[#]}"
hello
world
how are you today?
test
$ f hello "${array[#]}"
$ array=( "${_f_out[#]}" )
$ printf '%s\n' "${array[#]}"
hello
world
how are you today?
test
It works.
Remark. I used for i; do. It's a nice shortcut for for i in "$#"; do.
Method 2
You really want to fiddle with simili-pointers and do the appending in place (this is not really in the spirit of Bash—that's why it's a bit clumsy). The tool for that is to use printf with the -v option: from help printf:
-v var assign the output to shell variable VAR rather than
display it on the standard output
The good thing is that it also works with array fields.
Warning: you might see other methods that use eval. Avoid them like the plague!
f() {
local array_name=$2[#] i
local array=( "${!array_name}" )
for i in "${array[#]}"; do [[ $i = $1 ]] && return; done
# $1 was not found in array, so let's append it
printf -v "$2[${#array[#]}]" '%s' "$1"
}
and let's try it:
$ array=( hello world "how are you today?" )
$ f test array
$ printf '%s\n' "${array[#]}"
hello
world
how are you today?
test
$ f hello array
$ printf '%s\n' "${array[#]}"
hello
world
how are you today?
test
It works too.
Note. With both methods you can very easily have a return code of the function, e.g., 0 (success) if the value was inserted, and 1 (failure) if the value was already there (or the other way round)—the adaptation is straightforward and left as an exercise. This might be useful in Method 1 to determine whether you need to update array to the returned value _f_out or not. In that case, you could even slightly modify f so that it doesn't even set _f_out when the value is already in the array.
Caveat. In both methods shown I assumed that your arrays have contiguous indices that start at 0 (i.e., non-sparse arrays). I think it's a safe assumption here; but if it is not the case, these methods are broken: the first one will (after reassignment) transform the array into a non-sparse one, and the second one might overwrite fields (as, for an array a, ${#a[#]} expands to the number of elements in the array, not the highest index + 1 found in the array).

Resources