HW 2
HW 2
This homework is due by 11:59PM EDT on Tuesday Thursday, June 24 22, 2021
This homework is to be done individually, so do not share your code with anyone else
You must use C for this assignment, and all submitted code must successfully compile
via gcc with no warning messages when the -Wall (i.e., warn all) compiler option is used; we
will also use -Werror, which will treat all warnings as critical errors
All submitted code must successfully compile and run on Submitty, which uses Ubuntu
v18.04.5 LTS and gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04); also note that by
default the gnu11 C standard is used by gcc
To succeed in this course, do not rely on program output to show whether your code is correct.
Consistently allocate exactly the number of bytes you need regardless of whether you use static or
dynamic memory allocation. Further, deallocate dynamically allocated memory via free() at the
earliest possible point in your code. And as covered in initial class videos, make use of valgrind
to check for errors with dynamic memory allocation and use. Also close any open file descriptors
as soon as you are done using them. Another key to success in this course is to always read (and
re-read!) the corresponding man pages for library functions, system calls, etc.
Homework specifications
In this second homework, you will use C to implement a rudimentary interactive shell similar to
that of bash. The focus of this assignment is on process creation, process management, and inter-
process communication (IPC) using fork(), waitpid(), execv(), etc. Note that you must use
these specific system calls instead of their corresponding alternatives.
And to continue to master the use of pointers, you are not allowed to use square brackets
anywhere in your code! As with our first homework, if a '[' or ']' character is detected, including
within comments, that line of code will be removed before running gcc and you will receive a zero.
To properly implement your shell, create an infinite loop that repeatedly prompts the user to
enter a command, parses the given command, locates the command executable, then executes the
command (if found) via fork() and execv().
More specifically, to execute the given command, a child process is created via fork(); this child
process then calls execv() to execute the command in the memory space of the child process. In
the meanwhile, the parent process calls waitpid() to block its execution and wait for the child
process to terminate. This is called foreground processing.
If the user instead wants to run the command without waiting for the child process to complete its
execution, then your shell will instead use background processing. This uses the '&' character and
is explained in more detail on page 4.
Locating the command executable (in the parent process)
Before calling fork(), the command executable must be found by searching through the list of
possible paths specified by an assignment-specific environment variable called $MYPATH.
Do not use $PATH for this assignment (since $PATH is used for bash).
Similar to $PATH, this new $MYPATH environment variable consists of a series of paths delimited
by the ':' character. In your parent process, search left-to-right in $MYPATH to determine if the
requested command is found in one of the specified directories. To determine if the command
executable exists (e.g., does path /bin/cat exist?), use lstat().
If $MYPATH is not set, use "/bin:." as the default string, meaning commands will be searched for
first in the /bin directory, then in the . (i.e., current) directory.
By default, the $MYPATH variable is not set, so for testing, set and unset this variable manually in
the bash shell before running your own shell. Here’s how:
To obtain $MYPATH (or any environment variable, e.g., $HOME) from within your program, use
the getenv() function. Do not use setenv() for this assignment.
If the requested user command is found in $MYPATH, your program should call fork() and then run
the executable in the child process via the execv() system call.
Commands are line-based, as in bash. Therefore, each command may optionally have any number
of arguments (i.e., *(argv+1), *(argv+2), etc.). You can assume that each command (i.e., line
of input) read from the user will not exceed 1024 characters. Further, you can assume that each
argument will not exceed 64 characters, but all memory must be dynamically allocated.
You may also assume that command-line arguments do not contain spaces. In other words, do not
worry about parsing out quoted strings in your argument list, as in:
bash$ cat a.txt b.txt "some weird file name with spaces.txt" d.txt
Further, to help make parsing easier, you can assume that each argument (or any use of '&') is
separated and delimited by exactly one space character. You can also assume there are no leading
or trailing spaces.
2
Required output
The command prompt in the shell must simply show the '$' prompt character and one space
character. Use fgets() ((v1.2) or scanf()) to read in a command from the user and make sure
to avoid buffer overflow errors.
Required output is shown below, with sample input also shown. As per usual, you must match the
given output format exactly as shown, including the error message illustrated below ((v1.2) added
gcc to the example below).
$ cocoapuffs --extra-chocolatey
cocoapuffs: command not found
$ gcc -Wall -Werror annoying.c
$ ls
annoying.c a.out code hw2.aux hw2.log hw2.out hw2.pdf hw2.tex
$ ls -l
total 160
-rw-rw---- 1 goldsd goldsd 197 Jun 12 12:58 annoying.c
-rwxrwx--x 1 goldsd goldsd 16744 Jun 12 12:59 a.out
drwxrwx--x 2 goldsd goldsd 10 Jun 12 12:58 code
-rw-rw---- 1 goldsd goldsd 662 Jun 12 12:58 hw2.aux
-rw-rw---- 1 goldsd goldsd 14650 Jun 12 12:58 hw2.log
-rw-rw---- 1 goldsd goldsd 0 Jun 12 12:58 hw2.out
-rw-rw---- 1 goldsd goldsd 99660 Jun 12 12:58 hw2.pdf
-rw-rw---- 1 goldsd goldsd 13391 Jun 12 12:58 hw2.tex
$ cat annoying.c
/* annoying.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
while ( 1 )
{
printf( "Hey, get back to work!\n" );
sleep( 3 );
}
return EXIT_SUCCESS;
}
$ exit
bye
3
Special shell commands
Not all commands entered into the shell actually result in a call to fork(). As an example, for
the cd command, if your shell did execute this command via fork(), your shell’s current working
directory would not change! Therefore, in this case, you must instead use the chdir() system call
in the parent process. Further, if the cd command has no arguments, use the $HOME environment
variable as the target directory.
As for wildcards and special characters, you do not need to support *, ?, and [] in your shell,
though note that these are typically expanded by the shell before calling fork() and execv().
Finally, to exit your shell, the user enters exit. When this occurs, your shell must output bye and
terminate.
Normally, a shell will execute the given command via a child process, with the parent calling
waitpid() to wait for the child process to complete its command and terminate.
Your shell must also be able to execute a process in the background if the user includes an ampersand
(i.e., '&') at the end of a command. In this case, when the child process is created, the parent does
not wait for the child process to terminate before prompting the user for the next command. You
can assume that the '&' character is the last character in the given line of input (i.e., no trailing
spaces).
For each background process, the parent must report that the child process has been created.
The parent must also report when the child process does terminate (if it does). You should detect
whether any background processes have terminated right before you display the prompt to the user.
And when you do detect that a background process has terminated, display the exit status using
this format ((v1.1) replaced square brackets with angle brackets):
If multiple child processes have terminated, display a line for each of them before displaying the
prompt.
When the user exits your shell, do not clean up any of the background processes. Instead, remember
that they are inherited by the top-level init process (pid 1).
Also note that output may be interleaved with background processes, so do not expect to always
match the example output exactly line for line.
Example execution of your shell program running background processes is shown on the next page.
4
Example execution of your shell program running background processes is shown below, with the
user pressing ENTER a few times after running ls and a.out in the background to get back to the
shell prompt. ((v1.2) Note that the a.out executable corresponds to the annoying.c code shown
on page 3.)
(v1.3) Updated the sample output below to account for the user pressing ENTER before seeing the
background process termination message; therefore, blank lines indicate where the user hit ENTER.
$ ls -a
. .. annoying.c a.out code hw2.aux hw2.log hw2.out hw2.pdf hw2.tex
$ ls -a &
<running background process "ls">
$ . .. annoying.c a.out code hw2.aux hw2.log hw2.out hw2.pdf hw2.tex
$ ls
annoying.c a.out code hw2.aux hw2.log hw2.out hw2.pdf hw2.tex
$ Hey, get back to work!
Hey, get back to work!
Hey, get back to work!
$ ls xyz.txt &
<running background process "ls">
$ ls: cannot access 'xyz.txt': No such file or directory
<background process terminated with exit status 2> <=== v1.5 correction
$
$ exit
bye
bash$ Hey, get back to work!
Hey, get back to work!
Hey, get back to work!
For the above example, you will need to use kill in the bash shell to terminate the annoying a.out
background process since it will continue to execute after your shell terminates.
And again note that for this assignment, you are required to use waitpid() for both foreground
and background processes. Do not use any signal handlers.
5
Error handling
In general, report error messages to stderr, but do not abort or shutdown the running pro-
cess, if possible. Also, display a meaningful error message on stderr by using either perror()
or fprintf().
Note that you should not abort your shell program if the user enters an invalid command (e.g., com-
mand not found) or if the child process reports an error. If in doubt, try to mimic what bash does
in such situations.
Submission Instructions
To submit your assignment (and also perform final testing of your code), please use Submitty.
Note that this assignment will be available on Submitty a minimum of three days before the due
date. Please do not ask when Submitty will be available, as you should first perform adequate
testing on your own Ubuntu platform.
That said, to make sure that your program does execute properly everywhere, including Submitty,
use the techniques below.
First, make use of the DEBUG_MODE technique to make sure that Submitty does not execute any
debugging code. Here is an example:
#ifdef DEBUG_MODE
printf( "the value of q is %d\n", q );
printf( "here12\n" );
printf( "why is my program crashing here?!\n" );
printf( "aaaaaaaaaaaaagggggggghhhh!\n" );
#endif
And to compile this code in “debug” mode, use the -D flag as follows:
Second, output to standard output (stdout) is buffered. To disable buffered output for grading on
Submitty, use setvbuf() as follows:
You would not generally do this in practice, as this can substantially slow down your program, but
to ensure good results on Submitty, this is a good technique to use.