How do I deal with empty user input in a Bash script? - string

When the script asks me for input, I get an error if I just press Return without typing in anything. How do I fix this?
Here's the script:
#!/bin/bash
SUM=0
NUM=0
while true
do echo -n "Pruefungspunkte eingeben ('q' zum Beenden): "
read SCORE
if test "$SCORE" == "q"
then echo "Durchschnittspunktzahl: $AVERAGE."
break
else SUM=`expr $SUM + $SCORE`
NUM=`expr $NUM + 1`
AVERAGE=`expr $SUM / $NUM`
fi
done

How about using good bash practices?
#!/bin/bash
sum=0
num=0
while true; do
read -erp "Pruefungspunkte eingeben ('q' zum Beenden): " score
if [[ $score = q ]]; then
echo "Durchschnittspunktzahl: $average."
break
elif [[ $score =~ ^-?[[:digit:]]+$ ]]; then
((sum+=10#$score))
((++num))
((average=sum/num))
else
echo "Bad number"
fi
done
Good practice:
don't use capitalized variable names
use the [[ builtin instead of the test builtin
don't use backticks, use (( to invoke shell arithmetic
to make sure the user inputs a number, check that a number was really entered. The line
elif [[ $score =~ ^-?[[:digit:]]+$ ]]; then
just does that (see regular expressions). Incidentally it completely solves your original problem, since an empty input will not pass through this test
to prevent problems if a user enters 09 instead of 9, force bash to interpret the input in radix 10. That's why I'm using (10#$score) instead of just score.
Use read with the -p (prompt) option, instead of the clumsy combo echo -n / read
This version is much more robust and well-written than yours. Yet, it still has problems:
will break if user needs large numbers
as shell arithmetic is used, only integers can be used. Moreover, the average given by this program is rounded: if you want the average of 1 and 2 you'll have 1.
To fix both problems, you'll probably want to use bc or dc. But that will be the purpose of another question. Or not.

Initialise $SCORE beforehand or handle empty input like you do in q case.

[[ -z "$SCORE" ]] && echo "\$SCORE is zero, e.g. \"\""
This will test if the variable SCORE is empty string.
You should also set AVERAGE=0 at the beginning.

Related

How to implement a spell checker in random number generator in a Bash script

Just finished scripting a number generator game that requires a user to guess a number between 1 and 100, I was curious if there is a way to make the script only accept numbers in between the 1-100 number range and reject any letter characters? I tried using
typeset -i
But i'm not sure if I am fully utilizing it or missing something, here is the rest of the code
#!/bin/bash
num=$((RANDOM%100))
typeset -i attempts=0
until [[ $guess == $num ]]
do
echo -n "Enter your guessing number: "
read -r guess
if (( guess < num ));
then echo "Guess Higher..."
elif (( guess > num ));
then echo "Guess Lower..."
fi
(( attempts++ ))
done
printf "Congradulations! it took $attempts guesses!\n"
accept numbers in between the 1-100 number range and reject any letter characters?
So use some utility to check the input. You could use a regex to check if the input consist only of digits and after that check the range.
echo -n "Enter your guessing number: "
read -r guess
if ! [[ $guess=~ ^[0-9]+$ ]]; then
echo "Your guess is not a number, try again..."
continue;
fi
if (( guess < 1 )); then
echo "Your guess is lower then 1, try again..."
fi
if (( guess > 100 )); then
echo "Your guess is higher then 100, try again..."
continue
fi
# rest of your script
You can use:
typeset -i guess
read -r guess
but that will convert any input into a number without error checking. If you input blabla, guess will just be 0, with no error checking.
I would turn the test around and use a wildcard pattern (globbing) to check, whether the variable contains an illegal character:
if [[ $guess == *[^0-9]* ]]
then
echo Value is not an natural number
elif ((guess == 0 || guess > 100))
echo Value is out of range
else
... do your game here
fi

Count number of specific characters in input string

Im trying to count the number of letters, numbers and special characters in an input string
The user would type the string and then finish with a * to finish the program should then display a count for the number of letters numbers and special characters
So far i have this but i get errors on line 21 which i think is the else statement
The exact error message i get is "./masher3: line 21: 0: command not found"
#!/bin/bash
numcount=0
charcount=0
othercount=0
echo "Input string"
for char in $#
do
if [[ $char == "*" ]]
then
break
elif [[ $char == '0-9' ]]
then
$numcount = $numcount + 1
elif [[ $char == 'A-Z' ]]
then
$charcount = $charcount + 1
else
$othercount = $othercount + 1 <----- Error on this line
fi
done
echo $charcount
This program is written in pure bash (without calling any external programs).
Also look below the code – I added some more information.
#!/bin/bash
# Print the message without going to next line (-n)
echo -n "Your input string: "
# Read text from standard input. ‘-d '*'’ stops
# reading at first ‘*’ character. Remove it to
# terminate on press of key Enter.
# Result is stored in variable $input.
#
# -e enables backspace and other keys.
read -ed '*' input
# Jump to next line
echo
# Fill all counters with zeros
letters=0
digits=0
spaces=0
others=0
# While $input contains some text…
while [[ -n "$input" ]]
do
# Get the first character
char="${input:0:1}"
# Take everything from $input except
# the first character and store it again
# in $input
input="${input:1}"
# Is the character space?
if [[ "$char" == " " ]]
then
# Increase the $spaces variable by one
((spaces++))
# Else: If the $char after removal of all
# letters in (english) alphabet is empty string?
# That will be true when the $char is letter.
elif [[ -z "${char//[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]/}" ]]
then
# Increase $letters
((letters++))
# Else: If the $char …
# Just the same for digits
elif [[ -z "${char//[0123456789]/}" ]]
then
((digits++))
# Else increase the $others variable
else
((others++))
fi
done
# Show values
echo "Letters: $letters"
echo "Digits: $digits"
echo "Spaces: $spaces"
echo "Other characters: $others"
Also open/download the Bash Reference Manual (available as single page, plaintext, PDF). You probably have one copy already installed if you use Linux. Try commands info bash (usually shows hypertext browser if installed) or man bash (single page documentation but usually the same). It is sometimes hard to understand for beginners but you will learn more information about this programming language.
Bash has many builtin commands (such as read, [[, echo, printf etc.) that work like ordinary commands. Their help is in the Reference Manual or can be shown by typing help command_name in your bash shell.
See my other answer for solution.
Your program is quite weird.
Assignment into variable looks like variable=42 (not $variable = 42)
You have to use $((…)) syntax to perform calculations
[[ $char == '0-9' ]] means “When the character is exactly 0-9
$char contains separate arguments of the program, not characters in input.
$othercount = … means “run command with name specified in variable $othercount with arguments = and ….
Assigning into variable in bash
You have to NOT write $ before variable name when you want to assign to it and you have to have NO whitespace before =:
my_variable=42
variable_2=$(($my_variable + 8))
echo $my_variable # Prints “50”

How to sanitize user input in Bash

I am writing a script in bash which will prompt the user for two inputs. These inputs are assigned to the variables 'TO_SCHEMA' and 'FROM_SCHEMA' respectively.
I need a way to verify proper input. My requirements are as follows:
Each variable will have 3 acceptable values. They are the same values for both variables, but both variables must be in this list of three, and they cannot be the same value.
So if the values are 'myco', 'myco_int', and 'teambatch', then both variables must be one of those values, but they can't be the same.
${TO_SCHEMA} = myco && ${FROM_SCHEMA} = myco_int
Pass
${TO_SCHEMA} = myco_int && ${FROM_SCHEMA} = myco_int
Fail
${TO_SCHEMA} = mco && ${FROM_SCHEMA} = myco_int
Fail
${TO_SCHEMA} = myco && ${FROM_SCHEMA} = donkey
Fail
How can I accomplish this?
I began with an if statement full of AND and OR operators, but they got ugly fast. My experience with regex is limited, and my experience with sed and awk is non-existent, but I'm willing to learn and try any of that.
Any help would be appreciated.
EDIT:
I should also mention that this script is just for a somewhat small tedious one off task I have to do a lot at work that I would love to automate. If I'm not the one using it, then someone on my team will be. So this input checking is a want and not a need. It's not the end of the world if the script breaks because of bad input. I would just like it to handle bad input more elegantly.
EDIT AGAIN:
I appreciate everyone's suggestions, but I have to make some clarifications. The values won't actually be schema 1,2 and 3. I'm not allowed to provide proper names for security reasons, but I'm changing them to values more similar to the real ones.
The simplest solution requires bash 4.3. You simply store the valid inputs as the keys of an associative array, then use the -v operator to check if a given input is defined as a key.
declare -A valid
# Values don't matter, as long as the key exists.
# I put spaces in the keys just to show it's possible
valid[schema 1]=
valid[schema 2]=
valid[schema 3]=
if [[ $FROM_SCHEMA != $TO_SCHEMA && -v valid[$FROM_SCHEMA] && -v valid[$TO_SCHEMA] ]]; then
# inputs pass
else
# inputs fail
fi
In earlier 4.x versions, you can check for undefined values in an associative array slightly differently.
declare -A valid
# Now, we need to make sure the values are non-null, but
# otherwise it doesn't matter what they are
valid[schema 1]=1
valid[schema 2]=1
valid[schema 3]=1
if [[ $FROM_SCHEMA != $TO_SCHEMA && -n ${valid[$FROM_SCHEMA]} && -n ${valid[$TO_SCHEMA]} ]]; then
# inputs pass
else
# inputs fail
fi
Prior to bash 4, with no associative arrays, you can fall back to scanning the list of valid inputs stored in a regular array.
valid=("schema 1" "schema 2" "schema 3")
if [[ $TO_SCHEMA == $FROM_SCHEMA ]]; then
# inputs fail
else
ok_count=0
# We've already established that TO and FROM are different,
# so only at most one per candidate can match.
for candidate in "${valid[#]}"; do
if [[ $TO_SCHEMA == $candidate || $FROM_SCHEMA == $candidate ]]; then
ok_count+=1
fi
done
if (( ok_count == 2 )); then
# inputs pass
else
# inputs fail
fi
fi
Note, quick and dirty, lacking in elegance, but works. This is assuming your schema1 answers are accurate. And that I read your question right. you'd be replacing the [your input 1] etc with wherever you are getting the data from, it's a read of user input right? Why not just ask them to select it from a list?
values='1 2 3'
result1=''
result2=''
input1=[your input 1]
input2=[your input 2]
# force to lower case
input1=$(tr '[A-Z]' '[a-z]' <<< "$input1" )
input2=$(tr '[A-Z]' '[a-z]' <<< "$input2" )
for item in $values
do
if [[ -n $( grep -E "^schema$item$" <<< $input1 ) ]];then
values=$(sed "s/$item//" <<< $values )
result1=$input1
fi
done
for item in $values
do
if [[ -n $( grep -E "^schema$item$" <<< $input2 ) ]];then
result2=$input2
fi
done
if [[ -z $result1 ]];then
echo 'Your first entry is not right: ' $input1
elif [[ -z $result2 ]];then
echo 'Your second entry is not right: ' $input2
else
echo 'Valid data'
fi
Note that if you wanted to just test for the literal full string, you'd make it:
values='schema1 schema2 schema3'
then remove the 'schema' from the grep test, which would then just look for the full string thing. And sed would just remove the found item from the list of values.
If you are relying on user typed input, you must force it to lower to avoid spastic user actions or do a case insensitive pattern match with grep, but it's best to force it to lower before testing it.
input1=$(tr '[A-Z]' '[a-z]' <<< "$input1" )

Linux Shell if-statements

I am trying to write a script to add integers together. My first script works to add numbers
for number in $#; do
sum=$(($sum + $number))
done
echo $sum
Now I am trying to write an if statement that checks if $number is an valid integer and if not display an error message and kill the script.
if [ -z $number]; then
sum=$(($sum + $number))
else
echo $number
echo "Sorry, $number is not a number"
exit
fi
I have been trying to figure this out for a few hours and keep getting stuck and help would be nice
The -z operator doesn't check if the string is a number, it checks if the length of the string is zero.
There isn't an operator to check if a string is an integer, however there are several ways to do this in bash. The answers on this previous discussion should be helpful.
the -z test will just test of the variable is empty (zero length) or not. If you want to know that is has an integer you could use a regex match like
if [[ $number =~ ^-?[0-9]+$ ]]; then
sum=$((sum + number))
else
echo "Sorry, $number is not a number"
exit
fi

BASH: how to pass in arguments to an alias: CANNOT USE A FUNCTION - syntax of Bash conditionals

This question differs in that the classic "use a function" answer WILL NOT work. Adding a note to an existing Alias question is equivalent to sending a suggestion e-mail to Yahoo.
I am trying to write macros to get around BASH's horrendous IF syntax. You know, the [, [[, ((...BASH: the PHP of flow control...just add another bracket. I'm still waiting for the "(((((((" form. Not quite sure why BASH didn't repurpose "(", as "(" has no real semantics at the if statement.
The idea is to have named aliases for [, [[ and (( , as each one of these durned test-ish functions has a frustratingly different syntax. I honestly can never remember which is which (how COULD you? It's completely ad hoc!), and good luck trying to google "[[".
I would then use the names as a mnemonic, and the alias to get rid of the completely awful differences in spacing requirements. Examples: "whatdoyoucallthisIf" for "((", "shif" (for shell if), "mysterydoublesquarebacketif" for that awful [[ thing which seems to mostly do the same thing as [, only it doesn't.
Thus, I MUST have something of the form:
alias IFREPLACEMENT="if [ \$# ]; then"
But obviously not \$#, which would just cement in the current argument list to the shell running the alias.
Functions will not work in this case, as the function:
function IFREPLACEMENT {
if [ $# ]; then
}
is illegal.
In CSH, you could say
alias abc blah blah !*
!1, etc. Is there ANYTHING in BASH that is similar (no, !* doesn't work in BASH)?
Or am [ "I just out of luck" ]; ?
As an aside, here are some of the frustrating differences involving test-ish functions in BASH that I am trying to avoid by using well-defined aliases that people would have to use instead of picking the wrong "[[", "[" or "((":
"((" is really creepy...if a variable contains the name of another variable, it's derferenced for as many levels as necessary)
"((" doesn't require a spaces like '[' and '[['
"((" doesn't require "$" for variables to be dereferenced
['s "-gt" is numeric or die. [[ seems to have arbitrary restrictions.
'[' and '[[' use ">" (etc) as LEXICAL comparison operators, but they have frustratingly different rules that make it LOOK like they're doing numeric comparisons when they really aren't.
for a variable: a="" (empty value), [ $a == 123 ] is a syntax error, but (( a == 123 )) isn't.
Sure, functions will work, but not like a macro:
function IFREPLACEMENT {
[[ "$#" ]]
}
IFREPLACEMENT "$x" = "$y" && {
echo "the same
}
FWIW, here's a brutal way to pass arguments to an alias.
$ alias enumerate='bash -c '\''for ((i=0; i<=$#; i++)); do arg=${!i}; echo $i $arg; done'\'
$ enumerate foo bar baz
0 foo
1 bar
2 baz
Clearly, because a new bash shell is spawned, whatever you do won't have any effect on the current shell.
Update: Based on feedback from #konsolebox, the recommendation is now to always use [[...]] for both simplicity and performance (the original answer recommended ((...)) for numerical/Boolean tests).
#Oliver Charlesworth, in a comment on the question, makes the case for not trying to hide the underlying bash syntax - and I agree.
You can simplify things with the following rules:
Always use [[ ... ]] for tests.
Only use [ ... ] if POSIX compatibility is a must. If available, [[ ... ]] is always the better choice (fewer surprises, more features, and almost twice as fast[1]).
Use double-quoted, $-prefixed variable references - for robustness and simplicity (you do pay a slight performance penalty for double-quoting, though1) - e.g., "$var"; see the exceptions re the RHS of == and =~ below.
Whitespace rules:
ALWAYS put a space after the initial delimiter and before the closing delimiter of conditionals (whether [[ / (( or ]] / )))
NEVER put spaces around = in variable assignments.
These rules are more restrictive than they need to be - in the interest of simplification.
Tips and pitfalls:
Note that for numeric comparison with [[ ... ]], you must use -eq, -gt, -ge, -lt, -le, because ==, <, <=, >, >= are for lexical comparison.
[[ 110 -gt 2 ]] && echo YES
If you want to use == with pattern matching (globbing), either specify the entire RHS as an unquoted string, or, at least leave the special globbing characters unquoted.
[[ 'abc' == 'a'* ]] && echo YES
Similarly, performing regex matching with =~ requires that either the entire RHS be unquoted, or at least leave the special regex chars. unquoted - if you use a variable to store the regex - as you may have to in order to avoid bugs with respect to \-prefixed constructs on Linux - reference that variable unquoted.
[[ 'abc' =~ ^'a'.+$ ]] && echo YES
re='^a.+$'; [[ 'abc' =~ $re ]] && echo YES # *unquoted* use of var. $re
An alternative to [[ ... ]], for purely numerical/Boolean tests, is to use arithmetic evaluation, ((...)), whose performance is comparable to [[ (about 15-20% slower1); arithmetic evaluation (see section ARITHMETIC EVALUATION in man bash):
Allows C-style arithmetic (integer) operations such as +, -, *, /, **, %, ...
Supports assignments, including increment and decrement operations (++ / --).
No $ prefix required for variable references.
Caveat: You still need the $ in 2 scenarios:
If you want to specify a number base or perform up-front parameter expansion, such as removing a prefix:
var=010; (( 10#$var > 9 )) && echo YES # mandate number base 10
var=v10; (( ${var#v} > 9 )) && echo YES # strip initial 'v'
If you want to prevent recursive variable expansion.
((...), curiously, expands a variable name without $ recursively, until its value is not the name of an existing variable anymore:
var1=10; var2=var1; (( var2 > 9 )) && echo YES
var2 expands to 10(!)
Has laxer whitespace rules.
Example: v1=0; ((v2 = 1 + ++v1)) && echo YES # -> $v1 == 1, $v2 == 2
Caveat: Since arithmetic evaluation behaves so differently from the rest of bash, you'll have to weigh its added features against having to remember an extra set of rules. You also pay a slight performance penalty1.
You can even cram arithmetic expressions, including assignments, into [[ conditionals that are based on numeric operators, though that may get even more confusing; e.g.:
v1=1 v2=3; [[ v1+=1 -eq --v2 ]] && echo TRUE # -> both $v1 and $v2 == 2
Note: In this context, by 'quoting' I mean single- or double-quoting an entire string, as opposed to \-escaping individual characters in a string not enclosed in either single- or double quotes.
1:
The following code - adapted from code by #konsolebox - was used for performance measurements:
Note:
The results can vary by platform - numbers are based on OS X 10.9.3 and Ubuntu 12.04.
[[ being nearly twice as fast as [ (factor around 1.9) is based on:
using unquoted, $-prefixed variable references in [[ (using double-quoted variable references slows things down somewhat)
(( is slower than [[ with unquoted, $-prefixed variable on both platforms: about 15-20% on OSX, around 30% on Ubuntu. On OSX, using double-quoted, $-prefixed variable references is actually slower, as is not using the $ prefix at all (works with numeric operators). By contrast, on Ubuntu, (( is slower than all ]] variants.
#!/usr/bin/env bash
headers=( 'test' '[' '[[/unquoted' '[[/quoted' '[[/arithmetic' '((' )
iterator=$(seq 100000)
{
time for i in $iterator; do test "$RANDOM" -eq "$RANDOM"; done
time for i in $iterator; do [ "$RANDOM" -eq "$RANDOM" ]; done
time for i in $iterator; do [[ $RANDOM -eq $RANDOM ]]; done
time for i in $iterator; do [[ "$RANDOM" -eq "$RANDOM" ]]; done
time for i in $iterator; do [[ RANDOM -eq RANDOM ]]; done
time for i in $iterator; do (( RANDOM == RANDOM )); done
} 2>&1 | fgrep 'real' | { i=0; while read -r line; do echo "${headers[i++]}: $line"; done; } | sort -bn -k3.3 | awk 'NR==1 { baseTime=substr($3,3) } { time=substr($3,3); printf "%s %s%%\n", $0, (time/baseTime)*100 }' | column -t
Outputs times from fastest to slowest, with slower times also expressed as a percentage of the fastest time.

Resources