Correction:
In step 9 (top of page 3), I have "zero" and "non-zero" backwards.
The second half of the third paragraph of step 9 should read:
If the conntype is CONN_SEQ, we execute this command without regard to laststatus; if the conntype is CONN_AND, we execute this command only if laststatus is zero (because this command is preceded by '&&'); and if the conntype is CONN_OR, we execute this command only if laststatus is non-zero (because this command is preceded by '||').
I'm very sorry for the error and I hope this didn't cause you to waste any time.
Most of the general notes at the beginning of
the assignment two Q&A page
apply to assignment three, and probably to programming in general.
In addition to the universal Keep It Simple,
I'd like to repeat one point here:
In general, don't check whether operations will succeed; just try to do them and get an appropriate error result if applicable. For example, if you're about to fopen() a file, don't do a stat() and try to determine whether the file exists and/or is readable. Just do the fopen() and check for error. This results in a simpler program, and also one which functions more correctly in the invariable case that you have omitted checking something so you think it's going to succeed but it doesn't. And there can always be unexpected i/o errors, etc.
Q: What is "Makefile"?
A: The compilation of multi-file C programs is most easily managed by a program called "make", which makes (builds) files from other files according to rules you supply. You supply these rules in a file named "Makefile" or "makefile".
This was reviewed briefly in part of the tutorial on October 31, and is discussed in section 15.4 of the King book (pages 366-368) with an example. For this assignment you can just use the supplied Makefile; you don't have to modify it.
Given the Makefile I've provided, you can recompile .c files as necessary by simply typing "make".
N.B. that in the King book, his sample Makefile omits the "-Wall" flag -- this is important, and included in the fsh Makefile.
Also note that the "command" lines in a Makefile need to begin with a tab character; an equivalent number of spaces does not suffice. The "make" program decides that a line is a command, as opposed to a target and dependency list, based on this tab. Yes, this is a dubious file format, but it's how make is. In fairness, distinctions between spaces and tabs weren't blurred by editor software as badly in the 1970s as they are today.
A: No. Your program should compile with gcc -Wall with no warning or error messages. Almost all of the warning or error messages which gcc -Wall can output represent potentially-serious problems, and you need to fix them. I am willing to decode error messages by e-mail (although not generally to fix your bugs, obviously).
Q: May I create a fsh.h to declare some functions and/or variables?
A: No. Just declare them at the top of your fsh.c file, and don't create a fsh.h, and similarly don't modify builtin.h. The purpose of .h files is to coordinate declarations across multiple files. Those declarations are already present in the supplied .h files. Your submitted fsh.c and builtin.c will be compiled with the original versions of all other files.
Q: Do we have to check for errors from fork(), wait(), etc?
A: Yes. You must check the return values of all system calls, except for the extremely rare case (which does not occur in fsh) that there is nothing which you could do about the error. In almost all cases you can at a minimum print an error message and exit, or stop doing something which no longer makes sense given the failure of the first part.
Unfortunately there is a difficulty in testing many such error checks (i.e. arranging a test of your code which is testing the error status). It's hard to arrange to make a fork() fail, for example, unless there is a per-user process limit you can arrange to bump into. And making malloc() run out of memory is quite difficult.
So in some cases you need to think of some error checks as theoretical exercises, just as if we were writing the program with pen and paper and never actually running it.
But, in real life, some day one of your programs will run into an obscure error condition, and it will make a difference whether or not it performs appropriately under the circumstances. So make it good even though you can't test it. Testing is not the ultimate check of computer program behaviour anyway; it catches some kinds of errors, but misses others.
(We don't put an 'if' around the execve() call, but this is different -- we still do the error-handling, we just don't have to handle the success case because if an exec-family call succeeds, this program is overwritten with the new program so we won't be here.)
Q: Various segfault problems ("Segmentation exception").
A:
Q: How do I...
Note that unlike in lsl in assignment two, you want to call stat(), not lstat(), this time!
If there is a symlink which points to an executable file, we want to consider the path name of the symlink to be executable. E.g. if /bin/thingy is a symlink to /other/something, which is executable, then the user should be able to type "thingy" and get /bin/thingy (aka /other/something).
lstat() is only for directory hierarchy traversal, and a few specialized uses such as the implementation of "ls -l".
Remember that you only submit fsh.c and execute.c. Your submitted files must compile and work with the originals of all other files. So you want to avoid editing the other files in your working directory, and you should test at some late stage by copying the original fsh code into a new directory and then copying your fsh.c and execute.c there and typing "make".
Q: If I just press return, I get a crash / weird error message / something bad.
A: Check (e.g. with the supplied fsh.c skeleton) what you get in the struct parsed_line when you just press return, and make sure your execute() handles it properly. (Specifically, what you get is a non-null pointer to struct parsed_line, but all of its contents are null.)
Q: What's the best way to write "true" and "false" as constants in C?
A: There are many silly ideas about this topic out there. You should avoid complex constructions for simple ideas.
I recommend using "0" for false and "1" for true, rather than #defines or anything weirder. People who know C know how booleans work in C, but they don't know whatever additional constructions you create.
C99 introduces keywords (well, semi-keywords) "true" and "false", but you can't rely on having all of the C99 extensions in most C compilers (yet?), and in any case most people still don't use them because we already have 0 and 1 and they're perfectly fine.
Don't use casts.
In Java, casts are fairly safe because if the cast produces meaningless results you will generally get a runtime exception.
In C, casts are very unsafe. Basically, they turn off error messages. Error messages are good. Don't turn them off.
Only use a cast in C if you have a very good understanding of the situation. I don't believe that anything in fsh calls for a cast.
Don't use fatal() from error.c in your solution. Print appropriate error messages and loop around for the next prompt.
Q: So what are this, and the other similar functions in error.c, for?
A: Primarily parse.c. (Although you will also use efilenamecons() in your
execute().)
Q: The assignment says:
The parameter to
perror() should be the first parameter to execve() (including the
prepended directory name).
Why include the prepended directory name?
A: If not, the error message is misleading. For example, suppose you put a program named "glop" in /usr/bin and it's all ready to go, and you type "glop" to the shell and it says "glop: Permission denied". You could spend quite a while looking at /usr/bin/glop to figure out why you don't have permission before you discover that actually there is a /bin/glop, earlier in the search path, which is giving the problem. In general, error messages need to identify the file which caused the error.
Step 6 says "if p->pl->pl is non-null" -- You still also have to check p->pl (unless you are in a place in your code which would only be reached if p->pl is non-null).
A common problem which shows up in programming with fork() is that if you're not careful, the child process can end up executing code meant for the parent. In execute(), even in the case of error, the child process should exit. Your child process code must not return from execute(), because then it will return to the loop in main(), and you'll get two prompts, and it will get worse from there.
Note the comment in pipe-example.c that docommand() "does not return, under any circumstances". This includes any error circumstances as well as normal circumstances.
Q: How do you detect "argc" in writing a built-in function in fsh?
A: The argv parameter to a built-in function in fsh is terminated with a NULL pointer member, somewhat similar to how strings work in C (but with pointers instead of characters), and unlike the parameters to main() in C with a separate argc. This is because it's really being generated especially for passing to execve(), which expects argv to be in this format (see the man page).
But there are some advantages to working with argv in this format instead of having a separate int specifying its length. Anyway, it's the format you have to work with. (As a matter of fact, in unix, the parameters to main() have both formats -- there is an extra null pointer at the end of the argv array! This is not true in C generally, only in unix. So we rarely make use of this because it would be a gratuitous unportability.)
Taking builtin_exit() as an example, note how it checks its arg count. First, if argv[1] is non-null and so is argv[2], then we have at least two arguments, so it gives a usage message. Note that if argv[1] is a null pointer, that's (potentially) the end of the array, so even referencing argv[2] is an error, so we have to check in the order given and using the "short-circuit" behaviour of the "&&" operator.
Secondly, if argv[1] is a null pointer, then we have zero command-line arguments, so that's the plain "exit" command (use laststatus as the exit status), whereas if argv[1] is non-null but argv[2] is null (as tested previously), we have one command-line argument so that's the "exit ###" format of the command (where '###' is a number).
Q: What should builtin_cd() do if the HOME environment variable is not set?
A: I'm not going to answer that. Try my "fsh-solution" and see what it does. If you want to ask me for help about how to unset your HOME environment variable, or you want to ask me why it does what it does, those are fair questions (although I think it's obvious why, once you see the behaviour).
Q: Ok then, so how do I unset my HOME environment variable?
A: This differs between shells. In csh, type "unsetenv HOME". Then you can run fsh-solution and type "cd" (return). I suggest doing this in a new shell (e.g. a new terminal window) which you can then exit, because a lot of things will behave strangely without a HOME environment variable.
Q: When calling builtin_exit(), can we ignore its return value?
A: No.
Q: So we should say "laststatus = builtin_exit(...)"?
A: Yes.
Q: But how can it have a return value if it exits?
A: If there is a usage error, it doesn't exit, and its return value needs to be stored in laststatus. For example, "exit $x || echo $x is not a valid exit status" makes sense.
Q: You can use variables in fsh??
A: No. But maybe some day someone would implement them, and you wouldn't want them to have to fix a bug in your builtin_exit() handling to be able to implement variables. This kind of cutting corners only leads to grief. Write it properly.
Q: What is showprompt for? When do we change it?
A: It controls whether a '$' prompt is printed in the main loop. You don't have to change it for this assignment; it will always be 1, so fsh will always prompt.
But note how commands of the form "./fsh <file" (where file is a shell script) are messy because they output a lot of extra dollar signs. The prompt should only be shown when stdin is a terminal. Perhaps a future CSC 209 assignment will be to add this feature, which would be implemented in main(). But not today.