Count number of specific characters in input string - linux

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”

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

Decrement variables that contain letters

I have a set of valid characters [0-9a-zA-Z] and a variable that is assigned one of these characters. What I want to do is to be able to decrement that variable to the next in the set.
I can't figure out how to decrement letters , it works for numbers only.
#!/bin/bash
test=b
echo $test # this shows 'b'
let test-=1
echo $test # I want it to be 'a'
The advantage of
test=$(tr 1-9a-zA-Z 0-9a-zA-Y <<<"$test")
is that it correctly (I think) decrements a to 9 and A to z. And if that is not the order you want, it is easy to adjust.
See man tr for details. This is the Gnu version of tr; character ranges are not guaranteed by Posix, but most tr implementations have them. <<< "here strings" are also a common extension, which bash implements.
test=$(printf "\\$(printf '%03o' "$(($(printf '%d' "'$test") - 1 ))")")
you could try this:
#!/bin/bash
test=b
if [[ $test == A || $test == a || $test == 0 ]]
then
echo "character already at lowest value"
else
# convert $test to decimal digit
test_digit=$(printf '%d' "'$test")
decremented=$(( test_digit - 1 ))
# print $decremented as a char
printf "\\$(printf '%03o' "$decremented")\n"
fi
reference:
http://mywiki.wooledge.org/BashFAQ/071
If we set a variable (say a) to the whole string of characters:
$ a=$( IFS=''; set -- {0..9} {a..z} {A..Z}; echo "$*"); echo "$a"
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
We may take advantage of the fact that bash "arithmetic" may use a base up to 62 (in the same order as the letters presented).
$ test="A"; echo "${a:$((62#$test-1)):1}"
z
This works only for "one character" (and not zero 0).
It may be expanded to several characters, but that is not being asked.

Bash, how to check for control character(non-printable character) in a variable?

I have a Bash statement to get user input(a single character) into tmpchar :
read -n 1 -t 1 tmpchar
and I can check for printable character input like this:
if [ "$tmpchar" = "n" ] || [ "$tmpchar" = "N" ]; then
# do something...
fi
Now my question is: If user input just a Return, or ESC, or Ctrl+a, Ctrl+b etc, how do I check for them?
ENV: openSUSE 12.3 , Bash 4.2.42(1)-release
Maybe you're looking for ANSI-C quoting. E.g., Ctrl-a is represented as $'\ca'.
Use the regex match operator =~ inside of [[ ... ]]:
if [[ $tmpchar =~ [[:cntrl:]] ]]; then
# It's a control character
else
# It's not a control character
fi
Note that read -n1 won't do what you expect for a variety of special characters. At a minimum, you should use:
IFS= read -r -n1
Even with that, you'll never see a newline character: if you type a newline, read will set the reply variable to an empty string.
If you want to know if a character isn't a member of the set of printable characters, use a complementary set expression. This seems to work fine with case:
for c in $'\x20' $'\x19'; do
case "$c" in
[[:print:]]) echo printable;;
[^[:print:]]) echo 'not printable';;
*) echo 'more than one character?';;
esac
done
(outputs printable and then non printable)
for c in $'\x20' $'\x19'; do
if [[ $c = [[:print:]] ]]; then
echo printable
fi
if [[ $c = [^[:print:]] ]]; then
echo not printable
fi
done
works as well. If you want to know what characters sets your system supports, look at man 7 regex on linux or man 7 re_format on OS X.
You can filter the input with tr:
read -n 1 -t 1 tmpchar
clean=$(tr -cd '[:print:]' <<< $tmpchar)
if [ -z "$clean"]; then
echo "No printable"
else
echo "$clean"
fi
I find a trick to check for a sole Return input.
if [ "$tmpchar" = "$(echo -e '')" ]; then
echo "You just pressed Return."
fi
In other word, the highly expected way by #ooga,
if [ "$tmpchar" = $'\x0a' ]; then
echo "You just pressed Return." # Oops!
fi
does not work for Return anyhow, hard to explain.

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

Resources