CSCI 265: Fall 2019 Midterm Sample Solutions

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

Explanation: it begins by checking the prog1x dependency for all prog1.o needs to be rebuilt because the .cpp file is more recent hence the "g++ -Wall -Wextra -c prog1.cpp" (with the variable expanded) stuff.o needs to be rebuilt because the .cpp file is more recent hence the "g++ -c stuff.cpp" file1.o needs to be rebuilt because the .cpp file is more recent hence the "g++ -c file1.cpp" it displays and runs the echo command, hence the echo "rebuilding prog1x" rebuilding prog1x it displays and runs the command to build the executable, i.e. g++ -Wall -Wextra prog1.o stuff.o file1.o -o prog1x it then moves on to the prog2x dependency for all prog2.o needs to be rebuilt because the .cpp file is more recent hence the "g++ -Wall -Wextra -c prog2.cpp -o prog2.o" stuff.o does not need to be rebuilt (it was rebuilt above) it runs the echo command, producing "rebuilding prog2x" it displays and runs the "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:

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:

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

# alternative loop if you want to iterate through the arguments numerically, # (note that simply using $i, $$i, or ${$i} won't have the desired effect) for (( i=1,j=2; j<=$#; i=i+2,j=j+2 )) ; do staggerTouch ${!i} ${!j} # the ! acts like a lookup done # then to check for odd perhaps try mod 2: rem=$(($#%2)) if [ $rem -gt 0 ] ; then last=$# echo "Warning: leftover element ${!last}" fi
# another possibility to grab the i'th argument numerically is to use # arg=${$@[$i]}
# yet another possibility (though we generally avoid eval whenever possible) # arg=$(eval echo \$$i)

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


Git quick reference # working with our course git submit processes ssh -x csci info # lists accessible repos on server ssh -x csci fork csci265/lab1 csci265/$USER/lab1 # fork serverside git clone csci:csci265/$USER/lab1 # clone to local git push origin --all # to submit # link to updates in instructor's posted repo git remote add instructor csci:csci265/lab1 # fetch updates, check for changes, merge git fetch instructor git diff HEAD instructor master git merge instructor master # move a repo to the trash (serverside) ssh -x csci D trash csci265/$USER/lab1 # set up current directory as a git repo git init # display commit history git log git log --graph (ascii graph of branching) git log -p (shows full diff for each commit) git status (what isn't tracked, committed, pushed) # differences since last commit git diff # add, remove, rename files and update repo tracking git add filename git rm filename git mv oldname newname git commit -m "some message" # back up to earlier commit git revert HEAD git revert commitNum # list, create, switch-to, delete, merge branches git branch git branch newbranchname git checkout branchname git branch -D branchname git checkout . (throws away changes not yet added) git merge frombranchname

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]$'