Shell & Makefiles

Shell Environments

Remember that the shell is an interface between the users and the kernel, which manages hardware for you

Shell is about the lowest you can go in the computer

Variables

The shell environment is the place where you interact with the kernel, with a set of variables and values that allow the user to actually do anything

Ex. PATH determines the file that the shell looks at for your command and SHELL indicates the type of shell you’re using

You can also define new variables and changes other variables’ values, much like you can in C

You use shell variables by putting a $ in front of the names, many of which are define in .bash_profile and .bashrc

% echo $HOME

Two kinds of variables exist: environment variables (available to the current shell and the programs invoked from the shell) and regular shell variables (not available in programs invoked from the shell)

In bash, regular variables are defined with the following

varname=varvalue //no spaces allowed!!

For an environment variable, you do this instead

MYENVVAR="env value"
export MYENVVAR
//or
export MYENVVAR="env value"

For example, let’s say you run the following commands

% export MYENVVAR="foo"
% myregvar="bar"
% bash //here we enter a new shell
% echo $MYENVVAR
foo //has scope for the all shells
% echo $myregvar
bash: myregvar: unbound variables //has scope for only the shell it was defined in
//the above result only occurs when you have set -u in .bashrc, otherwise it's blank
% echo $SHLVL //shows the level of shell you're on

For csh/tsch, setting regular variables is a bit different

set varname=varvalue
unset varname //unsets the variable

Environment variables is also different

setenv MYENVVAR "env var"
unsetenv MYENVVAR //unsets the variable

The following are some common shell variables

But how does Unix actually find these commands?

Finding Commands

If you specify a pathname, the shell looks into that path for the executable

If you specify a filename (i.e. w/o / in the name), the shell looks for it in the search path

Sometimes there’s multiple versions of a command with the same name, so the shell will search in each directory of $PATH in left to right order and execute the first version

Startup

When bash is executed, it runs two config files: .bash_profile (runs once when you log in, sets up terminal) and .bashrc (runs each time a bash process runs, sets variables and aliases)

DO NOT ALTER THESE UNLESS YOU KNOW EXACTLY WHAT YOU’RE DOING

alias

You can also use the shell to set aliases for commands

% alias alias_name='real-command input'
//for example
% alias rm='rm -i' //good safety switch

You can put these in your .bashrc to set them up as soon as you log in

Quotes

You may notice I used single quotes instead of double quotes above

This is because different quotes mean different things in Unix

If you use double quotes, shell variables are expanded, but with single quotes, they’re simply treated as text

% echo "Welcome $HOME"
Welcome /home/user
% echo 'Welcome $HOME'
Welcome $HOME

With back quotes, the quotes are replaced with the return value of the execution of the command

% PS1=`hostname`
//below is a more standard way
% PS1=$(hostname)

History

If you have some commands that you’re repeating a bunch of times, you can repeat the last shell command you ran with !!

You can also call your history with the history command

When you’re in bash, you can also use the arrow keys

If you want a longer history list, you can change HISTSIZE in your .bashrc file

Completion

You can let the shell complete a long command name by typing the prefix and hitting TAB

Some Useful Commands

bash and tcsh (for tcsh set autolist)

export PATH=$PATH:$HOME/bin:.

export TERM=xterm

printenv or env

Shell Scripts

Much like you can make files of C code, you can make files of shell commands that are executed procedurally

These scripts usually begin with a #! and a shell name

#!/bin/sh

Any command in Unix can go into a shell script, which are then executed in the same order as they are in C

Every shell will use a different control structure, which is what makes the #! line very important

You can use this scripts to avoid repetitive tasks and to automate long tasks that are difficult to remember

For example, you can “encrypt a file with the following command

% tr abcdefghijklmnopqrstuvwxyz thequickbrownfxjmpsvalzydg < file1 > file2

This command is hard to remember, so we can take this and put it into a shell script

For example, an encrypting script will look like this

#!/bin/sh
tr abcdefghijklmnopqrstuvwxyz \ //the \ just means that the script goes to the next line
thequickbrownfxjmpsvalzydg

And a decrypting script will look like this

#!/bin/sh
tr thequickbrownfxjmpsvalzydg \
abcdefghijklmnopqrstuvwxyz

You can make these text files into executables with the chmod command

% chmod u+x encrypt decrypt

From here, you can run them like normal commands with ./ in front

% ./encrypt < file1 > file2
% ./decrypt < file2 > file2
% diff file1 file3
//no difference found

Shell variables work like they do in bash

% PATH=$PATH:$HOME/bin
% HA=$1
% PHRASE="House on the hill"
% export PHRASE

You can also use backquotes the same way you can in bash

#!/bin/sh
files=`ls`
echo $files

If you run this, you get the same result as just doing the ls command

You can also do calculations on integers with the expr command

#!/bin/sh
count=5
count=`expr $count + 1`
echo $count

You then get 6 as an output

expr also supports the following operators

Control

You also have access to all the same control statements that you do in C

For loop

#!/bin/sh
for var in val1 val2
do
	command_set //command_set is executed w/ each value of var (val1, val2...) in order
done

For example, you can print the numbers from 1 to 5 like so

#!/bin/sh
# prints from 1 to 5 //this is a comment in a shell script
for i in 1 2 3 4 5
do
	echo "$i"
done

Typing out each value gets cumbersome, so you can use (()) to do for loops like normal C

#!/bin/sh
for ((i=1; i<4; i++)); do
	for ((j=1; j<4; j++)); do
		(( value = i*j )) //(()) also allows for calculation without expr
		echo -n "$value " //-n echoes with no newline
	done
	echo
done

Bash constructs basically allow for C statements

The following is a good use case of a for loop, which just gets all the filenames in the current directory

#!/bin/sh
files=`ls`
for i in $files
do
	echo " $i"
	grep $i $i
done

You can also do this instead for the same result

#!/bin/sh
for i in *; do
	echo " $i"
	grep $i $i
done

Conditional

You can also use conditionals much like C, the only difference is that conditionals can only test whether or not a command is successful

Well-behaved commands always return back a return code (0 for a success, anything else for a failure [it’s actually just 1-255])

#!/bin/sh
if command_1
then
	command_set_1
fi //we use a different codeword to finish the conditional

For example

#!/bin/sh
//grep returns 0 if something is found and non-zero otherwise
//redurecting to /dev/null makes sure that no intermediate results get printed
if grep unix myfile >/dev/null
then
	echo "It's there"
else //else works the same way it does in C
	echo "It's not there"
fi

You can also have else if like so

#!/bin/sh
if grep "UNIX" myfile >/dev/null
then
	echo "UNIX occurs in file"
elif grep "DOS" myfile >/dev/null
then
	echo "Unix does not occur, but DOS does"
else
	echo "Nobody is there"
fi

Semicolons and Colons

Semicolons separate statements when you want separate statements on the same line, much like they do in C

#!/bin/sh
if grep "UNIX" myfile; then echo "Got it"; fi

You can also do the same thing on the command line

% ls; cd ..; ls

You can use the colon as a dummy command, which means it does nothing

#!/bin/sh
if grep "UNIX" myfile >/dev/null
then
	:
else
	echo "Nobody is there"
fi

While

While loops have the same conditional rules as if statements, with all the looping of a for loop

For example, the following script sums the numbers from 1 to 100

#!/bin/sh
i=1
sum=0
while [ $i -le 100 ]; do
	sum=`expr $sum + $i`
	i=`expr $i + 1`
done
echo The sum is $sum.

The until loop is basically the same, except it stops the execution when the command is successful

#!/bin/sh
x=1
until [ $x -gt 3 ]; do
	echo x = $x
	x=`expr $x + 1`
done

Case

Case works a lot like a switch in Java

#!/bin/sh
echo -n 'Choose command [1-4] > '
read reply
echo
case $reply in //format is case string in
	"1") date ;; //format is pattern) result_command ;;
	"2"|"3") pwd ;; //| is used as a logical or here
	"4") ls ;;
	*) echo Illegal choice! ;; //*) is the default case
esac //escaping the case

Test

If you want to actually use booleans like you do in C, you have to use the test command

Let’s see this in action with a simple example

count=0
for i in *; do
	if test x $i; then //tests if the file is executable
		count=`expr $count + 1`
	fi
done
echo "Total of $count files executable."

The following are some file switches you can use

There’s also a set of string switch you can use:

For example

if test -z $REMOTEHOST
then
	:
else
	DISPLAY="$REMOTEHOST:0"
	export DISPLAY
fi
//creates an environment variable with the remote host if it exists

Integers can also be compares with test with the following switches

-eq (=), -ne (≠), -lt (<), -le (≤), -gt (>), -ge (≥)

For example, the following script finds the smallest number in a list

#!/bin/sh
smallest=10000
for i in 5 8 19 8 7 3; do
	if test $i -lt $smallest; then
		smallest=$i
	fi
done
echo $smallest

You can also use [] as an alias for test

#!/bin/sh
smallest=10000
for i in 5 8 19 8 7 3; do
	if [ $i -lt $smallest ]; then
		smallest=$i
	fi
done
echo $smallest

Passing Input

You can pass in command line input with $1, $2 up to $9 for each input in the same order

For example, $3 would be the third input passed in

For example, the following script gets a particular line in a file

#!/bin/sh
//gets everything from the start to the line number and pipes that into the tail
//tail then gets the last element of that block, which is the line at the number
head -$1 $2 | tail -1

You can call it like so

% getfilenum filenum file

Other input variables go as follows

You can also use the shift command to shift all arguments to the left

For example, the following shell script prints the n oldest files in the directory

% oldestfiles n dir
#!/bin/sh
N=$1
shift
//does ls with the list sorted in descending order of time of modification
//takes that output and pipes into tail
//tail then removes the first line and pipes into another tail
//that tail then gives the block of N
ls -lt $* | tail -n + 2 | tail -$N

Special Variables

There’s a set of special variables available to us in Bourne Shell as well

We’ve seen a few, like $_ and $_, but there’s also:

Note that $@ and $* are different when the arguments contain spaces

You can read through all the special variables with the following

% man sh

Read

The read command reads one line of input from the standard input

% read var1 var2 var3
//for example
% read X Y Z
Here is input
% echo $X
Here
% echo $Z
input

Because this is a Unix command, you can use this in a shell script as well

Redirection

Redirection in shell works similarly to the way it does in Unix

You can use command « word to input into command up to, but not including, word beginning with word

For example, the following only displays lines with the word hello in it

#!/bin/sh
grep 'hello' << EOF //EOF stands for end of file
This is some sample text.
Here is a line with hello in it.
Here is another line with hello.
No more lines with that word.
EOF

For another example, let’s say you have a file marks.txt which some student information

091286899 90 H. White
197920499 80 J. Brown
899268899 75 A. Green

If you want to compute the average, you can do the following

#!/bin/sh
# statistics
sum=0; countfail=0; count=0;
//takes the student info from each line until we hit an error
while read studentnum grade name; do //name includes the rest of the args on the line
	sum=`expr $sum + $grade`
	count=`expr $count + 1`
	if [ $grade -lt 50 ]; then //executes when grade < 50
		countfail=`expr $countfail + 1`
	fi
done
echo The average is `expr $sum / $count`.
echo $countfail students failed.

You can execute this script on marks.txt by doing the following

% statistics < marks.txt

Makefiles

We’ve covered modularity in C with simple examples

However, when your code gets larger and larger, managing all these C files and header files manually is impossible

This is where makefiles come in

Makefiles are a way to automatically group together your disparate modules into one clear executable, which is especially useful for large projects such as the Mars Rover (which is ~1 million lines of C code)

For example, let’s say you use my_stat.h, my_stat.c and sample.c in your program

These are all the command you’ll have to run to compile the program

% cc -c my_stat.c
% cc -c sample.c
% cc -o sample same.o my_stat.o

You can put all this into a makefile using any text editor

# Makefile for sample //this is a comment
sample: sample.o my_stat.o //target: dependency1 dependency2...
	cc -o sample sample.o my_stat.o //execute command here
sample.o: sample.c my_stat.h //header file is also a dependency
	cc -c sample.c //btw these indents have to be tabs
my_stat.o: my_stat.c my_stat.h
	cc -c my_stat.c
clean: //removes all .o files and the executable
	rm sample *.o core

You then save this file with the name “Makefile” or “makefile” and execute

% make

This way, you don’t need to recompile everything in case you need to change one of your files

For example, if I were to change sample.c, only the commands for sample.o and sample would run

You can also clear every generated file with the clean command

% make clean

If you want to recompile everything, you can do this

% make clean; make

You can also do the following for the same effect

% touch my_stat.h //changes the time stamp of my_stat.h
% make

Separate Directories

You can also use makefiles with several directories, which you might need for larger programs, if you know what you’re doing with your architecture

If you store all the .c files related to each module in its own directory and all the .h files in a separate directory, you can have one makefile for each major module

The makefile for the main program will direct the creation of the final executable and makefiles for each module will create the .o bytecode files

For example, let’s say you had the following files:

StackTypes.h, StackInterface.h, QueueTypes.h, QueueInterface.h, StackImplementation.c, QueueImplementation.c, Main.c

You’ll want 4 subdirectories: Stack, Queue, Main and Include (which will store ALL of your header files)

Your makefile might look something like this for Stack

export: StackImplementation.o //exports the module
StackImplementation.o: StackImplementation.c \
												../Include/StackTypes.h \
												../Include/StackInterface.h
//-I lets us specify the path ../Include to look for .h files
	gcc -I../Include -c StackImplementation.c
# substitute a print command of your choice for lpr below
print:
	lpr StackImplementation.c
clean:
	rm -f *.o

Queue will follow the same form

For Main, it’s just a matter of collecting everything

export: Main
Main: Main.o StackDir QueueDir
	gcc -o Main Main.o ../Stack/StackImplementation.o \
											../Queue/QueueImplementation.o
Main.o: Main.c ../Include/*.h
	gcc -I../Include -c Main.c
StackDir:
// parentheses makes a subprocess, letting us cd into Stack without having to cd back
	(cd ../Stack; make export)
QueueDir:
	(cd ../Queue; make export)
print:
	lpr Main.c
printall:
	lpr Main.c
	(cd ../Stack; make print)
	(cd ../Queue; make print)
clean:
	rm -f *.o Main core
cleanall:
	rm -f *.o Main core
	(cd ../Stack; make clean)
	(cd ../Queue; make clean)

Macros

Much like in header files, macros can be used in makefiles to shorten long pieces of text

For example, our stack makefile will look like this

CC = gcc
HDIR = ../Include
INCPATH = -I$(HDIR) //general form for using a macro is $(MACRO_NAME)
DEPH = $(HDIR)/StackTypes.h
$(HDIR)/StackInterface.h
SOURCE = StackImplementation

export: $(SOURCE).o
$(SOURCE).o: $(SOURCE).c $(DEPH)
	$(CC) $(INCPATH) -c $(SOURCE).c
print:
	lpr $(SOURCE).c
clean:
	rm -f *.o

gmake allows for more complex makefiles as well, which can include loops and conditionals