# How to write a shell script to assign letter grades to numeric ranges?

I want to make a script that would prompt you for a number between 0-100 and then give you the grade based on the number.

I'd like it in bash.

``````PS3='Please enter your choice: '
(Something here)

do
case \$
"0-59")
echo "F"
;;
"60-69")
echo "D"
;;
"70-79")
echo "C"
;;
"Quit")
break
;;
*) echo invalid option;;
esac
done
``````

# Brevity vs. Readability: A Middle Ground

As you've seen, this problem admits to solutions that are moderately long and somewhat repetitive but highly readable (terdon's and A.B.'s bash answers), as well as those that are very short but non-intuitive and much less self-documenting (Tim's python and bash answers and glenn jackman's perl answer). All these approaches are valuable.

You can also solve this problem with code in the middle of the continuum between compactness and readability. This approach is almost as readable as the longer solutions, with a length closer to the small, esoteric solutions.

``````#!/usr/bin/env bash

case \$REPLY in [qQ]) exit;; esac

declare -A cutoffs
cutoffs[F]=59 cutoffs[D]=69 cutoffs[C]=79 cutoffs[B]=89 cutoffs[A]=100

for letter in F D C B A; do
((REPLY <= cutoffs[\$letter])) && { echo \$letter; exit; }
done
``````

In this bash solution, I've included some blank lines to enhance readability, but you could remove them if you wanted it even shorter.

Blank lines included, this is actually only slightly shorter than a compactified, still pretty readable variant of A.B.'s bash solution. Its main advantages over that method are:

• It's more intuitive.
• It automatically accepts input with leading and trailing spaces (see below for an explanation of how `((` `))` works).

All three of these advantages arise because this method uses the user's input as numeric data rather than by manually examining its constituent digits.

### How it Works

1. Read input from the user. Let them use the arrow keys to move around in the text they've entered (`-e`) and don't interpret `\` as an escape character (`-r`).
This script is not a feature-rich solution--see below for a refinement--but those useful features only make it two characters longer. I recommend always using `-r` with `read`, unless you know you need to let the user supply `\` escapes.
2. If the user wrote `q` or `Q`, quit.
3. Create an associative array (`declare -A`). Populate it with the highest numeric grade associated with each letter grade.
4. Loop through the letter grades from lowest to highest, checking if the user-provided number is low enough to fall into each letter grade's numeric range.
With `((` `))` arithmetic evaluation, variable names don't need to be expanded with `\$`. (In most other situations, if you want to use a variable's value in place of its name, you must do this.)
5. If it falls in the range, print the grade and exit.
For brevity, I use the short-circuit and operator (`&&`) rather than an `if`-`then`.
6. If the loop finishes and no range has been matched, assume the number entered is too high (over 100) and tell the user it was out of range.

### How this Behaves, with Weird Input

Like the other short solutions posted, that script doesn't check the input before assuming it's a number. Arithmetic evaluation (`((` `))`) automatically strips leading and trailing whitespace, so that's no problem, but:

• Input that doesn't look like a number at all is interpreted as 0.
• With input that looks like a number (i.e., if it starts with a digit) but contains invalid characters, the script emits errors.
• Multi-digit input starting with `0` is interpreted as being in octal. For example, the script will tell you 77 is a C, while 077 is a D. Although some users may want this, most probably don't and it can cause confusion.
• On the plus side, when given an arithmetic expression, this script automatically simplifies it and determines the associated letter grade. For example, it will tell you 320/4 is a B.

# An Expanded, Fully Featured Version

For those reasons, you may want to use something like this expanded script, which checks to ensure the input is good, and includes some other enhancements.

``````#!/usr/bin/env bash
shopt -s extglob

declare -A cutoffs
cutoffs[F]=59 cutoffs[D]=69 cutoffs[C]=79 cutoffs[B]=89 cutoffs[A]=100

case \$REPLY in  # allow leading/trailing spaces, but not octal (e.g. "03")
*( )@([1-9]*([0-9])|+(0))*( )) ;;
*( )[qQ]?([uU][iI][tT])*( )) exit;;
*) echo "I don't understand that number."; continue;;
esac

for letter in F D C B A; do
((REPLY <= cutoffs[\$letter])) && { echo \$letter; continue 2; }
done
done
``````

This is still a pretty compact solution.

### What features does this add?

The key points of this expanded script are:

• Input validation. terdon's script checks input with `if [[ ! \$response =~ ^[0-9]*\$ ]] ...`, so I show another way, which sacrifices some brevity but is more robust, allowing the user to enter leading and trailing spaces and refusing to allow an expression that might or might not be intended as octal (unless it's zero).
• I've used `case` with extended globbing instead of `[[` with the `=~` regular expression matching operator (as in terdon's answer). I did that to show that (and how) it can also be done that way. Globs and regexps are two ways of specifying patterns that match text, and either method is fine for this application.
• Like A.B.'s bash script, I've enclosed the whole thing in an outer loop (except the initial creation of the `cutoffs` array). It requests numbers and gives corresponding letter grades as long as terminal input is available and the user hasn't told it to quit. Judging by the `do`...`done` around the code in your question, it looks like you want that.
• To make quitting easy, I accept any case-insensitive variant of `q` or `quit`.

This script uses a few constructs that may be unfamiliar to novices; they're detailed below.

### Explanation: Use of `continue`

When I want to skip over the rest of the body of the outer `while` loop, I use the `continue` command. This brings it back up to the top of the loop, to read more input and run another iteration.

The first time I do this, the only loop I'm in is the outer `while` loop, so I can call `continue` with no argument. (I'm in a `case` construct, but that doesn't affect the operation of `break` or `continue`.)

``````        *) echo "I don't understand that number."; continue;;
``````

The second time, however, I'm in an inner `for` loop which is itself nested inside the outer `while` loop. If I used `continue` with no argument, this would be equivalent to `continue 1` and would continue the inner `for` loop instead of the outer `while` loop.

``````        ((REPLY <= cutoffs[\$letter])) && { echo \$letter; continue 2; }
``````

So in that case, I use `continue 2` to make bash find and continue the second loop out instead.

### Explanation: `case` Labels with Globs

I don't use `case` to figure out which letter grade bin a number falls into (as in A.B.'s bash answer). But I do use `case` to decide if the user's input should be considered:

• a valid number, `*( )@([1-9]*([0-9])|+(0))*( )`
• the quit command, `*( )[qQ]?([uU][iI][tT])*( )`
• anything else (and thus invalid input), `*`

These are shell globs.

• Each is followed by a `)` that is not matched by any opening `(`, which is `case`'s syntax for separating a pattern from the commands that run when it is matched.
• `;;` is `case`'s syntax for indicating the end of commands to run for a paticular case match (and that no subsequent cases should be tested after running them).

Ordinary shell globbing provides `*` to match zero or more characters, `?` to match exactly one character, and character classes/ranges in `[` `]` brackets. But I'm using extended globbing, which goes beyond that. Extended globbing is enabled by default when using `bash` interactively, but is disabled by default when running a script. The `shopt -s extglob` command at the top of the script turns it on.

### Explanation: Extended Globbing

`*( )@([1-9]*([0-9])|+(0))*( )`, which checks for numeric input, matches a sequence of:

• Zero or more spaces (`*( )`). The `*(` `)` construct matches zero or more of the pattern in the parentheses, which here is just a space.
There are actually two kinds of horizontal whitespace, spaces and tabs, and often it's desirable to match tabs also. But I'm not worrying about that here, because this script is written for manual, interactive input, and the `-e` flag to `read` enables GNU readline. This is so the user can move back and forth in their text with the left and right arrow keys, but it has the side effect of generally preventing tabs from being entered literally.
• One occurrence (`@(` `)`) of either (`|`):
• A nonzero digit (`[1-9]`) followed by zero or more (`*(` `)`) of any digit (`[0-9]`).
• One or more (`+(` `)`) of `0`.
• Zero or more spaces (`*( )`), again.

`*( )[qQ]?([uU][iI][tT])*( )`, which checks for the quit command, matches a sequence of:

• Zero or more spaces (`*( )`).
• `q` or `Q` (`[qQ]`).
• Optionally--i.e., zero or one occurrences (`?(` `)`)--of:
• `u` or `U` (`[uU]`) followed by `i` or `I` (`[iI]`) followed by `t` or `T` (`[tT]`).
• Zero or more spaces (`*( )`), again.

# Variant: Validating Input with an Extended Regular Expression

If you prefer to test the user's input against a regular expression rather than a shell glob, you might prefer to use this version, which works the same but uses `[[` and `=~` (like in terdon's answer) instead of `case` and extended globbing.

``````#!/usr/bin/env bash
shopt -s nocasematch

declare -A cutoffs
cutoffs[F]=59 cutoffs[D]=69 cutoffs[C]=79 cutoffs[B]=89 cutoffs[A]=100

# allow leading/trailing spaces, but not octal (e.g., "03")
if [[ ! \$REPLY =~ ^\ *([1-9][0-9]*|0+)\ *\$ ]]; then
[[ \$REPLY =~ ^\ *q(uit)?\ *\$ ]] && exit
echo "I don't understand that number."; continue
fi

for letter in F D C B A; do
((REPLY <= cutoffs[\$letter])) && { echo \$letter; continue 2; }
done
done
``````

Possible advantages of this approach are that:

• In this particular case, the syntax is a little simpler, at least in the second pattern, where I check for the quit command. This is because I was able to set the `nocasematch` shell option, and then all case variants of `q` and `quit` were covered automatically.

That's what the `shopt -s nocasematch` command does. The `shopt -s extglob` command is omitted as globbing isn't used in this version.

• Regular expression skills are more common than proficiency in bash's extglobs.

### Explanation: Regular Expressions

As for the patterns specified to the right of the `=~` operator, here's how those regular expressions work.

`^\ *([1-9][0-9]*|0+)\ *\$`, which checks for numeric input, matches a sequence of:

• The beginning--i.e., left edge--of the line (`^`).
• Zero or more (`*`, applied postfix) spaces. A space does not ordinarily need to be `\`-escaped in a regular expression, but this is needed with `[[` to prevent a syntax error.
• A substring (`(` `)`) that is one or the other (`|`) of:
• `[1-9][0-9]*`: a nonzero digit (`[1-9]`) followed by zero or more (`*`, applied postfix) of any digit (`[0-9]`).
• `0+`: one or more (`+`, applied postfix) of `0`.
• Zero or more spaces (`\ *`), as before.
• The end--i.e., right edge--of the line (`\$`).

Unlike `case` labels, which match against the whole expression being tested, `=~` returns true if any part of its left-hand expression matches the pattern given as its right-hand expression. This is why the `^` and `\$` anchors, specifying the beginning and end of the line, are needed here, and don't correspond syntactically to anything appearing in the method with `case` and extglobs.

The parentheses are needed to make `^` and `\$` bind to the disjunction of `[1-9][0-9]*` and `0+`. Otherwise it would be the disjunction of `^[1-9][0-9]*` and `0+\$`, and match any input starting with a nonzero digit or ending with a `0` (or both, which might still include non-digits in between).

`^\ *q(uit)?\ *\$`, which checks for the quit command, matches a sequence of:

• The beginning of the line (`^`).
• Zero or more spaces (`\ *`, see above explanation).
• The letter `q`. Or `Q`, since `shopt nocasematch` is enabled.
• Optionally--i.e., zero or one occurrences (postfix `?`)--of the substring (`(` `)`):
• `u`, followed by `i`, followed by `t`. Or, since `shopt nocasematch` is enabled `u` may be `U`; independently, `i` may be `I`; and independently, `t` may be `T`. (That is, the possibilities are not limited to `uit` and `UIT`.)
• Zero or more spaces again (`\ *`).
• The end of the line (`\$`).
• tell me the truth..how much time it has taken? ;) – heemayl May 9 '15 at 18:37
• @heemayl I'm not totally sure, since I wrote it in many little parts throughout the day (followed by a full read-through and edits, just before posting). I'm pretty sure it has ended up taking longer than I would've thought when I started, though, if I'd been thinking about how long it was going to take. :) – Eliah Kagan May 9 '15 at 18:43
• write more and more, I need a book of your answers. – Grijesh Chauhan May 10 '15 at 12:44
• TL;DR but made me laugh anyway! – Fabby Aug 25 '15 at 18:58
• everything from title to explanation is good. I understood it in the first go but i read it again just because i wanted to – Sumeet Deshmukh Apr 4 '17 at 5:23

You already have the basic idea. If you want to code this in `bash` (which is a reasonable choice since it is the default shell on Ubuntu and most other Linuxes), you can't use `case` because it doesn't understand ranges. Instead, you could use `if`/`else`:

``````#!/usr/bin/env bash

## If the response given did not consist entirely of digits
if [[ ! \$response =~ ^[0-9]*\$ ]]
then
## If it was Quit or quit, exit
[[ \$response =~ [Qq]uit ]] && exit
## If it wasn't quit or Quit but wasn't a number either,
## print an error message and quit.
echo "Please enter a number between 0 and 100 or \"quit\" to exit" && exit
fi
## Process the other choices
if [ \$response -le 59 ]
then
echo "F"
elif [ \$response -le 69 ]
then
echo "D"
elif  [ \$response -le 79 ]
then
echo "C"
elif  [ \$response -le 89 ]
then
echo "B"
elif [ \$response -le 100 ]
then
echo "A"
elif [ \$response -gt 100 ]
then
echo "Please enter a number between 0 and 100"
exit
fi
``````
• A bunch of those `-ge` tests can be eliminated, presumably, since you're using `elif`. And no love for `(( \$response < X ))`? – muru May 9 '15 at 12:11
• @muru true, thanks. I was stuck thinking in number ranges but there was no reason to. As for `(( \$response < X ))`, sure, but I find this clearer and the OP is obviously new to bash scripting. – terdon May 9 '15 at 12:17
``````#!/bin/bash

while true
do

case "\$choice"
in
[0-9]|[1-5][0-9])
echo "F"
;;
6[0-9])
echo "D"
;;
7[0-9])
echo "C"
;;
8[0-9])
echo "B"
;;
9[0-9]|100)
echo "A"
;;
[Qq])
exit 0
;;
*) echo "Only numbers between 0..100, q for quit"
;;
esac
done
``````

and a more compact version (Thx @EliahKagan):

``````#!/usr/bin/env bash

[0-9]|[1-5][0-9])   echo F ;;
6[0-9])             echo D ;;
7[0-9])             echo C ;;
8[0-9])             echo B ;;
9[0-9]|100)         echo A ;;

[Qq])               exit ;;
*)                  echo 'Only numbers between 0..100, q for quit' ;;
esac
done
``````
• Those are character ranges, surely? i.e. `[0-59]` means any character from 0,1,2,3,4,5 or 9 and so on. I don't see how that can work for numeric values. – steeldriver May 9 '15 at 12:02
• You don't have to be FGITW all the time. Take you time, write good answers. Look at how terdon or Eliah Kagan work. – muru May 9 '15 at 12:13
• @A.B. I've noticed that this solution can be shortened, mostly through stylistic changes, while still remaining quite readable. Brevity is rarely the most important consideration so I'm don't think you should change what you have in this way. But since the more compact form doesn't take up very much space, you might consider showing it too, in case some readers want a shorter script that works the same way. (If you like, please feel free to use my shortened version, or any variation on it.) – Eliah Kagan May 9 '15 at 18:09

All Ubuntu installs have Python, so here is a python script one liner. If you need it to be in bash, I've also written the equivalent as a shell script.

``````print (chr(75-max(5,int('0'+raw_input('Enter the number: ')[:-1]))))
``````

To run, save it in a file (e.g. `grade.py`) and then run it in terminal with this:

``````python grade.py
``````

This is what you will see:

``````Enter the number: 65
E
``````

How does this work?

1. Take input - `65`.
2. Add a 0 to the beginning - `065`.
3. Remove the last char - `06`.
4. 75 subtract that number - `70`.
5. Convert to a letter (A is 65, B is 66) - `E`.
6. Print it out - `E`.

My pronouns are He / Him

• I like your idea. +1 – A.B. May 9 '15 at 13:29
• Don't use `input()`, it will call `eval()`, use `raw_input()` instead..also your grading is not right as `90+` will print grade `B`..use `chr(74 - max(4, num))`.... – heemayl May 9 '15 at 14:20
• well..your solution is nice and will work in python2 also..just change the `input()` to `raw_input()` for python2..thats it.. – heemayl May 9 '15 at 14:48
• `print chr(75-max(5,int('0'+raw_input('Enter the number: ')[:-1])))` – heemayl May 9 '15 at 14:50
• Then you need to change your original code too..your modifed code as it currently stands is wrong though as `python3` does not have `raw_input()`.. i suggested `raw_input()` for your initial one as you have told to run it using `python2`.. – heemayl May 9 '15 at 14:58

Here's my semi-esoteric bash solution, which populates an array with 101 entries and then checks user input against them. Even for real-world use that's reasonable--if you needed excellent performance you wouldn't be using bash, and a hundred (or so) assignments is still fast. But it would stop being reasonable if extended to a way bigger range (like a million).

``````#!/usr/bin/env bash
p(){ for i in `seq \$2 \$3`; do g[\$i]=\$1; done; }
p A 90 100; p B 80 89; p C 70 79; p D 60 69; p F 0 59
while read -r n && [[ ! \$n =~ ^[qQ] ]]; do echo \${g[\$n]}; done
``````

• It's not really that esoteric. Although it's longer than the shortest solutions, and not quite as self-documenting as the longer solutions... it's reasonably self-documenting, while still decidedly on the tiny side.
• It operates in a loop and quits on `q`, `quit`, or anything starting with `q`/`Q`.
• Hmm, this does the job, keeps making sense even after you look at it, and has the essential features. You could actually use this!

• It gives you an F when you put in non-numeric input... but that's not really so bad, is it? If you give a non-number where a number is needed, maybe you deserve an F!
• Ambiguous, possibly octal input is treated as octal (since `g` is a one-dimensional indexed array). As the old saying goes, "It's not a bug, it's a feature!" Well, maybe.
• Input that's out of range or not a number causes an empty line to be printed. There's nothing really wrong with that though: it tells you what letter grade corresponds to your input, and for wrong input there isn't one.
• Put in a negative number, and it... well, call it an easter egg.
• Still significantly longer than Tim's python solution. Yeah, I can't really spin that to seem like an advantage.

Kinda cool, huh? (Well, I think so.)

### How It Works

1. The `p` function populates a numerically indexed array `g` of grades, at indexes ranging from its first argument to its second, with the (letter) value given in its third argument.
2. `p` is called for each letter grade, to define its numeric range.
3. Keep reading user input as long as it's available and doesn't start with `q` (or `Q`), checking the `g` array for which letter grade corresponds to the number entered, and printing that letter.
• What about this conditional? `[[ \$n =~ ^(0|[1-9]+[0-9]*)\$ ]]` – 0x2b3bfa0 Jun 22 '15 at 7:43

After making it in Python 2, I decided to make it in bash.

``````#! /bin/bash

read -p "Enter the number: " i
i=0\$i
x=\$((10#\${i::-1}))
printf "\x\$(printf %x \$((11-(\$x>5?\$x:5)+64)))\n"
``````

To run, save it in a file (e.g. grade.sh), make it executable with `chmod +x grade.sh` and then run with `./grade.sh`.

This is what you will see:

``````Enter the number: 65
E
``````

How does this work?

1. Take input - `65`.
2. Add a 0 to the beginning - `065` (and the `10#` keeps it base 10).
3. Remove the last char - `06`.
4. 75 subtract that number - `70`.
5. Convert to a letter (A is 65, B is 66) - `E`.
6. Print it out - `E`.

My pronouns are He / Him

• Very clever, well done – kos May 11 '15 at 5:38
• @kos thanks :) I doubt it will work for the OP because his ranges are probably not what he posted. I expect those are for simplicity. – Tim May 11 '15 at 7:15

And here is my awk version:

``````awk '{
if(\$_ <= 100 && \$_ >= 0) {
sub(/^([0-9]|[1-5][0-9])\$/, "F", \$_);
sub(/^(6[0-9])\$/, "D", \$_);
sub(/^(7[0-9])\$/, "C", \$_);
sub(/^(8[0-9])\$/, "B", \$_);
sub(/^(9[0-9]|100)\$/, "A", \$_);
print
}
else {
print "Only numbers between 0..100"
}
}' -
``````

or as one-liner:

``````awk '{if(\$_ <= 100 && \$_ >= 0) { sub(/^([0-9]|[1-5][0-9])\$/, "F", \$_); sub(/^(6[0-9])\$/, "D", \$_); sub(/^(7[0-9])\$/, "C", \$_); sub(/^(8[0-9])\$/, "B", \$_);sub(/^(9[0-9]|100)\$/, "A", \$_);   print} else { print "Only numbers between 0..100"}}' -
``````

``````perl -E '
print "number: ";
\$n = <>;
say qw/A A B C D E F F F F F/[11-(\$n+1)/10]
if \$n=~/^\s*\d/ and 0<=\$n and \$n<=100
'
``````

### Explanation

• `perl -E` : the `-E`, like `-e`, allows passing a script as a command line argument. This is a way to run perl one-liners. Unlike `-e`, `-E` also enables all optional features (such as `say`, which is basically a `print` with a trailing newline. ).
• `print "number: ";` : prompt the user to enter a number.
• `\$n = <>;` : save that number as `\$n`.

The next bit needs to be broken down a little. `qw/string/` evaluates to a list made by breaking `string` at whitespace. So, `qw/A A B C D E F F F F F/` is actually this list:

``````0 : A
1 : A
2 : B
3 : C
4 : D
5 : E
6 : F
7 : F
8 : F
9 : F
10 : F
``````

Therefore, `say qw/A A B C D E F F F F F/[11-(\$n+1)/10]` is equivalent to

``````my @F=("A","A","B","C","D","E","F","F","F","F","F");
print "\$F[11-(\$n+1)/10]\n"
``````

Now, Perl allows using negative indices to retrieve elements counting from the end of the array. For example, `\$arrray[-1]` will print the last element of the array. In addition, floating point array indices (e.g. 10.7) are automatically truncated to the next lower integer (10.7, or 10.3 or whatever all become 10.)

The result of all this is that the index `11-(\$n+1)/10` always evaluates to the appropriate element (grade) of the array.

• Esoteric answers are all nice, but please include an explanation. – muru May 9 '15 at 15:34

Although you asked for a bash solution, I think in python, this can be done in an elegant an short way. Covering both handling errors in case of incorrect input and the "conversion" of a number between 0 and 100 into letters from A to F (or any other):

``````#!/usr/bin/env python3
try:
n = int(input("number: ")); n = n if n>0 else ""
print("FEDCBA"[[n>=f for f in [50,60,70,80,90,101]].count(True)])
except:
print("invalid input")
``````

### Explanation

1. First we need to get the number from the user:

``````n = int(input("number: "))
``````
2. We test this number to be valid for a number of conditions:

``````n>=50, n>=60, n>=70, n>=80, n>=90
``````

For each of these tests, the result will be either `False` or `True`. Therefore (compressing the code a bit):

``````[n>=f for f in [50,60,70,80,90]].count(True)]
``````

will produce a figure from `0` to `5`

3. Subsequently, we can use this figure as an index for a string, to produce a character as output, e.g.

``````"ABCDEF"[3]
``````

will output "D" (since the first character = "A")

4. The additional `101` to the list is to generate an (Index-) error in case the number exceeds `100`, since `"ABCDEF"[6]` does not exist. The same goes for `n = n if n>=0 else ""`, which will create a (Value-) error if a number below 0 is entered
In those cases, as well as if the input is not a figure, the result will be:

``````invalid input
``````

The tests:

``````number: 10
F

number: 50
E

number: 60
D

number: 70
C

number: 80
B

number: 90
A

number: 110
invalid input

number: -10
invalid input

number: Monkey
invalid input
``````