The goal of this homework assignment is to allow you to practice using
pointers, arrays, and strings in C. The first activity involves
building a string library, while the second activity requires you to use
this library to build a translation utility similar to tr named trit
.
For this assignment, record your source code and any responses to the
following activities in the homework07
folder of your assignments
GitHub repository and push your work by noon Saturday, April 6.
Before starting this homework assignment, you should first perform a git
pull
to retrieve any changes in your remote GitHub repository:
$ cd path/to/repository # Go to assignments repository
$ git switch master # Make sure we are in master branch
$ git pull --rebase # Get any remote changes not present locally
Next, create a new branch for this assignment:
$ git checkout -b homework07 # Create homework07 branch and check it out
To help you get started, the instructor has provided you with the following skeleton code:
# Go to homework07 folder
$ cd homework07
# Download Makefile
$ curl -LO https://www3.nd.edu/~pbui/teaching/cse.20289.sp24/static/txt/homework07/Makefile
# Download C skeleton code
$ curl -LO https://www3.nd.edu/~pbui/teaching/cse.20289.sp24/static/txt/homework07/str.c
$ curl -LO https://www3.nd.edu/~pbui/teaching/cse.20289.sp24/static/txt/homework07/str.h
$ curl -LO https://www3.nd.edu/~pbui/teaching/cse.20289.sp24/static/txt/homework07/str.unit.c
$ curl -LO https://www3.nd.edu/~pbui/teaching/cse.20289.sp24/static/txt/homework07/trit.c
Once downloaded, you should see the following files in your homework07
directory:
homework07
\_ Makefile # This is the Makefile for building all the assignment artifacts
\_ str.c # This is the string library C source file
\_ str.h # This is the string library C header file
\_ str.unit.c # This is the string library C unit test source file
\_ trit.c # This is the translation utility C source file
Now that the files are downloaded into the homework07
folder, you can
commit them to your git repository:
$ git add Makefile # Mark changes for commit
$ git add *.c *.h
$ git commit -m "Homework 07: Initial Import" # Record changes
After downloading these files, you can run make test
to run the tests.
# Run all tests (will trigger automatic download)
$ make test
You will notice that the Makefile downloads these additional test data and scripts:
homework07
\_ str.test.py # This is the string library unit test Python script
\_ str.unit.sh # This is the string library unit test shell script
\_ trit.test.sh # This is the translation utility test shell script
You will be using these unit tests to verify the correctness and behavior of your code.
The test
scripts are automatically downloaded by the Makefile
, so any
modifications you do to them will be lost when you run make
again. Likewise,
because they are automatically downloaded, you do not need to add
or commit
them to your git repository.
The details on what you need to implement for this assignment are described in the following sections.
In Python, we had the luxury of strings being first-class objects with all sorts of useful methods such as str.lower or str.rstrip. Unfortunately, strings in C are just arrays that utilize the sentinel pattern for denoting the end of the strings and the standard library is a bit bare when it comes to manipulating strings.
To rectify this situation, for the first activity you are to create a string
library, libstr
, which contains functions such as str_lower
and
str_rstrip
which implement some of the functionality present in Python
but lacking in C.
str.h
¶The str.h
file is the header file for the string utilities library, which
means it contains the function prototypes:
/* str.h: string library */
#pragma once
void str_lower(const char *s, char *w);
void str_upper(const char *s, char *w);
void str_title(const char *s, char *w);
void str_rstrip(const char *s, const char *chars, char *w);
void str_delete(const char *s, const char *chars, char *w);
void str_translate(const char *s, const char *from, const char *to, char *w);
/* vim: set sts=4 sw=4 ts=8 expandtab ft=c: */
Other programs will #include
this file in order to use the functions we
will be implementing in this library.
Note: For this task, you do not need to modify this file. Instead, you should review it and ensure you understand the provided code.
Makefile
¶The Makefile
contains all the rules or recipes for building the
project artifacts (e.g. libstr.a
, libstr.so
, etc.):
CC= gcc
CFLAGS= -Wall -g -std=gnu99 -fPIC
LD= gcc
LDFLAGS= -L.
LIBS= -lstr
AR= ar
ARFLAGS= rcs
TARGETS= libstr.so libstr.a trit.dynamic trit.static
all: $(TARGETS)
#-------------------------------------------------------------------------------
# TODO: Add rules for libstr.a libstr.so
#-------------------------------------------------------------------------------
str.o:
libstr.so:
libstr.a:
#-------------------------------------------------------------------------------
# TODO: Add rules for trit.dynamic trit.static
#-------------------------------------------------------------------------------
trit.o:
trit.dynamic:
trit.static:
#-------------------------------------------------------------------------------
# DO NOT MODIFY BELOW
#-------------------------------------------------------------------------------
...
For this task, you will need to add rules for building the static library
libstr.a
and the shared library libstr.so
. Besure to have a recipe
for any intermediate object files that both libraries require as shown in
the DAG below:
You must use the CC
, CFLAGS
, LD
, LDFLAGS
, AR
, and ARFLAGS
variables when appropriate in your rules. You should also consider
using automatic variables such as $@
and $<
as well.
Once you have a working Makefile
, you should be able to run the following commands:
# Build all TARGETS
$ make
gcc -Wall -g -std=gnu99 -fPIC -c -o str.o str.c
gcc -L. -shared -o libstr.so str.o
ar rcs libstr.a str.o
# Run str library tests
$ make test-libstr
Testing libstr.so ...
test_00_str_lower (__main__.StrTestCase) ... FAIL
test_01_str_upper (__main__.StrTestCase) ... FAIL
test_02_str_title (__main__.StrTestCase) ... FAIL
test_03_str_rstrip (__main__.StrTestCase) ... FAIL
test_04_str_delete (__main__.StrTestCase) ... FAIL
test_05_str_translate (__main__.StrTestCase) ... FAIL
Score 0.55 / 3.00
Status Failure
...
# Remove generated artifacts
$ make clean
Note: The tests will fail if you haven't implemented the string library.
You must include the -Wall
flag in your CFLAGS
when you compile. This
also means that your code must compile without any warnings, otherwise
points will be deducted.
str.c
¶The str.c
file contains the C implementation for the string library.
For this task, you will need to implement the following functions:
void str_lower(const char *s, char *w)
This function converts all the letters in
s
to lowercase (e.g.s.lower()
in Python) and stores the result inw
.
char
to lowercase.
void str_upper(const char *s, char *w)
This function converts all the letters in
s
to uppercase (e.g.s.upper()
in Python) and stores the result inw
.
char
to uppercase.
void str_title(const char *s, char *w)
This function converts all the letters in
s
to titlecase (e.g.s.title()
in Python) and stores the result inw
.
void str_rstrip(const char *s, const char *chars, char *w)
This function strips any of the specified
chars
from the back ofs
(e.g.s.rstrip(chars)
in Python) and stores the result inw
. Ifchars
isNULL
, then all trailing whitespace is stripped.
Hint: After copying the string s
to w
, work backwards until you
reach a character that should not be stripped. Construct and use a
lookup table to efficiently determine if a character should be
stripped.
void str_delete(const char *s, const char *chars, char *w)
This function deletes all the letters in
chars
froms
(e.g.s.translate({c: None for c in chars})
in Python) and stores the result inw
.
Hint: Construct and use a lookup table to efficiently determine
if a character should be copied from s
to w
.
void str_translate(const char *s, const char *from, const char *to, char *w)
This function translates the letters in
s
using the mapping provided byfrom
andto
(e.g.s.translate(string.maketrans(from, to))
in Python).
Hint: Construct and use a lookup table to efficiently determine
if a character should substituted for another character while copying
from s
to w
.
The functions above must not modify the original string s
and must
store the result in the provided w
buffer.
You are not allowed to allocate any additional temporary strings as scratch space. Moreover, you cannot use the heap or dynamic memory allocation.
The functions above must run in O(n)
(ie. linear) time and use
O(1)
(ie. constant) space.
The functions above must only use pointers when iterating through a string or accessing individual characters in a string. That is, you cannot index into a string:
/* Allowed: Iterate through string using pointers */
for (char *c = s; *c; c++)
putc(*c, stdout);
/* Forbidden: Iterate through string using array index */
for (size_t i = 0; i < strlen(s); i++) {
putc(s[i], stdout); // Not allowed
putc(*(s + i), stdout); // Also not allowed
}
You cannot use any functions provided by string.h
, including strlen
,
strchr
, or strcpy
. You must perform all operations manually using
pointers.
As you implement the functions in str.c
, you should use the str.test.py
and str.unit.sh
scripts to test each function:
# Build test artificats and run test scripts
$ make test-libstr
...
This will run two different test scripts:
str.test.py
: This is a Python unit test script that uses your
libstr.so
.
str.unit.sh
: This is a shell unit test script that uses your libstr.a
with str.unit
.
You can run each manually:
# Run Python unit test script manually
$ ./str.test.py -v
Testing libstr.so ...
test_00_str_lower (__main__.StrTestCase) ... ok
test_01_str_upper (__main__.StrTestCase) ... ok
test_02_str_title (__main__.StrTestCase) ... ok
test_03_str_rstrip (__main__.StrTestCase) ... ok
test_04_str_delete (__main__.StrTestCase) ... ok
test_05_str_translate (__main__.StrTestCase) ... ok
Score 3.00 / 3.00
Status Success
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK
# Run Shell unit test script manually
$ ./str.unit.sh
Testing libstr.a ...
str_lower ... Success
str_upper ... Success
str_title ... Success
str_rstrip ... Success
str_delete ... Success
str_translate ... Success
Score 3.00 / 3.00
To use Python to interactively test a function, you can do something like the following:
# Import ctypes package
>>> import ctypes
# Load string utilies shared library
>>> libstr = ctypes.CDLL('./libstr.so')
# Create string buffers
>>> s = ctypes.create_string_buffer(b'Hello, World!')
>>> w = ctypes.create_string_buffer(1<<10)
# Call str_lowercase function
>>> libstr.str_lower(s, w)
# View resulting value
>>> w.value
b'hello, world!'
To debug your string library in C, you can use [gdb] on str.unit
# Start gdb on str.unit
$ gdb ./str.unit
(gdb) run 0 # Run str.unit with the "0" command line argument (ie. str_lower)
...
You can also use valgrind to check for memory errors:
# Check for memory errors on str_lower test case
$ valgrind --leak-check=full ./str.unit 0
You should practice iterative development. That is, rather than writing a bunch of code and then debugging it all at once, you should concentrate on one function at a time and then test that one thing at a time. The provided unit tests allow you to check on the correctness of the individual functions without implementing everything at once. Take advantage of this and build one thing at a time.
Once you have a working string library, you are to complete the trit
translation utility:
$ ./trit.static -h # Display usage
Usage: ./trit SET1 SET2
Post Translation filters:
-l Convert to lowercase
-u Convert to uppercase
-t Convert to titlecase
-s Strip trailing whitespace
-d Delete letters in SOURCE
$ echo " Hello world " | ./trit.static # Just echo input
Hello world
$ echo " Hello world " | ./trit.static -l # Lowercase
hello world
$ echo " Hello world " | ./trit.static -u # Uppercase
HELLO WORLD
$ echo " Hello world " | ./trit.static -t # Titlecase
Hello World
$ echo " Hello world " | ./trit.static -s # Strip traling whitespace
Hello world
$ echo " Hello world " | ./trit.static -d aeio # Delete
Hll wrld
$ echo " Hello world " | ./trit.static aeio 4310 # Translate
H3ll0 w0rld
$ echo " Hello world " | ./trit.static -s -l aeio 4310 # Translate, strip, and lowercase
h3ll0 w0rld
The trit
utility must use the corresponding functions from the string
utilities library you created above to translate and filter the input text.
Makefile
¶The first task is to modify the Makefile
to include additional rules for
the following targets:
trit.static
: This is a static executable of trit.c
.
trit.dynamic
: This is a dynamic executable of trit.c
.
Be sure to add any rules for any intermediate object files.
Once you have a working Makefile
, you should be able to run the following commands:
# Build all TARGETS
$ make
gcc -Wall -g -std=gnu99 -fPIC -c -o str.o str.c
gcc -L. -shared -o libstr.so str.o
ar rcs libstr.a str.o
gcc -Wall -g -std=gnu99 -fPIC -c -o trit.o trit.c
gcc -L. -o trit.dynamic trit.o -lstr
gcc -L. -static -o trit.static trit.o -lstr
# Run all tests
$ make test
Testing libstr.so ...
test_00_str_lower (__main__.StrTestCase) ... ok
test_01_str_upper (__main__.StrTestCase) ... ok
test_02_str_title (__main__.StrTestCase) ... ok
test_03_str_rstrip (__main__.StrTestCase) ... ok
test_04_str_delete (__main__.StrTestCase) ... ok
test_05_str_translate (__main__.StrTestCase) ... ok
Score 3.00 / 3.00
Status Success
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK
Testing libstr.a ...
str_lower ... Success
str_upper ... Success
str_title ... Success
str_rstrip ... Success
str_delete ... Success
str_translate ... Success
Score 3.00 / 3.00
Status Success
Testing trit utility...
trit -h ... Success
trit -h (valgrind) ... Success
trit ... Success
trit (valgrind) ... Success
trit aeio 4310 ... Success
trit aeio 4310 (valgrind) ... Success
trit blue gold ... Success
trit blue gold (valgrind) ... Success
trit -l aeio 4310 ... Success
trit -l aeio 4310 (valgrind) ... Success
trit -u blue gold ... Success
trit -u blue gold (valgrind) ... Success
trit -t ... Success
trit -t (valgrind) ... Success
trit -s swift snake ... Success
trit -s swift snake (valgrind) ... Success
trit -d aeio ... Success
trit -d aeio (valgrind) ... Success
trit -l -u aeio 4310 ... Success
trit -l -u aeio 4310 (valgrind) ... Success
trit -l -s aeio 4310 ... Success
trit -l -s aeio 4310 (valgrind) ... Success
trit -l -d gdbye ... Success
trit -l -d gdbye (valgrind) ... Success
trit -s -t aeio 4310 ... Success
trit -s -t aeio 4310 (valgrind) ... Success
trit -u -d antm ... Success
trit -u -d antm (valgrind) ... Success
Score 3.00 / 3.00
Status Success
# Remove generated artifacts
$ make clean
trit.c
¶The trit.c
file is contains the C implementation of the translation
utility described above:
/* trit.c: translation utility */
#include "str.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Constants */
enum {
LOWER = 1<<1,
UPPER = 0, // TODO: Modify
TITLE = 0, // TODO: Modify
STRIP = 0, // TODO: Modify
DELETE = 0, // TODO: Modify
};
/* Functions */
void usage(int status) {
fprintf(stderr, "Usage: trit SET1 SET2\n\n");
fprintf(stderr, "Post Translation filters:\n\n");
fprintf(stderr, " -l Convert to lowercase\n");
fprintf(stderr, " -u Convert to uppercase\n");
fprintf(stderr, " -t Convert to titlecase\n");
fprintf(stderr, " -s Strip trailing whitespace\n");
fprintf(stderr, " -d Delete letters in SET1\n");
exit(status);
}
void translate_stream(FILE *stream, const char *set1, const char *set2, int flags) {
// TODO
}
/* Main Execution */
int main(int argc, char *argv[]) {
// TODO: Parse command line arguments
// TODO: Translate standard input
return EXIT_SUCCESS;
}
In addition to implementing command line parsing in the main
function and
you will need to implement the translate_stream
function:
void translate_stream(FILE *stream, const char *set1, const char *set2, int flags)
This function reads one line at a time from the
stream
and performs either string deletion or string translation based on the values inset1
andset2
. Any post processing is controlled byflags
, which is a bitmask that specifies which filters to apply (ie. lower, upper, title, strip).
Note: You should apply the filters in the order specified above. Overall, the flow for each line of input should be: delete or translate, apply filters, and then print result.
Hint: To handle the flags
bitmask, you should have an enumerate for
each filter, where each filter corresponds to a particular bit field:
enum { /* Define Flags */
LOWER = 1<<1,
...
};
flags |= LOWER; /* Set a Flag */
if (flags & LOWER) { /* Check a Flag */
...
}
If you need a review of enum
, checkout Chapter 22. Enumerated Types: enum.
To test your trit
utility, you can use the provided trit.test.sh
script:
# Build artifacts
$ make
# Run test script manually
$ ./trit.test.sh
Testing trit utility...
trit -h ... Success
trit -h (valgrind) ... Success
trit ... Success
trit (valgrind) ... Success
trit aeio 4310 ... Success
trit aeio 4310 (valgrind) ... Success
trit blue gold ... Success
trit blue gold (valgrind) ... Success
trit -l aeio 4310 ... Success
trit -l aeio 4310 (valgrind) ... Success
trit -u blue gold ... Success
trit -u blue gold (valgrind) ... Success
trit -t ... Success
trit -t (valgrind) ... Success
trit -s swift snake ... Success
trit -s swift snake (valgrind) ... Success
trit -d aeio ... Success
trit -d aeio (valgrind) ... Success
trit -l -u aeio 4310 ... Success
trit -l -u aeio 4310 (valgrind) ... Success
trit -l -s aeio 4310 ... Success
trit -l -s aeio 4310 (valgrind) ... Success
trit -l -d gdbye ... Success
trit -l -d gdbye (valgrind) ... Success
trit -s -t aeio 4310 ... Success
trit -s -t aeio 4310 (valgrind) ... Success
trit -u -d antm ... Success
trit -u -d antm (valgrind) ... Success
Score 3.00 / 3.00
Status Success
Alternatively, you can both build the artifacts and run the test script by doing the following:
# Build and run test scripts
$ make test-trit
...
The trit.test.sh
shell script will use the valgrind tool to verify that
your program does not contain any memory errors such as memory leaks,
uninitialized values, or invalid accesses.
You can run valgrind manually by doing:
$ echo " Hello World" | valgrind --leak-check=full ./trit.dynamic
==28627== Memcheck, a memory error detector
==28627== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==28627== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==28627== Command: ./str-dynamic
==28627==
Hello World
==28627==
==28627== HEAP SUMMARY:
==28627== in use at exit: 0 bytes in 0 blocks
==28627== total heap usage: 2 allocs, 2 frees, 5,120 bytes allocated
==28627==
==28627== All heap blocks were freed -- no leaks are possible
==28627==
==28627== For counts of detected and suppressed errors, rerun with: -v
==28627== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Note: You should run valgrind on the dynamic executable rather than the static executable. Likewise, valgrind may behave differently on macOS than it does on Linux, so beware of spurious errors on the former.
Once you have completed all the activities above, you are to complete the following reflection quiz:
As with Reading 01, you will need to store your answers in a
homework07/answers.json
file. You can use the form above to generate the
contents of this file, or you can write the JSON by hand.
To test your quiz, you can use the check.py
script:
$ ../.scripts/check.py
Checking homework07 quiz ...
Q01 0.40
Q02 0.30
Q03 0.40
Q04 0.20
Q05 0.70
Score 2.00 / 2.00
Status Success
It is always fun to find Easter Eggs in programs and services. For instance, here is the first video game easter egg (as discussed in History of Computing):
For extra credit, you are to add an easter egg to your trit
program
that does any of the following:
Display scrolling your favorite song, quote, or poem.
Run a mini-game (ie. rogue, nethack, or any of the BSD games).
Display some sort of animation (ala cmatrix or chad_stride).
Note: Your trit
program must still pass all the provided tests after
you add your easter egg.
To get credit for this Guru Point, you must show a TA a demonstration of your easter egg in action (or attach a video / screenshot to your Pull Request). You have up until one week after this assignment is due to verify your Guru Point.
To submit your assignment, please commit your work to the homework07
folder
of your homework07
branch in your assignments GitHub repository.
Your homework07
folder should only contain the following files:
Makefile
answers.json
str.c
str.h
str.unit.c
trit.c
Note: You do not need to commit the test scripts because the Makefile
automatically downloads them.
#-----------------------------------------------------------------------
# Make sure you have already completed Activity 0: Preparation
#-----------------------------------------------------------------------
...
$ git add Makefile # Mark changes for commit
$ git add str.c # Mark changes for commit
$ git add str.h # Mark changes for commit
$ git commit -m "homework07: Activity 1" # Record changes
...
$ git add Makefile # Mark changes for commit
$ git add trit.c # Mark changes for commit
$ git commit -m "homework07: Activity 2" # Record changes
...
$ git add answers.json # Mark changes for commit
$ git commit -m "homework07: Activity 3" # Record changes
$ git push -u origin homework07 # Push branch to GitHub
Remember to create a Pull Request and assign the appropriate TA from the Reading 10 TA List.
DO NOT MERGE your own Pull Request. The TAs use open Pull Requests to keep track of which assignments to grade. Closing them yourself will cause a delay in grading and confuse the TAs.