| Question | Mark |
| 1. Git and Make [14 marks] | |
| 2. Life cycles and design [8 marks] | |
| 3. Bash scripting [14 marks] | |
| 4. Standards [9 marks] | |
| Exam Total [45 marks] |
Question 1: Git and Make [14]
(i) Basic make [6] Below we show the contents of a makefile and the modification times of the files in the same directory as the makefile.
Given that information, and assuming everything compiles cleanly (no errors or
warnings) show the precise output that would appear onscreen if we ran the
command make all
CFLAGS= -Wall -Wextra
objs= file1.o stuff.o prog1.o prog2.o
all: prog1x prog2x
prog1x: prog1.o stuff.o file1.o
echo "rebuilding prog1x"
g++ ${CFLAGS} $< stuff.o file1.o -o $@
prog2x: prog2.o stuff.o
@echo "rebuilding prog2x"
g++ -Wall -Wextra prog1.o stuff.o file1.o -o prog2x
prog1.o: prog1.cpp prog1.h file1.h stuff.h info.h
g++ ${CFLAGS} -c prog1.cpp
prog2.o: prog2.cpp prog2.h stuff.h info.h
g++ -Wall -Wextra -c prog2.cpp -o prog2.o
file1.o: file1.cpp file1.h info.h
g++ -c file1.cpp
stuff.o: stuff.cpp stuff.h info.h
g++ -c stuff.cpp
.phony: clean
clean: ;
@echo "cleaning now"
rm ${all} ${objs}
| Oct 5 11:29 file1.h Oct 5 11:40 file1.cpp Oct 5 12:01 prog1.cpp Oct 5 12:11 prog1.h Oct 6 12:25 stuff.cpp Oct 6 12:30 stuff.h Oct 6 12:33 prog2.h Oct 6 12:40 prog2.cpp Oct 6 12:43 makefile Oct 6 12:44 file1.o Oct 6 12:44 prog1.o Oct 6 12:44 prog1x* Oct 6 12:44 prog2.o Oct 6 12:44 prog2x* Oct 6 12:44 stuff.o Oct 6 13:50 info.h |
Sample solution g++ -Wall -Wextra -c prog1.cpp g++ -c stuff.cpp g++ -c file1.cpp echo "rebuilding prog1x" rebuilding prog1x g++ -Wall -Wextra prog1.o stuff.o file1.o -o prog1x g++ -Wall -Wextra -c prog2.cpp -o prog2.o rebuilding prog2x g++ -Wall -Wextra prog1.o stuff.o file1.o -o prog2x |
Question 1 continued:
(ii) Advanced make [8]
Suppose we wanted to use a makefile to automate our use of fork/clone/push for the csci labs and assignments across different courses.
Some of the things it would need to account for include:
R W csci265/whoever/lab1
git --git-dir csci265/lab1/.git ls-tree -r master --name-only
which outputs a list of file names
touch .pushed to create
a file identifying when we last pushed, and if we use the list of file names
above as dependencies we can see which ones are newer using the $? variable.)
Write a makefile to automate this process. (Write makefile comments anyplace where you can't think of the correct syntax/actions for specific parts.)
Sample solution
Note: this one was intended to be fairly challenging,
as it involves some out-of-the-box thinking about
your makefile and git and bash. In marking this one
I'm less concerned with getting all the nuts and bolts
exactly right, and more concerned with seeing your
thought processes/how you try to combine all the
moving parts.
The solution below is my own outline for how I'd tackle the
question given 15 minutes or so to develop an approach.
(i.e. it likely needs some careful debugging!)
# default variables for the course and repo, so the user can
# specify which one they want from the command line
Course=csci265
Repo=lab1
# we need to know the repo exists, and if so then we
# can just add/commit/push (easy way)
# or we can try to identify a list of files that
# are being tracked, and use the $? to see which
# of those have been updated since our last push
# (the git ls-tree command mentioned in the question
# could be used to generate the list, here I'm
# assuming we take the easy way)
push: $Course/$Repo/.git
git --git-dir $Course/$Repo add .
git --git-dir $Course/$Repo commit -m "auto-commit for push"
git --git-dir $Course/$Repo push
# to see if we need to clone, check if the repo exists
# (the repo is treated as a target further below)
clone: $Course/$Repo/.git
# to see if we need to fork, check if we've created a
# file named .forked_XXX where XXX is the repo name
# (the file is treated as a target further below)
fork: $Course/.forked_$Repo
# a target for the desired local repository,
# we need to have forked first
$Course/$Repo/.git: $Course/.forked_$repo
mkdir -p $Course/$Repo
((cd $Course) && (git clone csci:$Course/$USER/$Repo))
# a target for the file we create after a successful fork
# we'll use ssh csci info to get a list of accessible
# stuff on the server, see if the repo is already listed,
# and try to fork it if not
$Course/.forked_$repo:
((ssh csci info | grep $Course/$USER/$Repo) || \
((ssh -x csci fork $Course/$Repo $Course/$USER/$Repo) && \
(touch $@)))
|
Question 2: Life cycles and design [8]
Provide arguments both for and against the following claim:
Sample solution Argument for: In a waterfall model, you are carefully planning out the final product right from the early stages - you know what all the requirements will be and you can design in advance to minimize coupling/cohesion issues. Arguably the need for loose coupling and tight cohesion is also limited by the preconception that your implementation is "complete", and will not require extensive future modifications. In agile, on the other hand, since the code is constantly evolving and changing as we add and change features and functionality, it is highly likely that coupling and cohesion issues are going to arise in the code - it is unlikely our earliest designs foresaw the true role each component would have in the final product. This makes tight cohesion and loose coupling more difficult to achieve in an agile setting. Furthermore, since we are constantly needing to go back and revise the existing code base, having tight cohesion and loose coupling is much more crucial to being able to carry out our code modifications efficiently and successfully. Argument against: In an agile approach you have the ability to redesign and revise your code on the fly, as you recognize problems are arising with respect to coupling/cohesion. If the problem gets more serious over time then you can refactor that part of the system, even if it is a challenge to do so. With waterfall, on the other hand, if you have poor cohesion or coupling by the time you reach development, it means the problems are already "baked into" your design and (if truly following waterfall) you can't go back and fix it, you're stuck living with the problem. This means that getting your coupling and cohesion right in the design stage is far more crucial in a waterfall approach than it ever is in agile. |
Question 3: Bash scripting [14]
(i) Write a bash function [8]
Write a recursive bash function named staggerTouch that:
Sample solution
# note some behaviour was unspecified, all reasonable interpretations accepted:
# - what to do/return given an incorrect number of params,
# - what return value to use for error cases
# - what to do in staggerTouch if a recursive call fails
function staggerTouch()
{
# check for a valid number of parameters
if [ $# -ne 2 ] ; then
echo "staggerTouch called with incorrect number of parameters ($#)"
echo " expected use: staggerTouch directory-name delay-time"
return 1
fi
# check the directory name is valid (is directory, readable, executable)
local dirname=$1
if [ -d $dirname ] ; then
if [ -r $dirname ] ; then
if [ -x $dirname ] ; then
# check the delay time is a positive integer
local delay=$2
local intpat='^[1-9][0-9]*$'
if [[ $delay =~ $intpat ]] ; then
# touch each file, sleeping afterward
for file in $dirname/* ; do
if [ -f $file ] ; then
touch $file
sleep $delay
fi
done
# call on each subdirectory
for dir in $dirname/* ; do
if [ -d $dir ] ; then
staggerTouch $dir $delay
fi
done
return 0
else
echo "Error: delay ($delay) is not a positive integer"
return 2
fi
else
echo "Error: no execute permission for directory $dirname"
return 3
fi
else
echo "Error: no read permission for directory $dirname"
return 4
fi
else
echo "Error: $dirname is not a directory"
return 5
fi
}
|
Question 3 continued:
(ii) Write a bash script [6]
Assuming the function from part (i) is in file "q3.sh" in the same directory, write a bash script that:
foo 17 here/there/x 3
staggerTouch foo 17
then calls staggerTouch here/there/x 3
Sample solution #! /bin/bash source "q3.sh" # generate a warning if called with no arguments if [ $# -eq 0 ] ; then echo "expects an even number of arguments, in pairs of directory-name delay-time" echo "e.g. $0 hello 4 ../somedir/foo 12" fi # process elements in pairs while [ $# -gt 1 ] ; do # pass the next pair of arguments to staggerTouch staggerTouch $1 $2 # shift the two arguments out of the array of command line args shift 2 done # deal with the case where there is one leftover element at the end if [ $# -gt 0 ] ; then echo "Warning: leftover element $1" fi |
Question 4: Standards [9]
(i) make standards [5]
Recommend a set of makefile standards for the 265 project, and include justifications for your recommendations. (These would completely replace the existing standards, do not feel obligated to use/include the existing standards.)
Sample solution There could be quite a bit of variation in answers here, but some of the possibilities include: - requiring the use of variables for which compiler and which option sets (e.g. one variable for warnings, one variable for optimizations, one variable for version, etc), - requiring a variable for debugging on/off - requiring variables for the various directory paths - requiring targets for make clean, make all, make test, etc - requiring the use of -o $@ for the compilation target - for .o rules, require the relevant .c/.cpp file be the first in the pre-requisite list and requiring the use of -c $< for the compilation rule - requiring comments for each variable and each target/rule - requiring the use of .PHONY for phony targets |
(ii) standards to improve your habits [4]
Describe the aspect of your own coding style that tends to cause the most trouble for you later when debuggging/maintaining your code, and suggest one or two code standards you could/would realistically follow to improve that.
Sample solution The answers to this question will of course vary tremendously, since everyone has different strengths and weaknesses in coding and different perceptions of their own strengths and weaknesses. I'm mostly interested in seeing a genuine bit of reflection on what your weakness(es) are, and some thought on how you could improve that in a way you might actually stick to in practice. |
Makefile quick reference
# to run make as an executable file (instead of using make on command line)
#! /usr/bin/make -f
makefile -f makefilename target options # to run the named makefile
# rule format example using variables for options
# (paths required if files are in other directories)
OPTIONS = -Wall
target.o: target.cpp target.h someother.h
g++ -c target.cpp ${OPTIONS} -o target.o
# automatic variables
# $@ matches target $^ matches pre-req list
# $< matches first prereq $? matches prereqs newer than target
# ($@F) matches just the filename for target
# ($@D) matches just the directory for target
# similar formats for filename/directory of $< $? $^ variables
# variable assignment
X=Y # standard recursive assignment (X refers to Y)
X:=Y # copy value of Y into X
X?=Y # assign value of Y to X if X does not have value yet
# note that $ is used to reference a make variable, $$ for shell variables
# phony targets (no actual file for target)
.PHONY: clean
clean: ;
rm -f foo.o
# suppress display of command before running it, prepend @, eg.
@echo "whatever"
# ifeq syntax (also available: ifneq, ifdef, ifndef)
example: ;
ifeq(${X},foo)
@echo "matches"
else
@echo "no match"
fi
# loop syntax, showing use of ; and \ for line continuation
# ($$ inside loop because shell consumes one $)
L = 10 20 30
example1: ;
@for item in ${L} ; do \
echo $$item ; \
done
example2: ;
@for (( i=1; i<${M}; i++ )) ; do \
echo $$i; \
done
# also shows use of read for input into a variable
example3: ;
@while read line ; do \
echo $$line ; \
done
# use of wildcard to get list of files matching pattern
cppfiles=$(wildcard src/*.cpp)
# use of pattern substitution
newlist=$(patsubst src/%.cpp, obj/%.o, ${cppfiles})
# use of words and word to find num of words and Nth word in string
numWords=$(words ${str})
nthWord=$(word ${N},${str})
| Make continued
# other available functions include
# $(shell command) $(warning msg) $(error msg) $(sort list)
# $(strip str) - removes excess whitespace
# test if a file exists, use it if so, err msg otherwise
example4: ;
test -f ${filename} && echo "found it" ; \
test -f ${filename} || echo "did not find it"
# changing shell make uses for a target
mytarget: shell:/bin/bash
mytarget: ...stuff for the mytarget rule...
# pattern-based rules here matching any .o file it needs to build
obj/%.o: src/%.cpp hdr/%.h
g++ -c $< -o $@
# command line use of touch to change file modification date
touch filename
|
Bash/linux quick reference
# paths / is root of system, ~ is home dir (~user is user's home)
# . is current directory .. is parent directory
# file and directory commands
ls -al dir # list all files in directory (including hidden)
# with long-listing (permissions etc)
cd dir # switch to specified directory
mkdir dirname # create new directory (rmdir to remove if empty)
cp origname copyname # -r for recursive copy of dirs
mv oldname newname
rm filename # -r for directories
alias cmd='whatever full command is'
head -n N filename # show first N lines of file (tail for last N lines)
cat filename # dump contents of file to stdout
less filename # use less program to view file content
diff file1 file2 # show line-by-line differences between files
du dirname # show disk utilization for given directory and subdirs
wc -l filename # count lines in file (-c for chars, -w for words)
sort filename # display file content with lines sorted alphabetically
grep pattern file # search the file for lines containing the pattern
find dir -name fname -print # search the directory (and subdirs) for the file
gzip fname # compress the file (adds .gz extension), gunzip to uncompress
tar -cvf dir # archive the directory contents (adds .tar), -cvf to extract
# process control
sleep n # pause for n seconds
which command # find location of executable
ps # display active processes (including process ids)
kill -9 PID # kills process with specified process id
^C # kill active process (control-C)
^Z # pause current process, bg to put in background, fg to move to foreground
uptime # display system loads and how long it has been active
date # display current date/time
time command # run command and record cpu time,
# system time, and real time taken
# permissions u(user) g(group) o(other) r(read) w(write) x(execute)
chmod ugo+rwx file # + to grant permission, - to take away permission
# useful variables
$PWD # working directory $HOME # home directory
$PATH # search path for executables $$ # id of current process
$USER # username $HOSTNAME # system host name
$SHELL # current shell interpretter $OSTYPE # system OS
$LINENO # current line number in script $FUNCNAME # name of current function
$SECONDS # how long current script has been running
#! /bin/bash (or run from command line using bash scriptname)
# commands ended by semicolon ; or end of line (unless preceded by \)
# cannot have multiple bash keywords on same line without ; to separate
# assign multiple variable values, then show dereferencing
X=1; Y=X; Z=Y;
echo "${Z} ${${Z}} ${${${Z}}}" # displays "Y X 1"
# strong vs weak quotes (weak quotes permit evaluation of variables)
echo "hi $x" # displays "hi 1" (echo -e allows you to use \n for newlines)
echo 'hi $x' # displays "hi $x"
# special variables for parameter lists and command line arguments
argc=$# # look up number of args
argv=$@ # get entire array of args
arg1=$1 # get first arg ($0 is script name)
arg2=$2 # get second arg
# capturing output (echos, printfs etc) from a command or function call
x=`command` # store output in x
x=$(command) # also stores output in x
x=$(cat ${filename}) # store contents of file in variable x
# defining and calling a function, call and capture output and return value
function f ()
{
for arg in "$@" ; do
echo "next arg: $arg"
done
return $# # return values can only be ints in range 0-255 (one byte)
}
x=$(f 1 2 3)
r=$?
# command redirection
command < file1 > file2 # reads from file1, outputs into file2
command 2> file # redirects stderr instead of stdout
command &> file # redirects stderr AND stdout
command1 | command2 # command1 output used as command2 input
# note that >> appends to end of file instead of overwriting file
# different styles for integer arithmetic
i=$[$i+1] # oldschool bash syntax
let "i = i + 1" # let evaluates expression in string
i=$(( i + 1 )) # allows C-like syntax inside the ++
# use of bc to evaluate floating point
x="3.14"
y="4.5"
product=$(bc <<< "$x * $y")
# use of heredocs
command <<KEYWORD
command will work on all lines
of text until it sees the key word below
KEYWORD
| Bash continued
# array examples
arr=(10 20 30 40)
echo "first element: ${arr[0]}"
echo "num elements: ${#arr[@]}"
arr[4]=50 # adds new element at position 4
for val in "${arr[@]}" ; do
echo "next element: ${val}"
done
unset arr[3] # deletes the 40
unset arr # deletes entire array
# if/else if/else syntax with compound expressions
if [[ expr1 && expr2 ]] ; then
# commands
elif [[ expr3 || expr4 ]] ; then
# commands
elif ! [[ expr5 ]] ; then
# commands
else
# commands
fi
# note that == != < > <= >= operators are for string comparison,
# for integer comparison use -eq -ne -lt -gt -le -ge
# unary condition example (based on test)
if [ -f ${filename} ] ; then
# stuff to do if it is a file
else
# stuff to do otherwise
fi
# other unary tests
# -d (is it a directory) -r (is it readable)
# -w (is it writable) -x (is it executable)
# -z (is it null) -n (is it nonnull)
# compound expressions in [ ] use -a for and, -o for or
# loop examples
for x in a b c ; do
# body
done
for (( x=1; x<10; x++ )) ; do
# body
done
for x in ${somevar} ; do
# body
done
while [ expr ] ; do
# body
done
for word in "blah blah blah" ; do
# body
done
# to iterate across lines, set separator as newline, e.g.
IFS=$'\n'
for line in ${blockoftext} ; do
# body
done
# case statements for pattern matching (does first block that matches)
case ${x} in
*.c )
# do this for anything that ends with .c
;;
foo* )
# do this for anything that starts with foo
;;
* )
# default block
;;
esac
# subshells - create temporary shell, run instructions in that,
# example below switches to somedir, lists contents if successful
# otherwise prints error message,
# afteward the script containing this is still in old directory
(( cd somedir && ls -al ) || ( echo "no such directory" ))
# regular expressions
# outside [ ] the special chars are
# . (match any one char) * (match any non-whitespace block)
# ^ and $ match start/end of string \b matches word boundary
# + one or more, * 0 or more, {m,n} m to n, ? 0 or 1
# [a-z0-9_.] match any one character that is in range a-z, 0-9, or is a _ or .
# [^xyz] match any one character that is NOT an x, y, or z
# example: check if string contains a sequence of 5 alphanumeric characters
pattern='[a-zA-Z0-9]{5}'
if [[ str =~ ${pattern} ]] ; then
echo "${str} contains a 5-character alphanumeric sequence"
fi
# example: pattern for a string that has the exact form:
# 3-6 digits then a dash, then one or more lowercase alphas,
# then something char that isn't X
pattern='^[0-9]{3,6}-[a-z]+[^X]$'
|