Please read the entire README (including FAQ at the end) before
starting your assignment, or asking for help.
// ~ Overview ~ //
This assignment introduces writing proper main functions, which
includes handling command-line arguments. You will re-create the
essence of three small but useful unix programs that you should become
familiar with over the semester.
// ~ Learning Goals ~ //
(1) Write your own main(...) functions, including handling
command-line switches.
(2) Write simple versions "echo", "cat", and "grep". See below for
details.
(3) Learn the basics of handling files in C, including:
(3.a) The standard file descriptors "stdin", "stdout", and "stderr".
(3.b) How to open and close a file on disk.
(3.c) How to read and write files character by character ("fgetc" and
"fputc").
(3.d) How to read and write files a line at a time ("fgets" and
"fprintf").
(4) Be able to compile, run, and test your programs for correctness,
including using valgrind to detect memory errors. See the README from
PA01 for more details.
(5) Demonstrate that you ran your code under gdb. See the README from
PA01 for more details.
(6) Demonstrate use of version management software, such as git.
// ~ Submitting Your Assignment ~ //
You must submit one zip file to blackboard. This zip file must
contain:
(1) echo-lite.c, your solution for "echo"
(2) cat-lite.c, your solution for "cat"
(3) grep-lite.c, your solution for "grep"
(4) gdb.txt, a logfile produced through running gdb.
(5) git.log, a logfile produced by git.
With the exception of git.log, notes on creating each of these files
are included in the PA01 README. See the FAQ below on how to create
the git.log file.
You create the zip file using the following command.
> zip pa04.zip echo-lite.c cat-lite.c grep-lite.c gdb.txt git.log
If your zip file does not meet the above specification, then you may
get zero for this assignment. YOU HAVE BEEN WARNED. Following the
instructions is *part* of getting the assignment correct. So please
follow the instructions.
Submit "pa04.zip" to blackboard.
********************************************************************
NOTE:
gdb.log, and git.log must be non-empty and contain evidence that you
used gdb, and git, respectively. No credit will be given if these are
empty or have irrelevant contents.
********************************************************************
// ~ Determining Your Mark ~ //
You must write "echo-lite", "cat-lite", and "grep-lite". In each case,
your solution should work respectively in similar fashion to the
standard unix commands: "echo", "cat", and "grep". You can run these
programs (which are pre-installed in every unix installation, and in
cygwin) if you want to play around with working versions of the
programs. The tester program is there to ensure that you have followed
the instructions correctly.
Run the tester program as follows:
> ./tester
You can see the commands that the tester is executing. Sometimes the
tester needs to use an input file to test one of the programs. The
input files are in the directory "testcases". You can use these files
in order to replicate a failed testcase: a recommended technique for
debugging your code.
Please refer to the PA01 README for more information on the tester
program.
// ~ Assignment Part 1: writing echo-lite ~ //
"echo" is a program that prints the arguments that you give it. It
behaves just like an echo:
> echo say cheese
say cheese
You must write a program in a single file "echo-lite.c", that prints
out the passed command-line arguments (except the first) with a single
space between each one. Then, print a single new-line character before
exiting the program. echo-lite always returns EXIT_SUCCESS.
When your program compiles and runs, it should behave like so:
> ./echo-lite
> ./echo-lite note that multiple space becomes 1.
note that multiple space becomes 1.
> ./echo-lite but you can use " quotes " to pass along spaces.
but you can use quotes to pass along spaces.
> ./echo-lite --help
--help
There is no help-message for this program. Your solution should be
about 15 lines of code.
// ~ Assignment Part 2: writing cat-lite ~ //
"cat" is a program that concatenates files and then prints them to
standard output. You must write a simple version of cat in a file
called cat-lite.c. A good solution should be around 70 lines of code.
Cat-lite must have the following features:
(1) A --help switch. (Ignore other arguments when present.) Print
the message below, and then return EXIT_SUCCESS. The help switch can
appear in any position, in which case other arguments are ignored.
Usage: cat-lite [--help] [FILE]...
With no FILE, or when FILE is -, read standard input.
Examples:
cat-lite README Print the file README to standard output.
cat-lite f - g Print f's contents, then standard input,
then g's contents.
cat-lite Copy standard input to standard output.
(2) Every argument is treated as a file name. Open each file in turn,
and print the contents to stdout. (This is easy with fgetc and fputc.)
(3) The "-" argument is interpreted as the special file "stdin". The
--help message above should make this clear.
(4) If you fail to open one of the files specified in the command-line
arguments, then you must print to stderr: "cat cannot open %s\n",
where %s is replaced with the invalid filename. You must then exit the
program with EXIT_FAILURE.
For example:
> ./cat-lite tin
cat cannot open tin
(5) If every file is opened and printed successfully, then return
EXIT_SUCCESS.
// ~ Assignment Part 3: writing grep-lite ~ //
"grep" is an amazingly useful program that filters lines of input for
specific patterns. It has quite a few features, but the ones you need
to implement are simplified and listed in the help message. The spec
for this program is:
(1) A --help switch. (Ignore other arguments when present.) Print
the message below, and then return EXIT_SUCCESS. The help switch can
appear in any position, in which case other arguments are ignored.
Usage: grep-lite [OPTION]... PATTERN
Search for PATTERN in standard input. PATTERN is a
string. grep-lite will search standard input line by
line, and (by default) print out those lines which
contain pattern as a substring.
-v, --invert-match print non-matching lines
-n, --line-number print line numbers with output
-q, --quiet suppress all output
Exit status is 0 if any line is selected, 1 otherwise;
if any error occurs, then the exit status is 2.
(2) Abort the program if any bogus command-line arguments are
passed. You must print an error message to stderr. Your main(...)
function must return 2.
(3) The user must specify a pattern when they execute
grep-lite. PATTERN is always the last argument. PATTERN can *never*
begin with a '-' character. If the last argument begins with a '-'
character, then print an error message to stderr, and your main(...)
function must return 2.
(4) If the help switch is not specified, and the command-line
arguments are good, then you must read stdin line by line. IMPORTANT:
you can assume that no line is longer than 2000 characters. Use
"fgets(...)" to read each line.
(5) Use strstr(...) on each read line to determine whether you have a
match or not. See PA02 if you are unsure what strstr(...) is.
(6) By default you select those lines that match. If one or more
invert-match switches are present, then you select non-matching
lines. (Multiple invert-match switches have the same effect as one.)
(7) By default you will print every selected line. If the line-number
switch is passed, then printf("%d:", lineNumber) before printing the
selected line. You will need to keep track of line-numbers, and the
first line of the file is line number 1. (Not 0.)
(8) If the quiet switch is passed, then you never print anything. You
must still return the correct exit code.
(9) Your main(...) function should return 0 if there was at least one
matching line, and 1 otherwise.
The grep-lite program can be written in about 100 lines of clear and
concise code.
// ~ Summary ~ //
# Compile
> gcc -Wall -Wshadow -g echo-lite.c -o echo-lite
> gcc -Wall -Wshadow -g cat-lite.c -o cat-lite
> gcc -Wall -Wshadow -g grep-lite.c -o grep-lite
# Run -- you must write your own tests
> ./echo-lite "a b c" a b c
> ./echo-lite "The rain in Spain" | ./cat-lite
> ./echo-lite "The rain in Spain" | ./cat-lite | ./grep-lite -n Spain
# Run under valgrind -- you must test everything under valgrind
> valgrind --tool=memcheck --leak-check=full --verbose --log-file=memcheck.log ./echo-lite Say Cheese
# Don't forget to *LOOK* at the log-file "memcheck.log"
# Run under gdb -- this is an *example*. You will need to adjust these
# commands to your situation.
> gdb -ex "set logging overwrite on" -ex "set logging on" ./echo-lite
(gdb) b echo-lite.c:44
(gdb) run
(gdb) p argc
(gdb) n
(gdb) n
(gdb) c
(gdb) quit
# See what your grade should be (providing you submit everything
# correctly):
> ./tester
# Create a git log file
> git log > git.log
# Create a zip file of your solution:
> zip pa04.zip echo-lite.c cat-lite.c grep-lite.c gdb.txt git.log
# Upload pa04.zip to blackboard.
// ~ FAQ ~ //
(*) What functions do I need to know to do this assignment?
Look in the notes (and the unix man pages) for:
fopen, fclose
fgetc, fputc
fgets,
printf
strstr
----------------------------------------------------------------------
(*) What are "stdin", "stdout", and "stderr"
Under normal circumstances, whenever a program launches, the operating
system (windows or unix) always does a few things before handing
control over to the computer programmer. Before invoking the main
function, a computer program is supplied with three
"file-descriptors":
(1) "stdin" is for input, and any program can *read* from this
input. For example, you would use stdin if you wanted to read
characters that a human user types into the terminal whilst the
program is running.
(2) "stdout" is for normal output and (in general) goes to the
terminal. Calling printf(...) is the _same_ as calling fprintf(stdout,
...).
(3) "stderr" is for error output. By default stderr will flow onto the
terminal along with stdout; however, read on.
You get both stdout and stderr, because when you invoke a program you
can tell the operating system where to direct each output. The
following example directs errors to a file "errors.log", and standard
output goes to the terminal:
#include
#include
int main(int argc, char * * argv)
{
fprintf(stdout, "Printing normal stuff.\n");
fprintf(stderr, "This is just a DEBUG statement that I "
"want printed to stderr, and not stdout\n");
return EXIT_SUCCESS;
}
> ./a.out 2>errors.log
Printing normal stuff
> cat errors.log
This is just a DEBUG statement that I want printed to stderr, and not
stdout
Try this out: it is very useful to know.
If any assignment (including this one) requires particular output, it
will be output to stdout unless otherwise specified. In such cases,
you can save yourself a lot of time by printing error messages and
debug statements to stderr.
----------------------------------------------------------------------
(*) Handling command-line arguments
Once your program has been compiled it cannot change, but that doesn't
mean it always has to do the same thing everytime it runs. You can
pass arguments to your program, and in turn, the program can be
written to look at these arguments and execute in different ways.
The following program prints out the current command-line
arguments. Compile and run it to see...
#include
#include
int main(int argc, char * * argv)
{
int ind;
printf("Printing command-line arguments:\n");
for(ind = 0; ind < argc; ++ind) {
printf(" argv[%d] = '%s'\n", ind, argv[ind]);
}
printf("Done!\n");
return EXIT_SUCCESS;
}
This program is very similar to "echo-lite"
----------------------------------------------------------------------
(*) What is the best way to parse command-line arguments?
The best way is something clear and simple and that _you_
understand. Look at the file "example.c" for my suggestion. After
compiling this program, run it in multiple ways using different
arguments. For example:
> gcc -Wall -Wshadow -g example.c -o icecream
> ./icecream
Do you get icecream? Sorry, try again next-time.
> ./icecream --help
Usage: ./icecream OPTIONS...
-i, --serve-icecream Not as exciting as it sounds
> ./icecream -i
Do you get icecream? YES!!!
> ./icecream --serve-icecream
Do you get icecream? YES!!!
> ./icecream blagh
Unknown switch: 'blagh'
Aborting...
You are welcome to use example.c as a starting point for this
assignment.
----------------------------------------------------------------------
(*) What is EXIT_SUCCESS and EXIT_FAILURE?
When your main function finishes it can return a value. (That's why
it's "int main(...)" and not "void main(...)".) The returned value is
called the exit status, and should be a number between 0 and 255
inclusive.
By convention, EXIT_SUCCESS is 0. When your main(...) function returns
EXIT_SUCCESS, you are saying that your program ran successfully.
EXIT_FAILURE is 1, and, in general, is interpreted as your program
saying that it failed somehow.
But, and it's a big but, you can return any number [0..255], and can
interpret it however you want.
For the grep part of the assignment, you will sometimes return "1"
when you successfully find no instances of a pattern. Return value "2"
is interpreted as a legitimate error. The real grep program works this
way.
----------------------------------------------------------------------
(*) Okay, I'm done, but what about this git.log file?
cd into *your* working repository, and type the following:
> git log
You should see a stream of text float across the screen. The text is a
log of git activities. If you read through it, you should see all of
your commit statements.
To create the "git.log" file, use a "redirect" to direct the text from
screen into a file:
> git log > git.log
Now cat the file to make sure everything is okay. (You now know what
cat is, right??)
> cat git.log