How to match single character on bash script - bash

I want to write a bash script to read an input from user and check whether the input matches a single character. Below is the code.
read letter
if [[ $letter =~ ([a-zA-Z]) ]]
then
echo correct
fi
When I input two characters it still says "correct", see below output:
$ sh tmp.sh
aa
correct
how to write regular expression to matches exactly one character?

Your regex is not using anchors hence it will match any input that has [a-zA-Z] but it will match anything that is greater than 1 in length.
Having said that you don't even need regex and can use glob pattern here which will match exactly one alphabet:
if [[ $letter == [a-zA-Z] ]]
then
echo 'correct'
fi
If you must use regex then use:
if [[ $letter =~ ^[a-zA-Z]$ ]]
then
echo 'correct'
fi

Given that you probably want to take different actions depending on the value, you may want to consider a simple case statement:
case $letter in [a-zA-Z]) echo correct;; *) : do nothing;; esac
This will make it easy change to things like
case $letter in
a) : do something for a;;
[b-m]) : do something else;;
[n-zA-Z) : ... ;;
* ) ... ;;
esac

Related

In Bash, is it possible to match a string variable containing wildcards to another string

I am trying to compare strings against a list of other strings read from a file.
However some of the strings in the file contain wildcard characters (both ? and *) which need to be taken into account when matching.
I am probably missing something but I am unable to see how to do it
Eg.
I have strings from file in an array which could be anything alphanumeric (and include commas and full stops) with wildcards : (a?cd, xy, q?hz, j,h-??)
and I have another string I wish to compare with each item in the list in turn. Any of the strings may contain spaces.
so what I want is something like
teststring="abcdx.rubb ish,y"
matchstrings=("a?cd" "*x*y" "q?h*z" "j*,h-??")
for i in "${matchstrings[#]}" ; do
if [[ "$i" == "$teststring" ]]; then # this test here is the problem
<do something>
else
<do something else>
fi
done
This should match on the second "matchstring" but not any others
Any help appreciated
Yes; you just have the two operands to == reversed; the glob goes on the right (and must not be quoted):
if [[ $teststring == $i ]]; then
Example:
$ i=f*
$ [[ foo == $i ]] && echo pattern match
pattern match
If you quote the parameter expansion, the operation is treated as a literal string comparison, not a pattern match.
$ [[ foo == "$i" ]] || echo "foo != f*"
foo != f*
Spaces in the pattern are not a problem:
$ i="foo b*"
$ [[ "foo bar" == $i ]] && echo pattern match
pattern match
You can do this even completely within POSIX, since case alternatives undergo parameter substitution:
#!/bin/sh
teststring="abcdx.rubbish,y"
while IFS= read -r matchstring; do
case $teststring in
($matchstring) echo "$matchstring";;
esac
done << "EOF"
a?cd
*x*y
q?h*z
j*,h-??
EOF
This outputs only *x*y as desired.

bash case statement quoting behaves as documented, but please explain

c.f. the case example here
Specifically:
$: g=a*[b]; case $g in $g) echo 'unquoted pattern' ;; "$g") echo 'quoted pattern' ;; esac
quoted pattern
A simple string behaves exactly as expected -
$: g=a; case $g in $g) echo 'unquoted pattern' ;; "$g") echo 'quoted pattern' ;; esac
unquoted pattern
I made sure there was no matching file to glob, no shopt options muddying the water, and tested the value explicitly:
$: echo #$g# "#$g#" $g "$g"
#a*[b]# #a*[b]# a*[b] a*[b]
$: [[ $g == "$g" ]] && echo ok || echo no
ok
So why does it, in this case, choose the second option?
Shouldn't both evaluate to the same results?
The case statement uses pattern matching, not string equality, to compare the word to each pattern.
The pattern a*[b] matches any string starting with an a, with zero or more characters followed by a single b. It does not match the string a*[b], because that string ends with a ], not a b.

Shell Scripting - Numeric Checks and if statement questions

I'm relatively new here and to the coding world. I'm currently taking a class in Shell Scripting and I'm a bit stuck.
I'm trying to do a little extra credit and get the script to check for command line arguments and if none or only 1 is given, prompt the user to input the missing values.
For the most part I've been able to get most of it to work except for when it comes to the numeric check part. I'm not completely sure that I am doing the nested if statements correctly because it's displaying both the "if" echo and the "else" echo.
My script so far:
q=y
# Begins loop
until [[ $q == n ]];do
# Checks command line arguments
if [[ $# -lt 2 ]];then
# Asks for second number if only 1 argument.
if [[ $# == 1 ]];then
read -r -p "Please enter your second number: " y
if [[ y =~ [1-9] ]];then
echo "You've chosen $1 as your first number and $y as your second number."
break
else
echo "This is not a valid value, please try again."
fi
# Asks for both numbers if no arguments.
else
read -r -p "Please enter your first number: " x
if [[ x =~ [1-9] ]];then
break
else
echo "This is not a valid value, please try again."
fi
read -r -p "Please enter your second number: " y
if [[ y =~ [1-9] ]];then
break
else
echo "This is not a valid value, please try again."
fi
echo "You've chosen $x as your first number and $y as your second number."
fi
# If both command line arguments are provided, echo's arguments, and sets arguments as x and y values.
else
echo "You've chosen $1 as your first number and $2 as your second number."
x=$1
y=$2
fi
read -r -p "Would you like to try again? (n to exit): " q
done
When I run it I get this for output:
Please enter your first number: 1
This is not a valid value, please try again.
Please enter your second number: 2
This is not a valid value, please try again.
You've chosen 1 as your first number and 2 as your second number.
Please enter your first number:
And will just continue to loop without breaking. Any help/guidance would be greatly appreciated, thank you.
In your expression:
if [[ x =~ [1-9] ]]; then
You are actually comparing the string literal "x" with the regex. What you want is the variable:
if [[ $x =~ [1-9] ]]; then
This will interpolate the variable first in order to compare the variable's value with the regex. I think this change also applies to some of the other comparison expressions in your code.
However, as glenn jackman and user1934428 have commented, this will also match things like foo1bar, which is probably not what you want. To fix this, you can add start/end matchers to your regex. Finally, you may want to match even if the input has leading or trailing spaces. One way to do this is to add some [[:space:]]*'s to match zero or more spaces around your [1-9]:
if [[ $x =~ ^[[:space:]]*[1-9][[:space:]]*$ ]]; then
So, to break down the regex:
^ start of input
[[:space:]]* zero or more whitespaces
[1-9] a single digit, 1-9
[[:space:]]* zero or more whitespaces
$ end of the input
I'm assuming from your question than you only want to match on a single digit, not, for example, 12, or the digit 0. To match those would require a couple more regex tweaks.
and...glob pattern
Just because glen jackman's answer led me down a bash man page adventure 🏄 and I wanted to try them out, this is a glob pattern version (note the == instead of =~):
if [[ $x == *([[:space:]])[1-9]*([[:space:]]) ]]; then
It's basically the same pattern. But notably, glob patterns seem to be implicitly anchored to the start/end of the string being matched (they are tested against the entire string) so they don't need the ^ or $, while regular expressions match against substrings by default, so they do need those additions to avoid foo1bar matching. Anyway, probably more than you cared to know.
Here's an alternate implementation, for your consideration: hit me up with any questions
#!/usr/bin/env bash
get_number() {
local n
while true; do
read -rp "Enter a number between 1 and 9: " n
if [[ $n == [1-9] ]]; then
echo "$n"
return
fi
done
}
case $# in
0) first=$(get_number)
second=$(get_number)
;;
1) first=$1
second=$(get_number)
;;
*) first=$1
second=$2
;;
esac
# or, more compact but harder to grok
[[ -z ${first:=$1} ]] && first=$(get_number)
[[ -z ${second:=$2} ]] && second=$(get_number)
echo "You've chosen $first as your first number and $second as your second number."
This uses:
a function to get a a number from the user, so you don't have so much duplicated code,
a case statement to switch over the $# variable
input validation with the == operator within [[...]] -- this operator is a pattern matching operator, not string equality (unless the right-hand operand is quoted)
Note that [[ $x =~ [1-9] ]] means: "$x contains a character in the range 1 to 9" -- it does not mean that the variable is a single digit. If x=foo1bar, then the regex test passes.

Script Shell : Case issue

I have a script when you select a desktop file, but when I run this case function:
File=$(yad --file);
if [[ "$File" =~ *".desktop" ]]; then
echo "yes"
else
echo "no"
if
and i try this :
File=$(yad --file);
case $File in
*.desktop )
echo "yes"
;;
* )
echo "no"
;;
esac
it's always telling me that I have to try again I don't know what's the problem, can anyone help me?
I am not exactly sure what this script is supposed to do, but try this:
File="$(Yad --file)"
if [[ "$File" =~ .*[.]desktop$ ]]; then
echo "yes"
else
echo "no"
fi
Bash regular expression matching (=~) uses extended regular expressions, not glob expressions. To designate any sequence of zero or more characters, you need to use .*. The . means "any character", and the * means zero or more times. [.] designates a literal period, avoiding the "any character" meaning of . used alone. I also added an end-of-line anchor ($). This forces the pattern to match from the end of the filename, as you probably would want when matching with the extension.
There also is an error in your first line. There has to be no space between the $ sign and parentheses. And to close an if block, you need to use fi.
You can use glob-style matching with bash conditionals, just use an equal sign :
if [[ "$File" = *.desktop ]]; then

How to detect a filename within a case statement - in unix shell?

I coded the below code and if no -l or -L option is passes to the script I need to assume (detect) whether a filename was passed as a param. The below third condition only matches if filename is one lowercase character. How can I make it flexible to match upper and lower case letters or variable length of the string?
while [ $# -gt 0 ]
do
case $1 in
-l) echo ls;;
-L) echo ls -l;;
[a-z]) echo filename;;
*) echo usage
exit 1;;
esac
shift
done
Also, how can I include a condition in that case statement that would react to empty $1?
If for example the script is called without any options or filenames.
You can match an empty string with a '') or "") case.
A file name can contain any character--even weird ones likes symbols, spaces, newlines, and control characters--so trying to figure out if you have a file name by looking for letters and numbers isn't the right way to do it. Instead you can use the [ -e filename ] test to check if a string is a valid file name.
You should, by the way, put "$1" in double quotes so your script will work if the file name does contain spaces.
case "$1" in
'') echo empty;;
-l) echo ls;;
-L) echo ls -l;;
*) if [ -e "$1" ]; then
echo filename
else
echo usage >&2 # echo to stderr
exit 1
fi;;
esac
Use getopts to parse options, then treat remaining non-option arguments however you like (such as by testing if they're a file).
you find the information in the bash man page if you search for "Pattern Matching" without the quotes.this does the trick: [a-zA-Z0-9]*)
you should probably read on about pattern matching, regular expressions and so on.
furthermore you should honour john kugelmans hint about the double quotes.
in the following code snippet you can see how to check if no parameter got passed.
#!/bin/sh
case "$1" in
[a-zA-Z0-9]*)
echo "filename"
;;
"")
echo "no data"
;;
esac
#OP, generally if you are using bash, to match case insensitivity you can use shopt and set nocasematch
$ shopt -s nocasematch
to check null in your case statement
case "$var" in
"") echo "empty value";;
esac
You are better off falling into the default case *) and there you can at least check if the file exists with [ -e "$1" ] ... if it doesn't then echo usage and exit 1.

Resources