Shell Scripting - Numeric Checks and if statement questions - bash

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.

Related

Multiple conditions in while loop failing because of input type in Bash, or 3 digit number condition required

I am trying to control the user input to a script so that a only 3 digit number can pass. I started with two ifs and these work fine.
echo -e "Input sequence number:"
read SEQUENCE
NUMTEST='^[0-9]+$'
if ! [[ $SEQUENCE =~ $NUMTEST ]]; then
echo "ERROR:" "$SEQUENCE" "is not a number! Try again!"
exit 1
fi
SEQLEN=$(printf "%s" "$SEQUENCE" | wc -c)
if (($SEQLEN != 3)); then
echo "ERROR:" "$SEQUENCE" "is not a 3 digit number! Try again!"
exit 1
fi
A nicer solution would be to use a while loop so the user doesn't have to keep re-running the script, but whilst I can get a while loop to work for each individual condition, when I combine them a text input kills the loop as the numerical condition doesn't like it.
echo -e "Input sequence number:"
read SEQUENCE
NUMTEST='^[0-9]+$'
SEQLEN=$(printf "%s" "$SEQUENCE" | wc -c)
while ! [[ $SEQUENCE =~ $NUMTEST ]] && (($SEQLEN != 3)); do
echo "ERROR:" "$SEQUENCE" "is not a 3 digit number! Try again!"
echo -e "Input sequence number:"
read SEQUENCE
SEQLEN=$(printf "%s" "$SEQUENCE" | wc -c)
done
I'm thinking either I need to create a single condition $SEQUENCE == 3 digit number or find a way to suppress the errors such that any error mean the loop continues.
To be explicit my questions are:
Is there a way to write the condition $SEQUENCE == 3 digit number?
Can i suppress the error and still go to the loop, or is this a script terminating error?
Suggestions and improvements much appreciated. This is my first script so I won't be surprised if I'm committing some poor practices so any other recommendations appreciated.
Is there a way to write the condition $SEQUENCE == 3 digit number?
Yes. Change your regex to ^[0-9]{3}.
With a small trick you can also remove the duplicated input processing:
while true; do
read -p 'Input sequence number: ' sequence
[[ "$sequence" =~ ^[0-9]{3}$ ]] && break
echo "ERROR: '$sequence' is not a 3 digit number!"
done
Don't you mean:
while ! [[ $SEQUENCE =~ $NUMTEST ]] || (($SEQLEN != 3)); do
You need to loop again when not a number or not three digit (which is different from your construct, where you loop when not a number and length is not three).
Also, you can create a regexp for amtching three-digit numbers, as stated in #Socowi's answer.

How to match single character on bash script

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

How do I check if a variable contains at least one alphabetic character in Bash?

The version of bash i use is 4.3.11 and I use 'mcedit' as my script writer. I want to check if a variable contains at least one alphabetical character such that 'harry33' and 'a1111' are deemed valid.
I've tried the code below in my script however an error is returned which states that there is an error with '[[:'
SOLVED
#name = "test123"
read -p "Enter you name: " name
until [[ "$name" =~ [A-Za-z] ]]; do
read -p "Please enter a valid name: " name
done
The code you wrote has a couple of issues with spaces (one you already corrected after the [[) and the spaces around an equal should not exist:
name="test123"
if [[ "$name" =~ [A-Za-z] ]]; then
echo "Please enter valid input: "
fi
The line: "Please enter valid input: " will be printed in this case.
As $name contains several values in the range a-z.
Maybe what you want is the opposite, that the line is printed if the variable contains characters outside the range:
name="test"
if [[ "$name" =~ [^A-Za-z] ]]; then
echo "The input contains characters outside the a-z or A-Z range."
fi
But in this case, the characters accepted may include accented (international) characters like é, è, or ë. Which are in-range in several Language Collate sequences.
That also happens with [^[:alpha:]].
Either you embrace full internationalization or limit yourself to old ASCII:
name="test"
LC_COLLATE=C
if [[ "$name" =~ [^A-Za-z] ]]; then
echo "The input contains characters outside the a-z or A-Z range."
fi
If you want to have as valid names with Alpha and digits, there are two options. One which is very strict (old ASCII ranges):
name="harry33"
LC_COLLATE=C
if [[ "$name" =~ ^[0-9A-Za-z]+$ ]]; then
echo "The input only contains digits and alpha."
fi
The other option will also allow é, ß or đ, etc. (which is perfectly fine for an internationalized name), and the range is defined either by the variable LC_COLLATE or LC_ALL as set in the environment.
name="harry33"
if [[ "$name" =~ ^[[:alnum:]]+$ ]]; then
echo "The input only contains digits and alpha."
fi
This option will reject $%&() and similar.
The portable solution free of bashisms such as [[ would be
case $name in
(*[a-zA-Z]*)
echo "Yay! Got an alphabetic character."
;;
(*)
echo "Hmm, no a-z or A-Z found."
;;
esac
First, Bash is picky about spacing, so have a space after your test brackets [[ and ]] Also, if you are looking for user names, I'd think you'd want it to start with a letter, and if it didn't, then echo your prompt.
if [[ ! $var =~ ^[[:alpha:]] ]]; then
echo -n "Please enter valid input: "
read response
fi

substring extraction in bash

iamnewbie: this code is inefficient but it should extract the substring, the problem is with last echo statement,need some insight.
function regex {
#this function gives the regular expression needed
echo -n \'
for (( i = 1 ; i <= $1 ; i++ ))
do
echo -n .
done
echo -n '\('
for (( i = 1 ; i <= $2 ; i++ ))
do
echo -n .
done
echo -n '\)'
echo -n \'
}
# regex function ends
echo "Enter the string:"
read stg
#variable stg holds the string entered
if [ -z "$stg" ] ; then
echo "Null string"
exit
else
echo "Length of the $stg is:"
z=`expr "$stg" : '.*' `
#variable z holds the length of given string
echo $z
fi
echo "Enter the number of trailing characters to be extracted from $stg:"
read n
m=`expr $z - $n `
#variable m holds an integer value which is equal to total length - length of characters to be extracted
x=$(regex $m $n)
echo ` expr "$stg" : "$x" `
#the echo statement(above) is just printing a newline!! But not the result
What I intend to do with this code is, if I enter "racecar" and give "3" , it should display "car" which are the last three characters. Instead of displaying "car" its just printing a newline. Please correct this code rather than giving a better one.
Although you didn't ask for a better solution, it's worth mentioning:
$ n=3
$ stg=racecar
$ echo "${stg: -n}"
car
Note that the space after the : in ${stg: -n} is required. Without the space, the parameter expansion is a default-value expansion rather than a substring expansion. With the space, it's a substring expansion; -n is interpreted as an arithmetic expression (which means that n is interpreted as $n) and since the result is a negative number, it specifies the number of characters from the end to start the substring. See the Bash manual for details.
Your solution is based on evaluating the equivalent of:
expr "$stg" : '......\(...\)'
with an appropriate number of dots. It's important to understand what the above bash syntax actually means. It invokes the command expr, passing it three arguments:
arg 1: the contents of the variable stg
arg 2: :
arg 3: ......\(...\)
Note that there are no quotes visible. That's because the quotes are part of bash syntax, not part of the argument values.
If the value of stg had enough characters, the result of the above expr invocation would be to print out the 7th, 8th and 9th character of the value of stg`. Otherwise, it would print a blank line, and fail.
But that's not what you are doing. You're creating the regular expression:
'......\(...\)'
which has single quotes in it. Since single-quotes are not special characters in a regex, they match themselves; in other words, that pattern will match a string which starts with a single quote, followed by nine arbitrary characters, followed by another single quote. And if the string does match, it will print the three characters prior to the second single-quote.
Of course, since the regular expression you make has a . for every character in the target string, it won't match the target even if the target started and begun with a single-quote, since there would be too many dots in the regex to match that.
If you don't put single quotes into the regex, then your program will work, but I have to say that few times have I seen such an intensely circuitous implementation of the substring function. If you're not trying to win an obfuscated bash competition (a difficult challenge since most production bash code is obfuscated by nature), I'd suggest you use normal bash features instead of trying to do everything with regexen.
One of those is the syntax to determine the length of a string:
$ stg=racecar
$ echo ${#stg}
7
(although, as shown at the beginning, you don't actually even need that.)
What about:
$ n=3
$ string="racecar"
$ [[ "$string" =~ (.{$n})$ ]]
$ echo ${BASH_REMATCH[1]}
car
This looks for the last n characters at the end of the line. In a script:
#!/bin/bash
read -p "Enter a string: " string
read -p "Enter the number of characters you want from the end: " n
[[ "$string" =~ (.{$n})$ ]]
echo "These are the last $n characters: ${BASH_REMATCH[1]}"
You may want to add some more error handling, but this'll do it.
I'm not sure you need loops for this task. I wrote some example to get two parameters from user and cut the word according to it.
#!/bin/bash
read -p "Enter some word? " -e stg
#variable stg holds the string entered
if [ -z "$stg" ] ; then
echo "Null string"
exit 1
fi
read -p "Enter some number to set word length? " -e cutNumber
# check that cutNumber is a number
if ! [ "$cutNumber" -eq "$cutNumber" ]; then
echo "Not a number!"
exit 1
fi
echo "Cut first n characters:"
echo ${stg:$cutNumber}
echo
echo "Show first n characters:"
echo ${stg:0:$cutNumber}
echo "Alternative get last n characters:"
echo -n "$stg" | tail -c $cutNumber
echo
Example:
Enter some word? TheRaceCar
Enter some number to set word length? 7
Cut first n characters:
Car
Show first n characters:
TheRace
Alternative get last n characters:
RaceCar

BASH if comparison to force a valid e-mail address format is entered

I have a useradd bash script which requests the user enter an e-mail address for the user being created. This is so the user receives his username/password in an e-mail when his/her account is created.
Currently this part of the code is very simple:
echo Enter the users e-mail address
read ADDRESS
What i'm finding is that sometimes when the operators run the script they are entereing blank information. How can I put a if statement in place that enforces they enter an e-mail address format.
I tried the following code but it doesn't work. The idea was to at least verify they are using the # symbol.
if [[ $string != "#" ]] ; then
echo You have entered an invalid e-mail address!
exit 1
else
# do something
fi
If you're just looking for something quick and dirty, this bash conditional expression will match something that has at least one char, an '#', at least one char, a dot, and at least one char.
[[ "$email" == ?*#?*.?* ]]
Examples
$ [[ "a#b.c" == ?*#?*.?* ]] && echo Y || echo n
Y
$ [[ "foo#bar" == ?*#?*.?* ]] && echo Y || echo n
n
Actual email validation is gnarly (see here)
!= tests for exact inequality: the string would have to be exactly # with nothing else. Two ways to do the test you want are
case "$string" in
*#*)
;;
*)
echo You have entered an invalid email address! >&2
exit 1
;;
esac
or
if ! expr "$string" : '.*#' >/dev/null; then
echo You have entered an invalid email address! >&2
exit 1
fi
You need to redirect the result from expr because it will print the matched length. Note also that case uses shell globs, whereas expr uses POSIX basic regular expressions (so you can't use +, ?, etc.); and you need to quote the regex passed to expr so the shell doesn't expand it, but for case the whole point is to have the shell expand it.
I generally prefer the case one unless I actually need a regex.
You could e.g. use bash's =~ operator, e.g.:
if [[ $string =~ "#" ]] ; then
# do something
else
echo You have entered an invalid e-mail address!
exit 1
fi
You can use glob-style patterns in if conditionals in bash:
if [[ $string != *"#"* ]] ; then
echo You have entered an invalid e-mail address!
exit 1
else
# do something
fi
I'd go a step further and require at least one character at either side of the #:
if [[ $string != *?"#"?* ]] ; then
echo You have entered an invalid e-mail address!
exit 1
else
: # do something
fi

Resources