How to sanitize user input in Bash - linux

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" )

Related

How do I check this condition in shell script?

Eg. If I have a command
<package> list --all
Output of the command:
Name ID
abc 1
xyz 2
How can I check if the user input is the same as the name in the list, using a shell script. Something like this:
if ($input== $name in command )
echo "blabla"
name=$1
<package> list --all | egrep -q "^$name[ \t]"
result=$?
The somewhat dubious notation of package is from the question and is a kind of placeholder.
The result will be 0 on success and 1 on failure.
If the name is literally "name" it will match the headline, and if blanks might be in the name, it will be more complicated.
egrep -q "^$name[ \t]"
means 'quiet', don't print the matching case on the screen.
$name holds the parameter, which we assigned in the beginning.
The "^" prevents "bc" to match - it means "beginning of line".
The "[ \t]" captures blank and tab as end of word markers.
To provide an alternate approach (which allows reading and testing more than one value without rerunning your list command or needing to do an O(n) lookup):
#!/usr/bin/env bash
case $BASH_VERSION in
'') echo "This script requires bash 4.x (run with non-bash shell)" >&2; exit 1;;
[0-3].*) echo "This script requires bash 4.x (run with $BASH_VERSION)" >&2; exit 1;;
esac
declare -A seen=( ) # create an empty associative array
{
read -r _ # skip the header
while read -r name value; do # loop over other lines
seen[$name]=$value # ...populating the array from them
done
} < <(your_program list --all) # ...with input for the loop from your program
# after you've done that work, further checks will be very efficient:
while :; do
printf %s "Enter the name you wish to check, or enter to stop: " >&2
read -r name_in # read a name to check from the user
[[ $name_in ]] || break # exit the loop if given an empty value
if [[ ${seen[$name_in]} ]]; then # lookup the name in our associative array
printf 'The name %q exists with value %q\n' "$name_in" "${seen[$name_in]}"
else
printf 'The name %q does not exist\n' "$name_in"
fi
done

bash palindrome grep loop if then else missing '

My Syst admin prof just started teaching us bash and he wanted us to write a bash script using grep to find all 3-45 letter palindromes in the linux dictionary without using reverse. And im getting an error on my if statement saying im missing a '
UPDATED CODE:
front='\([a-z]\)'
front_s='\([a-z]\)'
numcheck=1
back='\1'
middle='[a-z]'
count=3
while [ $count -ne "45" ]; do
if [[ $(($count % 2)) == 0 ]]
then
front=$front$front_s
back=+"\\$numcheck$back"
grep "^$front$back$" /usr/share/dict/words
count=$((count+1))
else
grep "^$front$middle$back$" /usr/share/dict/words
numcheck=$((numcheck+1))
count=$((count+1))
fi
done
You have four obvious problems here:
First about a misplaced and unescaped backslash:
back="\\$numcheck$back" # and not back="$numcheck\$back"
Second is that you only want to increment numcheck if count is odd.
Third: in the line
front=$front$front
you're doubling the number of patterns in front! hey, that yields an exponential growth, hence the explosion Argument list too long. To fix this: add a variable, say, front_step:
front_step='\([a-z]\)'
front=$front_step
and when you increment front:
front=$front$front_step
With these fixed, you should be good!
The fourth flaw is that grep's back-references may only have one digit: from man grep:
Back References and Subexpressions
The back-reference \n, where n is a single digit, matches the substring
previously matched by the nth parenthesized subexpression of the
regular expression.
In your approach, we'll need up to 22 back-references. That's too much for grep. I doubt there are any such long palindromes, though.
Also, you're grepping the file 43 times… that's a bit too much.
Try this:
#!/bin/bash
for w in `grep -E "^[[:alnum:]]{3,45}$" /usr/share/dict/words`; do if [[ "$w" == "`echo $w|sed "s/\(.\)/\1\n/g"|tac|tr -d '\012'`" ]]; then echo "$w == is a palindrome"; fi; done
OR
#!/bin/bash
front='\([a-z]\)'
numcheck=1
back='\1'
middle='[a-z]'
count=3
while [ $count -ne "45" ]; do
if [[ $(($count % 2)) == 0 ]]
then
front=$front$front
back="\\$numcheck$back"
grep "^$front$back$" /usr/share/dict/words
else
grep "^$front$middle$back$" /usr/share/dict/words
## Thanks to gniourf for catching this.
numcheck=$((numcheck+1))
fi
count=$((count+1))
## Uncomment the following if you want to see one by one and run script using bash -x filename.sh
#echo Press any key to continue: ; read toratora;
done

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

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.

Bash reading txt file and storing in array

I'm writing my first Bash script, I have some experience with C and C# so I think the logic of the program is correct, it's just the syntax is so complicated because apparently there are many different ways to write the same thing!
Here is the script, it simply checks if the argument (string) is contained in a certain file. If so it stores each line of the file in an array and writes an item of the array in a file. I'm sure there must be easier ways to achieve that but I want to do some practice with bash loops
#!/bin/bash
NOME=$1
c=0
#IF NAME IS FOUND IN THE PHONEBOOK THEN STORE EACH LINE OF THE FILE INTO ARRAY
#ONCE THE ARRAY IS DONE GET THE INDEX OF MATCHING NAME AND RETURN ARRAY[INDEX+1]
if grep "$NOME" /root/phonebook.txt ; then
echo "CREATING ARRAY"
while read line
do
myArray[$c]=$line # store line
c=$(expr $c + 1) # increase counter by 1
done < /root/phonebook.txt
else
echo "Name not found"
fi
c=0
for i in myArray;
do
if myArray[$i]="$NOME" ; then
echo ${myArray[i+1]} >> /root/numbertocall.txt
fi
done
This code returns the only the second item of myArray (myArray[2]) or the second line of the file, why?
The first part (where you build the array) looks ok, but the second part has a couple of serious errors:
for i in myArray; -- this executes the loop once, with $i set to "myArray". In this case, you want $i to iterate over the indexes of myArray, so you need to use
for i in "${!myArray[#]}"
or
for ((i=0; i<${#a[#]}; i++))
(although I generally prefer the first, since it'll work with noncontiguous and associative arrays).
Also, you don't need the ; unless do is on the same line (in shell, ; is mostly equivalent to a line break so having a semicolon at the end of a line is redundant).
if myArray[$i]="$NOME" ; then -- the if statement takes a command, and will therefore treat myArray[$i]="$NOME" as an assignment command, which is not at all what you wanted. In order to compare strings, you could use the test command or its synonym [
if [ "${myArray[i]}" = "$NOME" ]; then
or a bash conditional expression
if [[ "${myArray[i]}" = "$NOME" ]]; then
The two are very similar, but the conditional expression has much cleaner syntax (e.g. in a test command, > redirects output, while \> is a string comparison; in [[ ]] a plain > is a comparison).
In either case, you need to use an appropriate $ expression for myArray, or it'll be interpreted as a literal. On the other hand, you don't need a $ before the i in "${myArray[i]}" because it's in a numeric expression context and therefore will be expanded automatically.
Finally, note that the spaces between elements are absolutely required -- in shell, spaces are very important delimiters, not just there for readability like they usually are in c.
1.-This is what you wrote with small adjustments
#!/bin/bash
NOME=$1
#IF NAME IS FOUND IN THE PHONE-BOOK **THEN** READ THE PHONE BOOK LINES INTO AN ARRAY VARIABLE
#ONCE THE ARRAY IS COMPLETED, GET THE INDEX OF MATCHING LINE AND RETURN ARRAY[INDEX+1]
c=0
if grep "$NOME" /root/phonebook.txt ; then
echo "CREATING ARRAY...."
IFS= while read -r line #IFS= in case you want to preserve leading and trailing spaces
do
myArray[c]=$line # put line in the array
c=$((c+1)) # increase counter by 1
done < /root/phonebook.txt
for i in ${!myArray[#]}; do
if myArray[i]="$NOME" ; then
echo ${myArray[i+1]} >> /root/numbertocall.txt
fi
done
else
echo "Name not found"
fi
2.-But you can also read the array and stop looping like this:
#!/bin/bash
NOME=$1
c=0
if grep "$NOME" /root/phonebook.txt ; then
echo "CREATING ARRAY...."
readarray myArray < /root/phonebook.txt
for i in ${!myArray[#]}; do
if myArray[i]="$NOME" ; then
echo ${myArray[i+1]} >> /root/numbertocall.txt
break # stop looping
fi
done
else
echo "Name not found"
fi
exit 0
3.- The following improves things. Supposing a)$NAME matches the whole line that contains it and b)there's always one line after a $NOME found, this will work; if not (if $NOME can be the last line in the phone-book), then you need to do small adjustments.
!/bin/bash
PHONEBOOK="/root/phonebook.txt"
NUMBERTOCALL="/root/numbertocall.txt"
NOME="$1"
myline=""
myline=$(grep -A1 "$NOME" "$PHONEBOOK" | sed '1d')
if [ -z "$myline" ]; then
echo "Name not found :-("
else
echo -n "$NOME FOUND.... "
echo "$myline" >> "$NUMBERTOCALL"
echo " .... AND SAVED! :-)"
fi
exit 0

Bash Script is returning true for both but opposite string tests

Before I ran the script I have entered
# export CPIC_MAX_CONV=500
The following is the test1.script file
#!/bin/bash
function cpic () {
var="`export | grep -i "CPIC_MAX_CONV" | awk '/CPIC_MAX_CONV/ { print $NF } '`"
[[ $var=="" ]] && (echo "Empty String <<")
[[ $var!="" ]] && (echo "$CPIC_MAX_CONV")
echo "$var" ;
}
cpic
The output is:
# test1.script ---- Me running the file
Empty String <<
500
CPIC_MAX_CONV="500"
No matter what I use "" or '' or [ or [[ the result is the same. The CPIC_MAX_CONV variable is found by the above script.
I am running this on Linux/CentOS 6.3.
The idea is simple: To find if CPIC_MAX_CONV is defined in the environment and return the value of it. If an empty space is there then of course the variable is not present in the system.
Why do you always get true? Let's play a little bit in your terminal first:
$ [[ hello ]] && echo "True"
What do you think the output is? (try it!) And with the following?
$ [[ "" ]] && echo "True"
(try it!).
All right, so it seems that a non-empty string is equivalent to the true expression, and an empty string (or an unset variable) is equivalent to the false expression.
What you did is the following:
[[ $var=="" ]]
and
[[ $var!="" ]]
so you gave a non-empty string, which is true!
In order to perform the test, you actually need spaces between the tokens:
[[ $var == "" ]]
instead. Now, your test would be better written as:
if [[ -z "$var" ]]; then
echo "Empty String <<"
else
echo "$CPIC_MAX_CONV"
fi
(without the sub-shells, and with just one test).
There's more to say about your scripting style. With no offence, I would say it's really bad:
Don't use backticks! Use the $(...) construct instead. Hence:
var="$(export | grep -i "CPIC_MAX_CONV" | awk '/CPIC_MAX_CONV/ { print $NF } ')"
Don't use function blah to define a function. Your function should have been defined as:
cpic () {
local var="$(export | grep -i "CPIC_MAX_CONV" | awk '/CPIC_MAX_CONV/ { print $NF } ')"
if [[ -z "$var" ]]; then
echo "Empty String <<"
else
echo "$CPIC_MAX_CONV"
fi
}
Oh, I used the local keyword, because I guess you're not going to use the variable var outside of the function cpic.
Now, what's the purpose of the function cpic and in particular of the stuff where you're defining the variable var? It would be hard to describe (as there are so many cases you haven't thought of). (Btw, your grep seems really useless here). Here are a few cases you overlooked:
An exported variable is named somethingfunnyCPIC_MAX_CONVsomethingevenfunnier
An exported variable contains the string CPIC_MAX_CONV somewhere, e.g.,
export a_cool_variable="I want to screw up Randhawa's script and just for that, let's write CPIC_MAX_CONV somewhere here"
Ok, I don't want to describe what your line is doing exactly, but I kind of guess that your purpose is to know whether the variable CPIC_MAX_CONV is set and marked for export, right? In that case, you'd be better with just this:
cpic () {
if declare -x | grep -q '^declare -x CPIC_MAX_CONV='; then
echo "Empty String <<"
else
echo "$CPIC_MAX_CONV"
fi
}
It will be more efficient, and much more robust.
Oh, I'm now just reading the end of your post. If you want to just tell if variable CPIC_MAX_CONV is set (to some non-empty value — it seems you don't care if it's marked for export or not, correct me if I'm wrong), it's even simpler (and it will be much much more efficient):
cpic () {
if [[ "$CPIC_MAX_CONV" ]]; then
echo "Empty String <<"
else
echo "$CPIC_MAX_CONV"
fi
}
will do as well!
Do you really care whether CPIC_MAX_CONV is an environment variable versus just 'it is a variable that might be an environment variable'? Most likely, you won't, not least because if it is a variable but not an environment variable, any script you run won't see the value (but if you insist on using aliases and functions, then it might matter, but still probably won't).
It appears, then, that you are trying to test whether CPIC_MAX_CONV is set to a non-empty value. There are multiple easy ways to do that — and then there's the way you've tried.
: ${CPIC_MAX_CONV:=500}
This ensures that CPIC_MAX_CONV is set to a non-empty value; it uses 500 if there previously wasn't a value set. The : (colon) command evaluates its arguments and reports success. You can arrange to export the variable after it is created if you want to with export CPIC_MAX_CONV.
If you must have the variable set (there is no suitable default), then you use:
: ${CPIC_MAX_CONV:?}
or
: ${CPIC_MAX_CONV:?'The CPIC_MAX_CONV variable is not set but must be set'}
The difference is that you can use the default message ('CPIC_MAX_CONV: parameter null or not set') or specify your own.
If you're only going to use the value once, you can do an 'on the fly' substitution in a command with:
cpic_command -c ${CPIC_MAX_CONV:-500} ...
This does not create the variable if it does not exist, unlike the := notation which does.
In all these notations, I've been using a colon as part of the operation. That enforces 'null or not set'; you can omit the colon, but that allows an empty string as a valid value, which is probably not what you want. Note that a string consisting of just a blank is 'not empty'; if you need to validate that you've got a non-empty string, you have to work a little harder.
I'm not dissecting your misuse of the [[ command; gniourf_gniourf has provided an excellent deconstruction of that, but overlooked the simpler notations available to do what seems to be the job.
Try this:
#!/bin/bash
function cpic () {
var="`export | grep -i "CPIC_MAX_CONV"`"
[ "$var" = "" ] && (echo "Empty String <<")
[ "$var" != "" ] && echo "$CPIC_MAX_CONV"
}
cpic
You need spaces in your conditions.
#!/bin/bash
function cpic () {
var="`export | grep -i "CPIC_MAX_CONV" | awk '/CPIC_MAX_CONV/ { print $NF } '`"
[[ $var == "" ]] && (echo "Empty String <<")
[[ $var != "" ]] && (echo "$CPIC_MAX_CONV")
echo "$var" ;
}
cpic

Resources