Trying to escape the backslash in this bash script - bash

The following bash script takes any input of numbers between 0-100 and prints out an average of the numbers to the screen. The script also has input validation that won't allow anything but a number between 0-100 and a q or Q. Once you enter a q or Q it computes the results and outputs to the screen. Input validation also checks to make sure there are no null values entered, special characters entered and that there are no number/letter combinations, special character/number combinations etc. entered.
The only problem I have is with the backslash character. The backslash is escaped in this script and when I run the script and enter a backslash it pauses and requires you to press return for the script to continue. Seems like the script still works but I'm curious about why it pauses. Most of the recommendations I've seen on this on this site have been to escape the backslash with more backslashes but that doesn't work.
#! /bin/bash
AVERAGE="0"
SUM="0"
NUM="0"
clear
while true; do
echo -n "Enter your score [0-100%] ('q' for quit): "; read SCORE;
if [[ "$SCORE" == *[a-pA-pr-zR-Z]* ]] ||
[[ "$SCORE" == *['!'\\##\$%^\&*()_+~\`\-=\[\]\{\}\|:\;\'\"\<\>,.?/\\]* ]] ||
[[ -z "$SCORE" ]] ||
(( "$SCORE" < "0" )) || (( "$SCORE" > "100" ))
then
echo "Be serious. Come on, try again: "
elif [[ "$SCORE" == [qQ] ]]; then
echo "Average rating: $AVERAGE%."
break
else
SUM=$[$SUM + $SCORE]
NUM=$[$NUM + 1]
AVERAGE=$[$SUM / $NUM]
fi
done
echo "Exiting."

Use read -r to disable backslash escaping, which is enabled by default.
Options:
-r do not allow backslashes to escape any characters

Related

Why is empty string changed into -n expression in bash

Taken this snippet:
$ [[ ""=="foo" ]] && echo yes || echo no
+ [[ -n ==foo ]]
+ echo yes
yes
How does [[ ""=="foo" ]] turn into [[ -n ==foo ]] ?
The RC was of course missing spaces around == - after adding them, it works as expected:
$ [[ "" == "foo" ]] && echo yes || echo no
+ [[ '' == \f\o\o ]]
+ echo no
no
But still i cannot understand why it behaved like this?
It's not changing the empty string into -n.
The string ""=="foo" is equivalent to the string ==foo. The trace output always shows strings in their simplest format, without unnecessary quotes.
A conditional expression that just contains a single string with no operators is true if the string is not empty. That's what the -n operator tests, so the -x expansion shows it that way.
Any operand that isn't preceded or followed by an operator is treated to have an equal operation as -n <operand>. Operators also need to be isolated with spaces to be distinguished. For a list of operators run help test. Also run help [[ to see how the keyword is different from the [ and test builtins.

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.

Loop's regex conditional doesn't work using extended test

I have a loop that evaluates based on a regex conditional:
until read -p "Enter oprator: " operator
[[ $operator =~ ^[+-*\/]$ ]] #doesn't work
do...
The loop will run until the user enters an arithmetic operator (+, -, * or /). When I enter any of those four, the loop still runs.
I've tried variations of this (i.e. place regex in variable, using quotes, grep) but nothing seems to work.
^[+-*\/]$ ]]$
Here problem is placement of an unescaped - in the middle of the bracket expression which acts as a range between + and *.
You may use this regex (no need to escape / in BASH regex):
[[ $operator =~ ^[-+*/]$ ]]
Or even better without regex use glob match:
[[ $operator == [-+*/] ]]
When including the dash or minus sign - in a character class of a Regex, it must be first or last position, or it will be handled like a range marker. Also the slash / does not need escaping with a backslash:
#!/usr/bin/env bash
until
read -r -n1 -p "Enter oprator: " operator
printf \\n
[[ "$operator" =~ [+*/-] ]] #doesn't work
do
printf 'Symbol %q is not an operator!\n' "$operator" >&2
done
POSIX shell grammar implementation:
#!/usr/bin/env sh
until
printf 'Enter oprator: '
read -r operator
printf \\n
[ -n "$operator" ] && [ -z "${operator%%[-+*/]}" ]
do
printf 'Symbol %s is not an operator!\n' "$operator" >&2
done

bash verbose string comparison slashes

I am comparing two strings in a bash script as follows:
x="hello"
y="hello"
if [[ "$x" != "$y" ]]; then
echo "different"
else
echo "same"
fi
This comparison works. When I execute the script with -x, the comparison still works, but it shows the output
+ x=hello
+ y=hello
+ [[ -n hello ]]
+ [[ hello != \h\e\l\l\o ]]
+ echo same
I'm curious why the right side of the string shows as\h\e\l\l\o and not hello
The simple explanation is for the same reason that the left-hand side doesn't have quotes around it.
-x is showing you an equivalent but not exact representation of what it ran. The right-hand side of = and != in [[ ... ]] is matched as a pattern.
From the manual:
When the == and != operators are used, the string to the right of the operator is considered a pattern and matched according to the rules described below under Pattern Matching. .... Any part of the pattern may be quoted to force it to be matched as a string.
The -x output, for some reason, chooses to use escaping instead quoting to disable pattern matching there.
When using =, ==, and != in [[, the right-side string can contain globs (*, ?, etc.).
The backslashes in your example aren't necessary, though they don't hurt. They are needed if the right-side string contains a possible wildcard character. For example:
$ set -x
$ [[ hi == 'hi*' ]]; echo $?
+ [[ hi == \h\i\* ]]
+ echo 1
1
$ [[ hi == hi* ]]; echo $?
+ [[ hi == hi* ]]
+ echo 0
0

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