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]}"
Related
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
This is part of a bash script with line numbers shown. I can understand how getopts works in bash, but can't understand line 116. if [[ ! " ${FS_OPTIONS[#]} " =~ " $OPTARG " ]]; then part. Earlier in the script there was
#!/usr/bin/env bash
FS_OPTIONS=("ubuntu" "busybox")
while getopts "hsf:" opt; do
case $opt in
f)
if [[ ! " ${FS_OPTIONS[#]} " =~ " $OPTARG " ]]; then
echo "Unsupported filesystem $OPTARG"
echo "Use ubuntu/busybox"
exit -1
else
echo "ok!"
export FILESYSTEM=$OPTARG
fi
;;
esac
done
ckim#chan-ubuntu:~/testbash$ test3.sh -f ubuntu
ok!
ckim#chan-ubuntu:~/testbash$ test3.sh -f busybox
ok!
ckim#chan-ubuntu:~/testbash$ test3.sh -f ubunt.
Unsupported filesystem ubunt.
Use ubuntu/busybox
I changed the condition line to if [[ ! " ${FS_OPTIONS[#]} " =~ $OPTARG ]]; then and can see the right side is regarded as regular expression. Now I can see
ckim#chan-ubuntu:~/testbash$ test3.sh -f ubunt.
ok!
This is because the " " was removed and the argument was taken as regexp.
The bash manual says
An additional binary operator, =~, is available, with the same precedence as == and !=. When it is used,
the string to the right of the operator is considered an extended regular expression and matched accordingly
(as in regex(3)). The return value is 0 if the string matches the pattern, and 1 otherwise.
This got long before my question. My first question is : From what I observed above, when the pattern matches, the =~ returns 1. (it has ! before) as opposed to the manual. Am I missing something?
My second questions : What are some use cases of using the original if [[ ! " ${FS_OPTIONS[#]} " =~ " $OPTARG " ]]; then? exploiting regular expression? Because it has " ", it will take the argument as string(without taking it as regular expression). Is there any usefulness for =~ with " "?
What's the meaning of the specific line you asked about?
Breaking down what's done by [[ ! " ${FS_OPTIONS[#]} " =~ " $OPTARG " ]]:
" ${FS_OPTIONS[#]} " is, in this context, a less-clear equivalent to " ${FS_OPTIONS[*]} " (since the usual distinction between them would have [#] expanding to multiple words, which isn't legal in this context). Thus, in acting like ${FS_OPTIONS[*]}, we're expanding to the complete list of words in FS_OPTIONS, with a separator (first character in IFS, by default a single space) added between them. Also, note that we're adding whitespace at the beginning and end (so our OPTARG can be matched in those positions and not just in the middle).
" $OPTARG " pads the string we're looking for with whitespace on both sides, so we can't match a substring.
With the right-hand side quoted, we're doing a substring search, so we're checking if the contents of $OPTARG -- with added whitespace at the beginning and end -- exist anywhere in the full expanded list generated by ${FS_OPTIONS[*]}.
The ! inverts the logical exit status of the rest of the expression (changing a 0 to a 1 or a 1 to a 0).
What's the use of that syntax in general?
A quoted string on the RHS acts as a regular, unanchored substring search. That is, [[ $foo = "bar" ]] is true only if the variable foo expands to exactly bar, but [[ $foo =~ "bar" ]] is true if the variable foo expands to contain bar anywhere in its contents. This is a useful semantic on its own.
Quoting is determined character-by-character, not for the full string. As an example of how this can be used:
regex_pre='([[:space:]]|^)'
literal_string='** match this exactly **'
regex_post='([[:space:]]|$)'
[[ $foo =~ ${regex_pre}"${literal_string}"${regex_post} ]]
# | | |
# | | \-> unquoted: acts like a regex
# | \-> quoted: acts like a literal string
# \-> unquoted: acts like a regex
...matches ** match this exactly ** only when it's at the start of a string or immediately preceded by whitespace, and also either at the end or immediately succeeded by whitespace, without needing to write a regex for the literal string in between, and without needing to do the pad-with-whitespace trick shown in the question.
ckim#chan-ubuntu:~/testbash$ test3.sh -f ubuntu
ok!
ckim#chan-ubuntu:~/testbash$ test3.sh -f ubunt.
Unsupported filesystem ubunt.
Use ubuntu/busybox
The bash manual says (in the discussion about [[...]]):
Any part of the pattern may be quoted to force the quoted portion to be matched as a string
So, to treat the input as a regular expression, plus add literal spaces at the ends, quote the spaces but leave the variable unquoted:
[[ " ${FS_OPTIONS[*]} " =~ " "$OPTARG" " ]]
# .........................^^^-------^^^
Demonstrating:
$ OPTARG=ubuntu
$ [[ " ${FS_OPTIONS[*]} " =~ " "$OPTARG" " ]] && echo Y || echo N
Y
$ OPTARG=ubunt.
$ [[ " ${FS_OPTIONS[*]} " =~ " "$OPTARG" " ]] && echo Y || echo N
Y
$ OPTARG=ubunt
$ [[ " ${FS_OPTIONS[*]} " =~ " "$OPTARG" " ]] && echo Y || echo N
N
I'm trying to see if there's an easier way to get a substring from starting character to an end character to identify "date format" that it has, I have a solution that is working but I'm looking if there's a way much easier to do it.
EXAMPLE:
file_name=file_test_<yyyymmdd>_format.xls
SUBSTRING FROM "<" to ">"
<yyyymmdd>
this is what I have and is working:
file_name="test_file_<mmddyyyy>_test"
format_date=""
Char_Found=0
index=0
while [ $index -lt ${#file_name} ]; do
letter=$(echo $file_name | cut -c$(( $index + 1 )))
if [ $letter == "<" ]; then
Char_Found=1
fi
if [ $letter == ">" ]; then
format_date=${format_date}">"
break
fi
if (( ${Char_Found} )); then
format_date=${format_date}"${letter}"
fi
index=$(( $index + 1 ))
done
echo "Format string: ${format_date}"
#OUTPUT: <mmddyyyy>
The posted implementation is not only lengthy,
it's very inefficient, due to calling cut in a loop,
for every character.
A simpler, far more efficient solution is possible with what is called parameter expansion in Bash,
which seems to work just fine in ksh too:
file_name="test_file_<mmddyyyy>_test"
format_date=$file_name
format_date="<${format_date#*<}" # chop off the beginning until <
format_date="${format_date%%>*}>" # chop off the end from >
echo "Format string: ${format_date}"
Outputs:
Format string: <mmddyyyy>
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 '.
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.