bash array substitution broken on for loop - arrays

I have this piece of bash code in a script:
[..]
ARRAY=("foo-2" "foo-3" "foo-4")
IGNORE=2
while sleep 2
do
for ii in "${ARRAY[#]/foo-${IGNORE}/}"
do
echo $ii
done
[..]
done
This piece of code on its own works. But in my full script a weird thing happens:
First loop of sleep runs well. 2nd iteration however, uses the ${ARRAY[#]/foo-${IGNORE}/} as a literal string. Making a single echo command in the for loop.
Debugging the for loop:
Works:
for module in ${ARRAY[#]}
Also works:
for module in $(echo ${ARRAY[#]} | xargs -n 1 | grep -v foo-${IGNORE})
What do you think might be the problem? Is it some sort of bash limitation? Is there any other way you can suggest I do this?
Thanks!

"${ARRAY[#]/foo-${IGNORE}/}" should expand to multiple elements but one of them would just be an empty string and not completely ignored.
See this:
# A=(1 2 3)
# printf '|%s|\n' "${A[#]//1/}"
||
|2|
|3|
It also sounds like your array doesn't expand at all. Probably cause of it is that your shell is not being run by Bash.
Anyway better just compare your element instead:
for ii in "${ARRAY[#]}"
do
[[ $ii == "foo-$IGNORE" ]] && continue
echo $ii
done
You can also use globbing:
for ii in "${ARRAY[#]}"
do
[[ $ii == *"foo-$IGNORE"* ]] && continue
echo $ii
done

Related

Does array 1 contain any of the strings in array 2

I am trying to make an if statement where if array1 contains any of the strings in array2 it should print "match" else print "no match"
So far I have the following. Not sure how to complete it. Both loops should break as soon as the first match is found.
#!/bin/bash
array1=(a b c 1 2 3)
array2=(b 1)
for a in "${array1[#]}"
do
for b in "${array2[#]}"
do
if [ "$a" == "$b" ]; then
echo "Match!"
break
fi
done
done
Maybe this isn't even the best way to do it?
This illustrates the desired result
if [ array1 contains strings in array2 ]
then
echo "match"
else
echo "no match"
fi
To check whether array1 contains any entry from array2 you can use grep. This will be way faster and shorter than loops in bash.
The following commands exit with status code 0 if and only if there is a match. Use them as ...
if COMMAND FROM BELOW; then
echo match
else
echo no match
fi
Single-Line Array Entries
The simple version for strings without linebreaks is
printf %s\\n "${array1[#]}" | grep -qFxf <(printf %s\\n "${array2[#]}")
Multiline Array Entries
Sadly there doesn't seem to be a straightforward way to make this work for array entries with linebreaks. GNU grep has the option -z to set the "line" delimiters in the input to null, but apparently no option to do the same for the file provided to -f. Listing the entries from array2 as -e arguments to grep is not working either -- grep -F seems to be unable to match multiline patterns. However, we can use the following hack:
printf %q\\n "${array1[#]}" | grep -qFxf <(printf %q\\n "${array2[#]}")
Here we assume that bash's built-in printf %q always prints a unique single line -- which it currently does. However, future implementations of bash may change this. The documentation help printf only states that the output thas to be correctly quoted for bash.
For a fast solution, you're better off using an external tool that can process the entire array as a whole (such as the grep-based answers). Doing nested loops in pure bash is likely to be slower for any substantial amount of data (where the item-by-item processing in bash is likely to be more expensive than the external process start-up time).
However, if you do need a pure bash solution, I see that your current solution has no way to print out the "no match" scenario. In addition, it may print out "match" multiple times.
To fix that, you can just store the fact that a match has been found, and use that to both:
exit the outer loop early as well as the inner loop; and
print the correct string at the end.
To do this, you can use something like:
#!/bin/bash
# Test data.
array1=(a b c 1 2 3)
array2=(b 1)
# Default to not-found state.
foundMatch=false
for a in "${array1[#]}" ; do
for b in "${array2[#]}" ; do
# Any match switches to found state and exits inner loop.
[[ "$a" == "$b" ]] && foundMatch=true && break
done
# If found, exit outer loop as well.
${foundMatch} && break
done
# Output appropriate message for found/not-found state.
$foundMatch && echo "Match" || echo "No match"
For array elements which does not contain newlines, the grep -qf with printf "%s\n" would be a good option. For comparing arrays with any elements, I ended with this:
cmp -s /dev/null <(comm -z12 <(printf "%s\0" "${array1[#]}" | sort -z) <(printf "%s\0" "${array2[#]}" | sort -z))
The printf "%s\0" "${array[#]}" | sort -z print a sorted list of zero terminated array elements. The comm -z12 then extracts common elements in both lists. The cmp -s /dev/null checks if the output of comm is empty, which will not be empty if any element is in both lists. You could use [ -z "$(comm -z ...)" ] to check if the output of comm would be empty, but bash will complain that the output of a command captured with $(..) contains a null byte, so it's better to cmp -s /dev/null.
I think | is faster then <(), so your if could be:
if ! printf "%s\0" "${array1[#]}" | sort -z |
comm -z12 - <(printf "%s\0" "${array2[#]}" | sort -z) |
cmp -s /dev/null -; then
echo "Some elements are in both array1 and array2"
fi
The following could work:
printf "%s\0" "${array1[#]}" | eval grep -qzFx "$(printf " -e %q" "${array2[#]}")"
But I believe I found a bug in grepv3.1 when matching a newline character with -x flag. If you don't use the newline character, the above line works.
Would you try the following:
array1=(a b c 1 2 3)
array2=(b 1)
declare -A seen # set marks of the elements of array1
for b in "${array2[#]}"; do
(( seen[$b]++ ))
done
for a in "${array1[#]}"; do
(( ${seen[$a]} )) && echo "match" && exit
done
echo "no match"
It may be efficient by avoiding the double loop, although the discussion of efficiency may be meaningless as long as using bash :)

Bash use variables as indexes to array

how to assign to a variable in Bash an array element such that the array is index itself by a variable (a most trivial thing in any language but seems special in bash?)
I tried many variants such as:
let temp1=list1[list1Index]
or
temp1=${list1[$list1Index]}
none of them work obviously. No error but nothing is displayed by echo $temp1
What can I do?
here is the full code (with dummy names for paths):
#! /bin/bash
pathFrontalGray='/mnt/c/Users/dummyName/'
list1=('x' 'y' 'z' 'a' 'b' 'c')
prefix1='dummyName'
echo $pathFrontalGray
touch fileNamesGray.txt
ls $pathFrontalGray > fileNamesGray.txt
fileGray='fileNamesGray.txt'
list1Index=0
i=0
while IFS= read -r line
do
let i=i+1
echo $i
#means if NOT divisibe by 9 -> if <expr evaluated to 0> -> equiv if false
if (($i % 9));then
let list1Index=list1Index+1
fi
temp1=$list1[$((list1Index))]
echo $temp1
#mv $pathFrontalGray$line $prefix1${list1[$list1Index]}
# display $line or do somthing with $line
#printf '%s\n' "$line"
done <"$fileGray"
I found the problem. I m sorry in fact
curr1=${list1[index1]}
works fine. The problem was that the echo of that got lost in lots of other stuff displayed. Sorry for the inconvenience. I guess I was a bit tired...

grep through array in bash

Is it possible to grep an array in bash? Basically I have two arrays, one array I want to loop through and the other array I want to check to see if each string is in the other array or not.
Rough example:
for CLIENTS in ${LIST[#]}; do
#lost as to best way to grep a array with out putting the values in a file.
grep ${server_client_list[#]}
done
You can use grep -f using process substitution:
grep -Ff <(printf "%s\n" "${LIST[#]}") <(printf "%s\n" "${server_client_list[#]}")
Bash provides substring matching. This eliminates the need to spawn external processes and subshells. Simply use:
for CLIENTS in ${LIST[#]}; do
if [[ "${server_client_list[#]}" =~ "$CLIENTS" ]]; then
echo "CLIENTS ($CLIENTS) is in server_client_list"
fi
done
Note: this works for bash, not sh.
You could use the following function :
function IsInList {
local i
for i in "${#:2}"; do
[ "$i" = "$1" ] && return 0
done
return 1
}
for CLIENT in "${LIST[#]}"; do
IsInList "$CLIENT" "${server_client_list[#]}" && echo "It's in the list!"
done
Note that the quotes around ${LIST[#]} and ${server_client_list[#]} are very important because they guarantee the array expansion to expand the way you want them to, instead of having a bad surprise when a space make its way into one of those array values.
Credits : I took this function from patrik's answer in this post when I needed something like that.
Edit :
If you were looking for something with regexp capabilities, you could also use the following :
function IsInList {
local i
for i in "${#:2}"; do
expr match "$i" "$1" && return 0
done
return 1
}

Array returns false when i know it should be true

I know that this particular volume should be coming out as true but it keeps returning as false. Can anyone see what is wrong with it? My guess is it has something to do with the if statement but i have no idea what. the "3D/Gather" are 2 seperate strings but i want it so that if they are together then it returns true. This is the start of what is going to be quite a long script so i want to make it a function that i can call upon later on. Thanks in advance for your help.
#!/bin/bash
session_path=$PWD/testSess/3DBPvol.proc
outPath=""
sess=$session_path
{
ThreeD="false"
while read line; do
IFS=" "
arr=$(echo ${line})
unset IFS
for i in ${arr[#]} ; do
if [[ "$i" =~ "3D" ]] && [[ "$i" =~ "Gather" ]]; then
ThreeD="true"
fi
done
done < "$sess"
echo "Is 3D? $ThreeD"
}
arr=$(echo ${line})
This doesn't create an array, you'd need extra parens:
arr=($(echo ${line}))
But you don't actually need the echo, this should be enough:
arr=(${line})
So you want to see if the file under /proc contains "3D Gather"? You can do that simply with a grep:
#!/bin/bash
session_path=$PWD/testSess/3DBPvol.proc
grep -q '3D Gather' "$session_path" && ThreeD=true || ThreeD=false
echo "Is 3D? $ThreeD"
It seems you don't need to break the line into a BASH array. Consider this rafactored script that does the same job but with a lot less code:
ThreeD="false"
while read line; do
[[ "$line" == *"3D Gather"* ]] && ThreeD="true" && break
done < "$sess"
echo "Is 3D? $ThreeD"

Returning array from a Bash function

I am making a bash script and I have encountered a problem. So let's say I got this
function create_some_array(){
for i in 0 1 2 3 .. 10
do
a[i]=$i
done
}
create_some_array
echo ${a[*]}
Is there any way I can make this work? I have searched quite a lot and nothing I found worked.
I think making the a[] a global variable should work but I can't find something that actually works in my code. Is there any way to return the array from the function to main program?
Thanks in advance
This won't work as expected when there are whitespaces in the arrays:
function create_some_array() {
local -a a=()
for i in $(seq $1 $2); do
a[i]="$i $[$i*$i]"
done
echo ${a[#]}
}
and worse: if you try to get array indices from the outside "a", it turns out to be a scalar:
echo ${!a[#]}
even assignment as an array wont help, as possible quoting is naturally removed by the echo line and evaluation order cannot be manipulated to escape quoting: try
function create_some_array() {
...
echo "${a[#]}"
}
a=($(create_some_array 0 10))
echo ${!a[#]}
Still, printf seems not to help either:
function create_some_array() {
...
printf " \"%s\"" "${a[#]}"
}
seems to produce correct output on one hand:
$ create_some_array 0 3; echo
"0 0" "1 1" "2 4" "3 9"
but assignment doesn't work on the other:
$ b=($(create_some_array 0 3))
$ echo ${!b[#]}
0 1 2 3 4 5 6 7
So my last trick was to do assignment as follows:
$ eval b=("$(create_some_array 0 3)")
$ echo -e "${!b[#]}\n${b[3]}"
0 1 2 3
3 9
Tataaa!
P.S.: printf "%q " "${a[#]}" also works fine...
This works fine as described. The most likely reason it doesn't work in your actual code is because you happen to run it in a subshell:
cat textfile | create_some_array
echo ${a[*]}
would not work, because each element in a pipeline runs in a subshell, and
myvalue=$(create_some_array)
echo ${a[*]}
would not work, since command expansion happens in a subshell.
You can make an array local to a function, and then return it:
function create_some_array(){
local -a a=()
for i in $(seq $1 $2); do
a[i]=$i
done
echo ${a[#]}
}
declare -a a=()
a=$(create_some_array 0 10)
for i in ${a[#]}; do
echo "i = " $i
done
Hi here is my solution:
show(){
local array=()
array+=("hi")
array+=("everything")
array+=("well?")
echo "${array[#]}"
}
for e in $(show);do
echo $e
done
Try this code on: https://www.tutorialspoint.com/execute_bash_online.php
Both these work for me with sh and bash:
arr1=("192.168.3.4" "192.168.3.4" "192.168.3.3")
strArr=$(removeDupes arr1) # strArr is a string
for item in $strArr; do arr2+=("$item"); done # convert it to an array
len2=${#arr2[#]} # get array length
echo "${len2}" # echo length
eval arr3=("$(removeDupes arr1)") # shellcheck does not like this line and won't suppress it but it works
len3=${#arr3[#]} # get array length
echo "${len3}" # echo length
As an aside, the removeDupes function looks like this:
removeDupes() {
arg="$1[#]"
arr=("${!arg}")
len=${#arr[#]}
resultArr=()
# some array manipulation here
echo "${resultArr[#]}"
}
This answer is based on but better explains and simplifies the answers from #Hans and #didierc

Resources