User Tools

Site Tools


Sidebar

pfk:shell_programming

Shell Programming

This tutorial is a bridge between the chapter on BASIC and the chapter on C.

Who is this tutorial for?

This tutorial is for anyone who wants to learn C programming in a Linux environment. It is aimed towards a parent who wants to teach C to their child. It assumes you have completed the section on Commodore BASIC 7.0. There are many concepts of program design which are explained there which we will not cover in this section.

What will you learn?

You will learn to apply what you know about BASIC to shell scripting in a Linux environment. Specifically, we will cover the MVP and make a sample game:

  • You will be able to write the same programs you wrote in BASIC, but in Shell Script.
  • You will gain an understanding and a familiarity with the Linux command line environment.
  • You will be able to create your own custom commands to make working in Linux easier.
  • You will be able to write video games in Shell Script.

What is the Shell?

In the beginning (1971), people had to load programs into the computer by typing a command like “LOAD PROGRAM”. Then someone named Ken Thompson had the brilliant idea to write a program to type this for you. Ken Thompson's “shell” program had the job of loading other programs when you typed their name. Then he had another brilliant idea and started writing programs called IF and GOTO, which were just command-line programs, but worked together to create a sort of shell-based scripting/programming language. By 1977 a new shell was created, called the Bourne Shell, that brought many of these ideas together into one program. Thus Shell Scripting was… bourne. Today (1989 and onwards) we have the Bourne Again shell, for use on Linux.

The big difference between Commodore BASIC and the Shell can be encapsulated in three big ideas: One, the operating system and disk controller were software (versus hardware, such as the extra 6502 CPU in a 1541 disk drive), which allowed for users to improve the system. Second, you could execute a program merely by typing its name. Third, and this is even more revolutionary, you could pass arguments to the executable file directly on the command line, and pass the program's output this way into other programs. It is at this point I like to remind students that the Commodore PET was released in 1977, the Commodore 64 in 1982, and the Commodore 128 in 1985. Given that the Bourne Shell had been around this entire time it is difficult to imagine why Commodore didn't develop a more interesting operating system with a shell. If you want to know why Commodore didn't take over the world with a successor to the C64 and C128 as it should have done, now you have a very good answer.

Towards an MVP

As noted in the BASIC section, the MVP is really the name change game. But the number guessing game is of sufficient complexity that it feels more satisfying to create. Recalling our lessons in BASIC, for the number guessing game we are looking to find four distinct components of a computer language which will enable us to write simple programs.

  1. Output data to the user.
  2. Input data from the user (into a variable)
  3. We also need the ability to compare variables and affect program flow (ex. IF..THEN..GOTO)
  4. We need to be able to get random numbers to spice up our games.

These four components are as follows:

Component 1. Print to the screen

        #!/bin/bash

        echo "This is a way to print to the screen."

Note that the first line of a shell script must contain a comment with the path to the preferred shell script interpreter. All other comments are ignored.

Component 2, user input

#!/bin/bash

echo -n "What is your favorite number? "
read n
echo "You typed: $n"

The -n means no newline at the end of the echo.

To finish things off, we need to test the value of n and to control program flow as a result:

Component 3, comparing variables

if (("$n" < 50)); then
    echo "Your number is less than 50."
fi

The above is very simple. It treats $n as a number, and compares it using the double-parentheses construct which allows using < and > like in BASIC or C. This leads us to the proto-MVP “name change game”, which can be used to print different messages based on user input:

Proto-MVP: The Name Change Game

#!/bin/bash

echo -n "What is your name? "
    
read n
    
if [ ${n,,} == "neo" ]; then
    echo "Hello, Master!"
else
    echo "Hello, $n!"
fi

Note the use of $n expanded to ${n} and then with the ,, command applied inside the curly braces. This tells the shell interpreter to convert $n into lowercase before comparing it.

Backquote Magic

To get to the point where we can make our number guessing game, we need a way to generate random numbers. This can be achieved by cheating and noting that if you enclose something in backquotes, it will be escaped as a shell command (after all, we are a shell script) and the output will replace the backquotes. For example:

#!/bin/bash

d=`date`
echo "Today's date: $d"

Note that there are *no spaces* between the variable (d), the equals sign (=) and the escaped command in backquotes (`date`).

Next we note that since we're on a UNIX-like system we probably have access to /dev/random and a program called od (octal dump). Both of these exist at least on Linux, BSD, MacOS, AIX, Solaris, HP-UX and several other UNIX systems. The following command will net us one random byte:

od -An -N1 -i /dev/random

the command line option -An hides the address offset, -N1 shows one byte only, and -i shows it as a decimal integer. We can now assign this to a variable by enclosing that command in backquotes. With a little fandangling (pulling two bytes) we can get a larger number, then use the modulus operation to get a number between 0 and 99, then add 1 to it to get a number in the range of 1 to 100:

random_num=$((`od -An -N1 -i /dev/random | tr -d " "` % 100 + 1))

Note once again the double-parentheses construct used to do mathematical operations in shell script. Also note the use of tr to remove whitespace from the output of od.

The Number Guessing Game

We are now able to construct a number guessing game. If it feels like we're moving fast, keep in mind we already learned BASIC – all we are doing here is re-writing the same thing we already wrote, but in a different language!

The concept we will introduce for the Number Guessing game is the while loop. With this we can test to make sure the user's guess is correct or not, and that he has guesses left to play. The break command escapes from this loop, enabling us to let the user win the game before his guesses run out.

Component 5 the 'while' loop

#!/bin/bash

guesses=7
random_num=$((`od -An -N2 -i /dev/random | tr -d " "` % 100 + 1))
guess=0

while [[ $guesses -gt 0 ]]
do
    echo
    echo "You have $guesses guesses left."

    echo -n "Please guess a number: "
    read guess

    if [ $guess -gt $random_num ]; then
        echo "Your number is to big!"
    fi

    if [ $guess -lt $random_num ]; then
        echo "Your number is too small!"
    fi

    if [ $guess -eq $random_num ]; then
        echo "Good guess! You win."
        break;
    fi

    guesses=$(($guesses-1))
done

if [ $guesses -eq 0 ]; then
    echo
    echo "Oh no, you ran out of guesses! The end!"
fi

echo

The extra “echo” statements on a line by themselves are there to beautify the output of the program.

You'll also want to pay attention to the form if [ … -gt … ] and -lt which means greater than or less than, and the form echo -n which means don't output a newline at the end of the echo.

Functions

Shell Scripting with BASH allows you to create functions. This can help you refactor code into reusable chunks that you can share between shell scripts. Here is an example of a function that gives you a random number between A and B:

The 'abs' program

#!/bin/bash

n=$1

function abs() {
  abs_val=`echo -e "sqrt($1*$1)\nquit\n" | bc -q -i | head -2 | tail -1`
}

abs $n
echo $abs_val

Save this into a file named abs, then type chmod +x abs to turn it into an executable script. Then you can type ./abs -7 and the program will print out “7”. Of course, this is merely a wrapper for the use of bc, but because of how it uses a function call, you can use this version of abs in other functions in other scripts. Check out the following:

Random number program 'rnd'

#!/bin/bash

#Here, $1 and $2 are function parameters.
function min() {
    if [ "$1" -lt "$2" ]; then
        rval=$1
    else
        rval=$2
    fi
}

#Here, $1 and $2 are function parameters.
function max() {
    if [ "$1" -gt "$2" ]; then
        rval=$1
    else
        rval=$2
    fi
}

# Here, $1 and $2 are values from the command line.
min_value=$1
max_value=$2

#confirm min and max values
min $min_value $max_value
mint=$rval
max $min_value $max_value
maxt=$rval
min_value=$mint
max_value=$maxt

#calculate range
range=$(($max_value - $min_value + 1))

#get random number between min_value and max_value
random_number=$((`od -An -N2 -i /dev/random | tr -d " "` % $range + $min_value))

echo "Your random number between $min_value and $max_value is $random_number."

This program teaches us how to take arguments from the command line and also to take arguments to a function. In both cases you use $1, $2, $3… for each argument.

The GOTO function

There is no GOTO in Shell Script. The truth is, using careful design and function calls, it has been shown that GOTO is not required to write any kind of computer program. Thus, GOTO is often considered “evil” by programmers who do not know BASIC or Assembly Language.

Yet, nature always finds a way. At the start of your Shell Script, if you include the following function as follows, you can use “label:” and “jumpto label” to jump around in your code.

#!/bin/bash

function jumpto
{
    label=$1
    cmd=$(sed -n "/$label:/{:a;n;p;ba};" $0 | grep -v ':$')
    eval "$cmd"
    exit
}

start=${1:-"start"}

jumpto $start

start:
# your script goes here...

We found this solution on Bob Copeland's blog as quoted by StackOverflow, although we suspect it is a part of something much more ancient, probably discovered in an attempt to process command line arguments in a shell that didn't have scripting, such as the Thompson shell from 1971, which implemented the if command (/bin/if, the precursor to /bin/test) as a separate command. Ironically, however, the Thompson shell also made use of a separate program /bin/goto which was used to transfer control around within a shell script.

The downside of this kludge is that no other line in your code may end with a colon except a label. So you must be vigilant you have no line of code in your program such as

cat filename | grep : | cut -f2 -d:

…since the block containing such code may not be jumpto'd.

Battle Monsters

It was at this point that I asked my son to write his own game in BASH Shell Script. I told him two important things. One, he could write any kind of game he wanted, as long as it was cool. Two, I would get him some ice cream and chips after he finished the game. A couple hours later he showed me the following prorgam (full of bugs, I might add). When he ran it, nothing happened.

I'm going to go through it with you now, and we will fix the bugs together.

Battle Monsters v1, by Neo

#!/bin/bash

#Strings and variables
oppodam=$((`od - An - N2 -i /dev/random | tr -d " "` % 10 + 5))
oppohealth=$((`od - An - N2 -i /dev/random | tr -d " "` % 100 + 10))
ranoppo=$((`od - An - N2 -i /dev/random | tr -d " "` % 5 + 1))
playerhealth=$((`od - An - N2 -i /dev/random | tr -d " "` % 100 + 50))
playerdam=$((`od - An - N2 -i /dev/random | tr -d " "` % 20 + 50))

The first thing I noticed immediately is that he had put spaces between the switch and the command line argument. Mental note, explain to him how UNIX command line switches work. What is happening here is that the shell is interpreting the dash by itself on the line to represent input from stdin, so the shell blocks and waits for you to type something on the keyboard until it reaches an EOF (ctrl-d IIRC).

#Start
echo -n "What is your name? "
read n
echo -n "Your opponent is ... "

#Picking opponent
if [[ ranoppo = 1 ]]; then
	echo "an Orc!"
	echo "It does $oppodam damage."
fi

...

Here was a whole section of IF statements where the variable should have had $ sign in front, and a double equals (==) should have been used. Fixed.

#The program loop begins
while [ playerhealth !< 1 ]; do

No wonder his program wasn't working. I have no idea where he got this but I fixed it in the final listing. You're supposed to use -lt 1 (less than 1) here.

	if [ ${movement,,} = "potion" ]; then
		echo
		echo "You drink the Health potion and gain 20 health."
		playerhealth = playerhealth + 20
	fi

	if [ ${movement,,} = "attack" ]; then
		echo
		echo "You attack your opponent."
		echo "The opponent attacks back!"
		playerhealth = playerhealth - oppodam
	fi

	if [ ${movement,,} != "potion" or "attack" ] then
		echo
		echo "The opponent attacks!"
		playerhealth = playerhealth - oppodam
	fi

The logic in this section is wrong. First, you can just sit there drinking potions and nothing would ever happen to you. Second, you can't win if you stop drinking potions because you aren't doing any damage to your opponent. And of course the way he is comparing the strings in the if statement is wrong. All fixed.

Complete Listing

After a few more cosmetic changes, we had the following working program (and let me say, it was fun to play!)

Battle Monsters v2

#!/bin/bash

#Strings and variables
ranoppo=$((`od -An -N2 -i /dev/random | tr -d " "` % 6 + 1))
oppohealth=$((`od -An -N2 -i /dev/random | tr -d " "` % 10 + 20))
playerhealth=$((`od -An -N2 -i /dev/random | tr -d " "` % 10 + 10))
oppodam=$((`od -An -N2 -i /dev/random | tr -d " "` % 5 + 1))
potion_used=0

#Start
echo -n "What is your name? "
read n
echo -n "Your opponent is ... "

#Picking opponent
if [ $ranoppo -eq 1 ]; then
	echo "an Orc!"
	echo "It does $oppodam damage."
fi

if [ $ranoppo -eq 2 ]; then
	echo "a Stick monster!"
	echo "It does $oppodam damage."
fi

if [ $ranoppo -eq 3 ]; then
	echo "a Giant slug!"
	echo "It does $oppodam damage."
fi

if [ $ranoppo -eq 4 ]; then
	echo "a Zombie!"
	echo "It does $oppodam damage."
fi

if [ $ranoppo -eq 5 ]; then
	echo "a Fighting bear!"
	echo "It does $oppodam damage."
fi

#The program loop begins
while [ $playerhealth -gt 1 ]; do
        playerdam=$((`od -An -N2 -i /dev/random | tr -d " "` % 6 + 1))
        oppodam=$((`od -An -N2 -i /dev/random | tr -d " "` % 5 + 1))
        echo
        echo
        echo "Your health is $playerhealth."
        echo "Your opponent's health is $oppohealth."
        echo
        echo "Do you want to //attack// your opponent?"
        echo "Or use Health //potion// ?"
        echo -n "Choice: "
        read movement

	if [ $potion_used -lt 1 ]; then
		if [ ${movement,,} == "potion" ]; then
			echo
			echo "You drink the Health potion and gain 20 health!"
			playerhealth=$((playerhealth + 20))
        	        potion_used=1
		fi
	else
		echo "Oh no, you're out of potions!"
	fi

	if [ ${movement,,} == "attack" ]; then
		echo
		echo "You attack your opponent."
		echo "The opponent attacks back!"
		oppohealth=$((oppohealth-playerdam))
	fi

	echo
	echo "The opponent attacks!"
	playerhealth=$((playerhealth - oppodam))

	if [ $oppohealth -lt 1 ]; then
		echo
		echo
		echo "==============================="
		echo "| You defeated your opponent! |"
		echo "|-----------------------------|"
		echo "| You win!                    |"
		echo "==============================="
		echo
		exit
	fi

done

echo
echo
echo "=================================="
echo "| Oh no! You have 0 health left! |"
echo "|--------------------------------|"
echo "| Game over!                     |"
echo "=================================="
echo

I felt that with this game, he had achieved something important in the realm of shell scripting; he had reached a similar level of competence to his skill in Commodore BASIC. However, the real power of shell scripting lay ahead…

The True Power of Shell Scripting

The real reason shell scripting is important is not to write games where you battle monsters, or roll dice and get random numbers. The power is in how the shell script allows you to operate command line programs in an interactive way, and in how it can move the input and output of those programs around in more interesting ways than catting and piping (> and |).

However.

To fully grasp this amount of power would require a detailed course in command line UNIX, a detailed study of commands like awk, sed, cut, ls, df, du, ps, cron, and literallyl dozens more. However, the competent and dilligent will find a world of power available to them as you are able to do literally anything from the command line except graphical applications.

So yes, the shell is powerful, very powerful. But No, we cannot learn this now. It is something you either learn from experience, or have to go to school for (and then end up learning by experience anyways). We'll give this one time. It's time to move on to the next language!

Recommended next:

pfk/shell_programming.txt · Last modified: 2019/05/18 08:21 by serena