pete > courses > CS 315 Fall 26 > lecture 02: C review, basic Linux usage, program design
Lecture 02: C review, basic Linux usage, program design
Goals
- run Linux programs from the command-line
- define option and non-option arguments
- explain the difference between relative and absolute paths
- use manpages to understand how to use programs and library functions
- write a basic C program
this course is going to be mostly about using system calls to build bigger and better software components that other programmers can use to themselves write applications for people to use
it’s also going to be about using various tools that make the task of programming easier, more efficient, and more effective
today is going to be a first look at basic usage of C, and of a few of the tools we’ll be using: namely, git, make, and manpages
both git and make are super complex; in this afternoon’s lab session, we will start with very simple usage and build up the complexity over the course of the semester
throughout the course, we will also discuss "conventions"—that is, accepted ways of solving particular problems or phrasing particular solutions (we’ll see some today)
speaking of tools, the most important one in your arsenal (for this class, and likely in general in your programming career) is a text editor
it’s exactly what it advertises to be: a program that edits text
my guess is that most of you have used Atom or VSCode for this purpose in the past
(I enumerate some other alternatives here—you are more than welcome to experiment in this course, but keep in mind some have a steep learning curve—vim is a particular offender in this regard, though I am more than happy to help you with it should you wish)
if you’re using a virtual machine, you will need to install your own text editor
you will find that you also need to install, eg, a compiler, as well as the other tools I’m going to talk about later today
instructions for installing software are available under the "Things you need to know" part of the Linux guide I wrote
if you’re using weathertop, it should have both text editors and compilers installed (though neither Atom nor VSCode)
you can actually use Atom or VSCode running your own computer to edit files located on another computer: instructions at the bottom of this page
I’d like to start with a few words about running programs on Linux
specifically using the command-line or shell interface (ie, the text-based window that may seem primitive but is actually more powerful than graphical interfaces in some important ways)
files are organized into hierarchies, just as on OS X and Windows, where folders can contain files or other folders, ad infinitum
the exception is that instead of the word "folders", we call them directories on Linux
the very top of this hierarchy is "/", which is pronounced "the root directory" (which is, confusingly, distinct from another directory, "/root", which is usually pronounced "slash root")
the shell is just a program that prints out a prompt and reads/executes what you type at it
(this is simplistic, but it will do for now; examples of other shell behavior will pop up throughout the course)
every running process, including this shell that I’m projecting on screen right now, has a notion of a current working directory ("cwd")
we can ask the shell to report its current working directory (also called the current directory):
$ pwd /home/pete
we can also change the current working directory (this implies that a directory called "tmp" is one of the things in the directory called "/home/pete"):
$ cd tmp $ pwd /home/pete/tmp
we can refer to the parent directory by the special name ".."
$ cd .. $ pwd /home/pete
it might seem silly, but we can refer to the current directory by the special name "."
$ pwd /home/pete $ cd . $ pwd /home/pete
the names "tmp", "..", and "." refer to locations in the filesystem; the name for the thing that identifies a location in a filesystem is a path
therefore, we can say that the argument to cd is path to the directory we want to change to
you may observe that "/home/pete" and "/home/pete/tmp" also seem to be paths, but look a bit different; we’ll get to that in a sec
note that, in all three uses of cd above, the interpretation of the path given ("tmp" and "..") depends on the current directory
that is, "cd tmp" only worked because
- the current directory was "/home/pete"
- AND b) "/home/pete" contains a directory called "tmp"
if I was in a different directory (eg, "/home") that doesn’t itself contain a directory called "tmp", that command would not have worked:
$ cd .. $ pwd /home $ cd tmp bash: cd: tmp: No such file or directory
this is the shell (bash) telling us that it was unable to change directory because there is no such thing as "/home/tmp"
because the interpretation of "tmp", "..", and "." depends on the current working directory, these are called relative paths—they describe a location relative to where we are currently, and they mean nothing without knowing where we are currently
in contrast, paths like "/home", "/home/pete", and "/home/pete/tmp" are always meaningful: they start with "/", which is the very top of the directory hierarchy, and they always mean the same thing no matter what our current directory is
this latter type of path is called an absolute path
unless the documentation tells you otherwise, any program or function that uses paths will accept either absolute or relative paths
so this will work:
$ cd /home/pete/tmp $ pwd /home/pete/tmp
yes, this means that there are multiple ways to get where you want to go
as you get more comfortable using the command-line, you will get more efficient at specifying things like this
you might ask yourself how one might know what directories exist so that we might successfully change to them…
one uses the ls command to list files
when run with no parameters, ls lists all the files in the current directory:
$ ls
we can also ask ls to show us the files in another directory:
$ ls tmp
another way to modify the behavior of ls is to use command-line options
for instance, the -l option will show more information about each file:
$ ls -l total 220 -rw-rw---- 1 pete pete 3331 Sep 1 13:18 202-datapath-notes drwxrwx--- 2 pete pete 4096 Sep 9 12:18 bin drwxrwx--- 3 pete pete 4096 Apr 24 19:52 doc drwxrwx--- 2 pete pete 4096 Sep 15 22:54 log drwxrwx--- 2 pete pete 4096 Jun 2 2019 mnt drwxrwx--- 33 pete pete 4096 Sep 1 00:22 scratch drwxrwx--- 28 pete pete 4096 Oct 20 2020 shiznat drwxrwx--- 33 pete pete 4096 Feb 16 2020 src -rw-rw---- 1 pete pete 6608 Sep 10 19:00 sway.out drwxrwx--- 8 pete pete 4096 Sep 15 22:54 tmp -rw-rw---- 1 pete pete 178332 Jun 4 2020 "what-did-ada-lovelace's-program-actually-do.pdf"
by columns: permissions, links, owner, group, size, modification time, name (will elaborate on these in a few minutes)
we can get a long listing of another directory by combining the -l option and the "non-option" argument demonstrated above:
$ ls -l tmp
in general, option arguments a) start with a '-' or '–' and b) modify how the program operates
and non-option arguments a) don’t start with a '-' or '–' and b) modify what the program operates on
you might be wondering: how does one know what arguments a particular program accepts and what do they do?
it is tradition in UNIX-like operating systems (of which Linux is one) to ship documentation with the OS—it is often referred to as the "manual" or the "online manual"
one can access the manual using the man command
$ man ls
(scroll using arrow keys, page-up, and page-down; exit by hitting 'q')
note that it describes the basic behavior and enumerates all the options it accepts
we’ll see manual pages in a slightly different context in a few minutes
back to the output of ls -l
one of the lines looked like this:
drwx------ 28 pete pete 4096 Sep 11 18:38 scratch
the first "pete" refers to the owner of the file and the second "pete" refers to the group that owns the file
so these are two different "pete"s: the first is the name of a user and the second is the name of a group
users you are probably familiar with through interacting with your own systems; groups maybe not so much
in UNIX (and therefore, in this case, in OS X), a user may be a member of multiple groups
you may see your group membership using the id program:
$ id uid=1000(pete) gid=1000(pete) groups=1000(pete),998(wheel)
the "gid=1000(pete)" part says that my primary group is called "pete" and has id number 1000
the last part says that I am also a member of the "wheel" group (number 998)
the "drwx——" stuff describes the type and permissions of the file
the "d" means it’s a directory; "regular" files like programs and text files would have a "-" here; we’ll see a few other file types later, but these two are by far the most prevalent
as for the rest, UNIX defines three operations one might perform on a file: read, write, and execute
UNIX allows one to specify whether the user who owns the file can read, write, and/or execute it; separately, whether the group that owns the file can read, write, and/or execute it; and finally whether everyone else can read, write, or execute it (often referred to as "other")
the first triplet of those nine permissions characters refers to the permissions granted to the owner: in this case, the user pete can read, write, and execute the directory bin; but the user pete can only read and write the file notes.md
the second triplet applies to the group: any users in the group pete can read and execute the directory bin and only read the file notes.md
the final triplet applies to all users who are neither pete nor in the group pete—the "other"
(all running processes run on behalf of a user; we’ll see more of this later)
from the example above, the "drwx——" permissions means that the owner (pete) can read, write, and execute this file; members of the owning group (also named pete) can do nothing to it, and everyone else can do nothing either
as an alternative, check out this file:
$ ls -l /usr/bin/gcc -rwxr-xr-x 3 root root 1132936 Jun 22 15:52 /usr/bin/gcc
this is a regular file (leftmost character is "-"), the owner can read/write/execute it, and everyone else can both read and execute it
(since gcc is installed in /usr/bin, it is assumed to be "blessed" by the system administrator and thus "more trustworthy", which is why only the root user can write it—if anybody could write to it, a bad person could put unwanted functionality into the compiler everybody is using)
on to C…
here’s the C program we’ll use as an example for the rest of this lecture: copy-file.c
to compile it:
$ gcc -Wall -pedantic -o copy-file copy-file.c
the -Wall and -pedantic parts ask gcc to give warnings if the code does anything sketchy (or, rather, it lowers the bar for what gcc consider sketchy)—and that I will require your submissions in this class to compile without warnings using these two flags
the -o copy-file part tells gcc what to name the resulting program (in this case, the output will be put in a file named "copy-file")
and the copy-file.c part tells gcc what source file(s) to read as input
you’re going to be writing a lot of software in this class, and invariably you will start with a prompt that describes what the software should do
in the case of today’s example, the program should copy the contents of one file into another file
taking what you know about manipulating files in Python and/or Java, what high-level steps would the program need to perform to carry out this task?
something like:
- verify the command-line arguments
- open the files
- perform the copy
- close the files
just as every programming task features a prompt of some kind, the first step is always to come up with a high-level outline of behavior just like this
I strongly suggest you do this in this course (and in your future programming endeavors) as it forces you think and focus your thoughts
additionally, I strongly suggest you use these as comments in your program: eg, copy-file-design.c
now that you’ve got the skeleton, pick a comment and ask yourself whether you can immediately implement the behavior it describes in code
if you can, great: do it
it you can’t, further break down the comment into smaller steps
rinse and repeat until you’ve got the program written
bonus: now it’s commented and you don’t need to make a mad scramble to add comments, hours after you wrote the corresponding code, just to appease the professor’s seemingly-arbitrary-but-not-really grading requirements
and doing so, you’d end up with something like the file linked before: copy-file.c
let’s walk through that now
first, the overall structure
just like public static void main in Java, the main function in C is the primary entry point for all programs: you must define this function and it is where execution begins
it accepts two parameters:
- argc is the number (count) of command-line arguments
- argv is an array (vector) of those arguments
the count includes the name of the program itself, so if I run
$ ./copy-file input-file output-file
then argv[0] will be "./copy-file", argv[1] will be "input-file", and argv[2] will be "output-file"
like in Java, comments are bracketed in /* and */ pairs
now, the part that checks command-line arguments
/* check command-line arguments */
if(argc != 3) {
printf("usage: %s source dest\n", argv[0]);
exit(1);
}
src_filename = argv[1];
dst_filename = argv[2];
fairly simple: check that the correct number of arguments are provided
if not, print an error and exit
otherwise, save those arguments in well-named variables
printf is the primary function for performing output from C
in addition to printing, it also formats variables (hence the "f")
the first argument to printf ("usage: %s source dest\n") is the format string, which tells printf what the output should look like
any time the "%" symbol shows up in the format string, printf will replace it with the value of a parameter
in this case, the first one in the string ("%s") will be replaced by the value of the first parameter, argv[0]
the "s" in the format specifier indicates that the value should be interpreted as a string
you might wonder, given that the manual we looked at earlier documents programs, whether a similar mechanism exists for functions like printf
it does!
in fact, it’s the same command:
$ man printf
what you will see when you run this, however, is that the manpage that shows up does not appear to document a function called printf, but rather a program called printf
this is indeed the case
so what do we do when there exists both a program and a function with the same name?
it turns out the manual is broken into different sections: section 1 for programs and section 3 for functions (and section 2 for system calls, which we’ll see next week)
therefore, to see the documentation for the printf function, you need to tell man to look in section 3:
$ man 3 printf
much better
next part of the code:
/* open files */
in = fopen(src_filename, "r");
if(in == NULL) {
perror("fopen");
exit(2);
}
unsurprisingly, fopen opens a file
but what does the "r" mean? how can we know what parameters in general a function like fopen accepts? the manual!
$ man 3 fopen
and we see that "r" tells fopen to open the file for reading, whereas "w" opens the file for writing
also note that I’ve been pretty careful about making sure the functions worked: I check both their return values and exit if either fails
in other classes, my guess is you’ve not had to be particularly disciplined about checking return values
in this class, you will
recall that we are building software for other programmers to use: it is unreasonable to just randomly crash if something goes wrong: we need to provide more tools to our users to make sure our stuff gets used correctly
therefore, one of the requirements of your submissions will be that all return values are checked and the appropriate action taken in response to failure
in this case, either file failing to open means we can’t finish the job, and therefore exiting is the correct response
question, though: how do we know how fopen indicates an error?
read the RETURN VALUE part of the manpage (a standard part of all function manpages), which says:
Upon successful completion fopen(), fdopen() and freopen() return a FILE pointer. Otherwise, NULL is returned and errno is set to indicate the error.
easy peasy
but what is this errno thing?
errno is a global variable that indicates what kind of error has occured
therefore, when fopen encounters a problem, it will set this variable to a value that reflects what kind of problem it encountered, and then return NULL
when fopen does return, it is up to us to interpret that value
fortunately, there’s a function called perror that will print out a text representation of the error
we can test this:
$ ./copy-file foo bar fopen: No such file or directory
unsurprising, because indeed the file foo does not exist
now to the other call to fopen, which does more or less the same thing but looks a little different:
if((out = fopen(dst_filename, "w")) == NULL) {
perror("fopen");
exit(3);
}
here I’ve used a C idiom (that is, a particular way of performing an operation that is widely used) to both perform an assignment and check its result on the same line of code
it is annoyingly complicated, at least until you get used to it, but you need to get used to it because it is widely used
it’s easiest to tackle from the inside out, so we start with the function call:
fopen(dst_filename, "w")
we can check the fopen manpage to see that this opens a file for writing
next we zoom out one level:
out = fopen(dst_filename, "w")
and see that the result of fopen is assigned to the variable out
zoom out once more:
if((out = fopen(dst_filename, "w")) == NULL)
and the result of the assignment is compared to NULL
which begs the question: what is the result of an assignment statement?
in C (but not in all languages!) the result of an assignment statement is the value assigned
so here we are checking whether the value stored in out is NULL
and therefore these four lines are philosophically equivalent to the five lines used to open the input file (though you will probably see the condensed version more often in the wild)
now, actually copying bits:
while((bytes_read = fread(buf, 1, BYTES_PER_ITERATION, in)) > 0) {
bytes_written = fwrite(buf, 1, bytes_read, out);
if(bytes_written == 0) {
perror("fwrite");
exit(4);
}
}
the loop invariant is again an example of idiomatic C
again, the easiest way is to work from the inside out:
fread(buf, 1, BYTES_PER_ITERATION, in)
checking out the manpage for fread, we learn that this reads a certain number of bytes from the in and puts them into the chunk of memory pointed to by buf
looking to the top of the program, buf is declared thusly:
char buf[BYTES_PER_ITERATION];
recall that, in C, an array variable (in this case buf) is just a pointer to the first element of the array, and so buf is indeed a pointer and can be used as the first argument to fread
fread returns the number of bytes it actually read (which may be less than the number requested because the file may be smaller)
the result of this call is being saved in the variable bytes_read:
bytes_read = fread(buf, 1, BYTES_PER_ITERATION, in)
and then the result of this assignment is being compared to 0:
(bytes_read = fread(buf, 1, BYTES_PER_ITERATION, in)) > 0
again, in C, the result of an assignment is the value assigned
so this is an idiomatic way to both save the result of fread and compare it to zero
thus the loop will execute as long as fread returns a value greater than zero, which the manpage tells us it will do as long as it was able to read from the file (ie, as long as it doesn’t read the end of the file or encounter another error)
now, inside the loop, we take those bytes we just read from in and write them to out:
bytes_written = fwrite(buf, 1, bytes_read, out);
followed by our responsible error checking
note that we do NOT use BYTES_PER_ITERATION for the number of bytes to write: this is because fread may sometimes read fewer than this many (if the file isn’t big enough)
this is the kind of subtle behavior that is super important to get correct
after the loop:
/* the while loop above could terminate if fread encountered an error, so
* check that and report if necessary */
if(ferror(in)) {
perror("fread");
exit(5);
}
recall that fread can return 0 under two cases: either it reached the end of the file (which is benign) or it encountered an error (which needs to be handled)
reading the fread manpage, we find that the ferror function tells us whether an error occurred, so we use that to be responsible here
(note also that this wasn’t in our original design: we had to modify our design based on what we learned about the behavior of fread(2). we also added a comment explaining why this check exists, because it’s not immediately apparent unless you really know the behavior of fread(3))
finally, we clean up in the obvious way:
/* clean up */ fclose(out); fclose(in);
that should be enough to get you refreshed a bit with C
recall that you are welcome to consult external references when working on your assignments, with the only requirement being that you document which references you use
you are, of course, also most welcome to talk to me or the course assistants