Bash: Variable string contains variable string with special characters (e.g. "=", "'") - bash

I want to do a basic
if [[ "$string" =~ "$substring" ]]; then
echo "True"
else echo "False"
fi
However, the substring I am looking for looks like "THING = '$VAR '" and that just doesn't fly. Yes, $VAR is a variable in my code. Help?
Here's an example of what I am looking at (the $string is extracted from a file):
# $string = "THIS = stuff / more random letters taking up space MORE STUFF = '42 ' here's some numbers asdgh asdgjhd a;lkdjg asdlkja dddd 'a::' THING = 'R ' more specs = THIS and THAT END"
for VAR in V R I B WA NA HA1 HA4 HA5; do
substring="SUBSET = '$VAR '"
if [[ "$string" =~ "$substring" ]]; then
echo "True"
else echo "False"
fi
done
I apologize if this is not more illuminating. Perhaps having the variable in so many quotations is not actually bringing up the variable, but when I print out $substring, it says THING = 'V '.

Related

Bash check string that don't match on order

how to compare string in bash? I only want to compare words, not word order
for example i have variable
VAR1=eu-endpoint-2021.09.20 prod-store-2021.09.20 service-trace-2021.09.20
and another variable that stores the same info but with different order
VAR2=prod-store-2021.09.20 eu-endpoint-2021.09.20 service-trace-2021.09.20
and how can i compare this only by words? nor the words order
for example
if $VAR1 == $VAR2
then
do smth;
else
do smth;
fi
Since both your input string only contains parts that don't contain any spaces, we can
Convert the strings into arrays ($VAR1)
Loop over array1: Loop through an array of strings in Bash?
Check if current element exist in array2: Check if a Bash array contains a value
If not, set result to false, and break out of the loop
#!/bin/bash
VAR1='eu-endpoint-2021.09.20 prod-store-2021.09.20 service-trace-2021.09.20'
VAR2='prod-store-2021.09.20 eu-endpoint-2021.09.20 service-trace-2021.09.20'
ARR1=($VAR1)
ARR2=($VAR2)
RES=1
for i in "${ARR1[#]}"; do
[[ ! " ${ARR2[*]} " =~ " ${i} " ]] && RES=0 && break
done
[ $RES -eq 1 ] && echo 'Equal' || echo 'Not equal'
Will show Equal for the provided example strings as you can try here.
If you change any of the strings, you'll get Not equal as you can try here.
I'd just sort them then compare the result, e.g.:
$ VAR1='eu-endpoint-2021.09.20 prod-store-2021.09.20 service-trace-2021.09.20'
$ VAR2='prod-store-2021.09.20 eu-endpoint-2021.09.20 service-trace-2021.09.20'
$ if [[ $(tr ' ' '\n' <<<"$VAR1" | sort) = $(tr ' ' '\n' <<<"$VAR2" | sort) ]]; then echo same; else echo diff; fi
same

BAD SUBSTITUTION: String comparison using array elements in bash

I'm getting started with bash programming, and am stumbling through the syntax a little. The following is part of the code to play tic-tac-toe from the command line.
i=0
initialGrid=('_' '_' '_' '_' '_' '_' '_' '_' '_')
if [ "${ initialGrid[ $i ] }" = "_" ]; then
echo "I'm here"
fi
However I get the following error
tic.sh: line 48: ${ initialGrid[ $i ] }: bad substituion
So the echo never gets executed
I was able to substitute the above if statement and do the following without error
element=" $initialGrid[$i] "
if [ "${!element}" = "_" ]; then
echo "Element equals underscore"
fi
But again, the echo never gets execute even though i believe it clearly should
Spaces are not optional. You need to use:
if [[ "${initialGrid[i]}" == "_" ]]; then echo "I'm here"; fi
Spaces in bash almost always are special characters. In your first case:
i=0
initialGrid=('_' '_' '_' '_' '_' '_' '_' '_' '_')
if [ "${ initialGrid[ $i ] }" = "_" ]; then
echo "I'm here"
fi
You need to replace the line with the condition with
if [ "${initialGrid[$i]}" = "_" ]; then
In your second case, you are padding the value with spaces, but then trying to compare it to the value without spaces, _ is not the same as _. You are also not dereferencing the array correctly.
element=" $initialGrid[$i] "
if [ "${!element}" = "_" ]; then
echo "Element equals underscore"
fi
Replace the first line with element="${initialGrid[$i]}"

check for string format in bash script

I am attempting to check for proper formatting at the start of a string in a bash script.
The expected format is like the below where the string must always begin with "ABCDEFG-" (exact letters and order) and the numbers would vary but be at least 3 digits. Everything after the 3rd digit is a do not care.
Expected start of string: "ABCDEFG-1234"
I am using the below code snippet.
[ $(echo "$str" | grep -E "ABCDEFG-[0-9][0-9][0-9]") ] && echo "yes"
str1 = "ABCDEFG-1234"
str2 = "ABCDEFG-1234 - Some more text"
When I use str1 in place of str everything works ok and yes is printed.
When I use str2 in place of str i get the below error
[: ABCDEFG-1234: unary operator expected
I am pretty new to working with bash scripts so any help would be appreciated.
If this is bash, you have no reason to use grep for this at all; the shell has built-in regular expression support.
re="ABCDEFG-[0-9][0-9][0-9]"
[[ $str =~ $re ]] && echo "yes"
That said, you might want your regex to be anchored if you want a match in the beginning rather than anywhere in the content:
re="^ABCDEFG-[0-9][0-9][0-9]"
[[ $str =~ $re ]] && echo "yes"
That said, this doesn't need to be an ERE at all -- a glob-style pattern match would also be adequate:
if [[ $str = ABCDEFG-[0-9][0-9][0-9]* ]]; then echo "yes"; fi
Try grep -E "ABCDEFG-[0-9][0-9][0-9].*"

multiline regexp matching in bash

I would like to do some multiline matching with bash's =~
#!/bin/bash
str='foo = 1 2 3
bar = what about 42?
boo = more words
'
re='bar = (.*)'
if [[ "$str" =~ $re ]]; then
echo "${BASH_REMATCH[1]}"
else
echo no match
fi
Almost there, but if I use ^ or $, it will not match, and if I don't use them, . eats newlines too.
EDIT:
sorry, values after = could be multi-word values.
I could be wrong, but after a quick read from here, especially Note 2 at the end of the page, bash can sometimes include the newline character when matching with the dot operator. Therefore, a quick solution would be:
#!/bin/bash
str='foo = 1
bar = 2
boo = 3
'
re='bar = ([^\
]*)'
if [[ "$str" =~ $re ]]; then
echo "${BASH_REMATCH[1]}"
else
echo no match
fi
Notice that I now ask it match anything except newlines. Hope this helps =)
Edit: Also, if I understood correctly, the ^ or $ will actually match the start or the end (respectively) of the string, and not the line. It would be better if someone else could confirm this, but it is the case and you do want to match by line, you'll need to write a while loop to read each line individually.

How can I check if a string is in an array without iterating over the elements?

Is there a way of checking if a string exists in an array of strings - without iterating through the array?
For example, given the script below, how I can correctly implement it to test if the value stored in variable $test exists in $array?
array=('hello' 'world' 'my' 'name' 'is' 'perseus')
#pseudo code
$test='henry'
if [$array[$test]]
then
do something
else
something else
fi
Note
I am using bash 4.1.5
With bash 4, the closest thing you can do is use associative arrays.
declare -A map
for name in hello world my name is perseus; do
map["$name"]=1
done
...which does the exact same thing as:
declare -A map=( [hello]=1 [my]=1 [name]=1 [is]=1 [perseus]=1 )
...followed by:
tgt=henry
if [[ ${map["$tgt"]} ]] ; then
: found
fi
There will always technically be iteration, but it can be relegated to the shell's underlying array code. Shell expansions offer an abstraction that hide the implementation details, and avoid the necessity for an explicit loop within the shell script.
Handling word boundaries for this use case is easier with fgrep, which has a built-in facility for handling whole-word fixed strings. The regular expression match is harder to get right, but the example below works with the provided corpus.
External Grep Process
array=('hello' 'world' 'my' 'name' 'is' 'perseus')
word="world"
if echo "${array[#]}" | fgrep --word-regexp "$word"; then
: # do something
fi
Bash Regular Expression Test
array=('hello' 'world' 'my' 'name' 'is' 'perseus')
word="world"
if [[ "${array[*]}" =~ (^|[^[:alpha:]])$word([^[:alpha:]]|$) ]]; then
: # do something
fi
You can use an associative array since you're using Bash 4.
declare -A array=([hello]= [world]= [my]= [name]= [is]= [perseus]=)
test='henry'
if [[ ${array[$test]-X} == ${array[$test]} ]]
then
do something
else
something else
fi
The parameter expansion substitutes an "X" if the array element is unset (but doesn't if it's null). By doing that and checking to see if the result is different from the original value, we can tell if the key exists regardless of its value.
array=('hello' 'world' 'my' 'name' 'is' 'perseus')
regex="^($(IFS=\|; echo "${array[*]}"))$"
test='henry'
[[ $test =~ $regex ]] && echo "found" || echo "not found"
Reading your post I take it that you don't just want to know if a string exists in an array (as the title would suggest) but to know if that string actually correspond to an element of that array. If this is the case please read on.
I found a way that seems to work fine .
Useful if you're stack with bash 3.2 like I am (but also tested and working in bash 4.2):
array=('hello' 'world' 'my' 'name' 'is' 'perseus')
IFS=: # We set IFS to a character we are confident our
# elements won't contain (colon in this case)
test=:henry: # We wrap the pattern in the same character
# Then we test it:
# Note the array in the test is double quoted, * is used (# is not good here) AND
# it's wrapped in the boundary character I set IFS to earlier:
[[ ":${array[*]}:" =~ $test ]] && echo "found! :)" || echo "not found :("
not found :( # Great! this is the expected result
test=:perseus: # We do the same for an element that exists
[[ ":${array[*]}:" =~ $test ]] && echo "found! :)" || echo "not found :("
found! :) # Great! this is the expected result
array[5]="perseus smith" # For another test we change the element to an
# element with spaces, containing the original pattern.
test=:perseus:
[[ ":${array[*]}:" =~ $test ]] && echo "found!" || echo "not found :("
not found :( # Great! this is the expected result
unset IFS # Remember to unset IFS to revert it to its default value
Let me explain this:
This workaround is based on the principle that "${array[*]}" (note the double quotes and the asterisk) expands to the list of elements of array separated by the first character of IFS.
Therefore we have to set IFS to whatever we want to use as boundary (a colon in my case):
IFS=:
Then we wrap the element we are looking for in the same character:
test=:henry:
And finally we look for it in the array. Take note of the rules I followed to do the test (they are all mandatory): the array is double quoted, * is used (# is not good) AND it's wrapped in the boundary character I set IFS to earlier:
[[ ":${array[*]}:" =~ $test ]] && echo found || echo "not found :("
not found :(
If we look for an element that exists:
test=:perseus:
[[ ":${array[*]}:" =~ $test ]] && echo "found! :)" || echo "not found :("
found! :)
For another test we can change the last element 'perseus' for 'perseus smith' (element with spaces), just to check if it's a match (which shouldn't be):
array[5]="perseus smith"
test=:perseus:
[[ ":${array[*]}:" =~ $test ]] && echo "found!" || echo "not found :("
not found :(
Great!, this is the expected result since "perseus" by itself is not an element anymore.
Important!: Remember to unset IFS to revert it to its default value (unset) once you're done with the tests:
unset IFS
So so far this method seems to work, you just have to be careful and choose a character for IFS that you are sure your elements won't contain.
Hope it helps anyone!
Regards,
Fred
In most cases, the following would work. Certainly it has restrictions and limitations, but easy to read and understand.
if [ "$(echo " ${array[#]} " | grep " $test ")" == "" ]; then
echo notFound
else
echo found
fi
Instead of iterating over the array elements it is possible to use parameter expansion to delete the specified string as an array item (for further information and examples see Messing with arrays in bash and Modify every element of a Bash array without looping).
(
set -f
export IFS=""
test='henry'
test='perseus'
array1=('hello' 'world' 'my' 'name' 'is' 'perseus')
#array1=('hello' 'world' 'my' 'name' 'is' 'perseusXXX' 'XXXperseus')
# removes empty string as array item due to IFS=""
array2=( ${array1[#]/#${test}/} )
n1=${#array1[#]}
n2=${#array2[#]}
echo "number of array1 items: ${n1}"
echo "number of array2 items: ${n2}"
echo "indices of array1: ${!array1[*]}"
echo "indices of array2: ${!array2[*]}"
echo 'array2:'
for ((i=0; i < ${#array2[#]}; i++)); do
echo "${i}: '${array2[${i}]}'"
done
if [[ $n1 -ne $n2 ]]; then
echo "${test} is in array at least once! "
else
echo "${test} is NOT in array! "
fi
)
q=( 1 2 3 )
[ "${q[*]/1/}" = "${q[*]}" ] && echo not in array || echo in array
#in array
[ "${q[*]/7/}" = "${q[*]}" ] && echo not in array || echo in array
#not in array
#!/bin/bash
test="name"
array=('hello' 'world' 'my' 'yourname' 'name' 'is' 'perseus')
nelem=${#array[#]}
[[ "${array[0]} " =~ "$test " ]] ||
[[ "${array[#]:1:$((nelem-1))}" =~ " $test " ]] ||
[[ " ${array[$((nelem-1))]}" =~ " $test" ]] &&
echo "found $test" || echo "$test not found"
Just treat the expanded array as a string and check for a substring, but to isolate the first and last element to ensure they are not matched as part of a lesser-included substring, they must be tested separately.
if ! grep -q "$item" <<< "$itemlist" ; then .....
Should work fine.
for simple use cases I use something like this
array=( 'hello' 'world' 'I' 'am' 'Joe' )
word=$1
[[ " ${array[*]} " =~ " $word " ]] && echo "$word is in array!"
Note the spaces around ". This works as long as there are no spaces in the array values and the input doesn't match more values at once, like word='hello world'. If there are, you'd have to play with $IFS on top of that.

Resources