CMSC216 Project 4: A Shell Called Shellac
- Due: 11:59pm Mon 24-Nov-2025
- Approximately 4.0% of total grade
- Submit to Gradescope
- Projects are individual work: no collaboration with other students is allowed. Seek help from course staff if you get stuck for too long.
CODE/TEST DISTRIBUTION: p4-code.zip
VIDEO OVERVIEW: https://umd.instructure.com/courses/1388320/pages/week11-videos
CHANGELOG:
- Mon Nov 17 04:48:17 PM EST 2025
- A few minor typos have been
corrected.
- When printing jobs via
job_print(), if the PID field is 0 or negative, print just the number and omit the # symbol. Documentation comments have been added to apprise this as it affects Problem 2 Test 1. - If during
job_start()a call to anexec()-family function fails, the child process should immediately exit with the exit codeJOBCOND_FAIL_EXEC. Documentation comments have been updated to reflect this. shellac_add_job()should return the integer index (job number) of where the job is added into thejobs[]array.- Documentation strings for some
shellac_control.cfunctions have had the incorrect mention ofargv[]corrected tojobs[]which is the proper field to deal with in those functions. - A few mentions of incorrect function names have been corrected.
- When printing jobs via
- Fri Nov 14 03:46:28 PM EST 2025
- An overview video has been posted to the Week 11 Canvas videos: https://umd.instructure.com/courses/1388320/pages/week11-videos
1 Introduction: A Simple Shell
Command line shells allow one to access the capabilities of a computer using simple, interactive means. Type the name of a program and the shell will bring it into being, run it, and show output. The name "shell" is indicative of the program providing a thin convenience "layer" around more core facilities of a computing and operating system: run programs easily and see what they do. Familiarizing yourself with a basic shell implementation teaches about this interface layer and will make working in full-blown shells more palatable.
The goal of this project is to write a simple, quasi-command line
shell called shellac. The shell will be less functional in many
ways from standard shells like bash (default on most Linux
machines), zsh (default on MacOS) and tcsh (default on GRACE), but
will have some similar features to those standard tools. Like most
interesting projects, shellac uses a variety of system calls
together to accomplish its overall purpose. Most of these will be
individually discussed in lecture but the interactions between them is
what inspires real danger and romance.
Completing the project will educate an implementer on the following systems programming topics.
- Basic C Memory Discipline: A variety of strings and structs are allocated and de-allocated during execution which will require attention to detail and judicious use of memory tools like Valgrind.
- fork() and exec(): Text entered that is not recognized as a built-in is treated as an command (external program) to be executed. This spawns a child process which executes the new program.
- open(), dup2(): The shell will support basic I/O redirection that requires use of basic system calls for this purpose
- wait() and waitpid(), blocking and nonblocking: Child processes usually take a while to finish so the shell will check on their status every so often
2 Download Code and Setup
Download the code pack linked at the top of the page. Unzip this which will create a project folder. Create new files in this folder. Ultimately you will re-zip this folder to submit it.
| File | State | Notes |
|---|---|---|
shellac_main.c |
CREATE | main() function for shellac |
shellac_job.c |
CREATE | Functions that operate on the job_t struct |
shellac_control.c |
CREATE | Functions that operate on the shellac_t struct |
shellac.h |
Provided | Header file for shellac |
shellac_util.c |
Provided | Utility functions provided |
| Build/Testing Files | ||
Makefile |
Provided | Build file to compile all programs |
gradescope-submit |
Provided | Allows submission from the command line |
testy |
Testing | Test running script |
test_prob1.org |
Testing | Tests for Problem 1 |
test_prob2.org |
Testing | Tests for Problem 2 |
test_prob3.org |
Testing | Tests for Problem 3 |
test_shellac_job.c |
Testing | Unit tests for job struct |
test_shellac_control.c |
Testing | Unit tests for control struct |
test_standardize_pids |
Testing | Filter to standardize PIDs during testing |
test-data/ |
Testing | Subdirectory with files / programs used during testing |
3 Problem 1: Interactive Loop
3.1 Basic Interactive Loop
This Problem will have you complete a basic interactive loop for
shellac_main.c. You'll also need to at least create the other files
like shellac_job.c and shellac_control.c but need not put anything
in them until the next problem.
The basic interactive loop for Shellac is similar to interactive command loops we have previously worked on such as:
- Project 2's Treeset Application
- Discussion 02's List Application
Review these past code bases as needed or get help from staff if you need assistance in setting up the basic Shellac main loop. Structure the interactive loop as follows.
- An indefinite (likely
while()) loop - At the top of each loop, print the
(shellac)prompt - Read a line of text using the
fgets()function (see notes on this later) and possibly print it if echoing is enabled - Tokenize the string using the provided
tokenize_string()function - Analyze the 0th token for one of the built-in commands like
helporexitand act accordingly - If the token is not a builtin command, start a child process to execute the job (completed in Problem 2)
- The
exitcommand breaks out of the interactive loop and shuts down the program.
Like the previous interactive programs that maintained a tree, hash
table, or linked list, Shellac also maintains a data structure. It is
a shellac_t struct which has an array of job_t structs to track
processes launched in the background. Problem 2 will introduce those
types while for now, just setting up the interactive loop is
sufficient.
3.2 Sample Session
This section provides a sample of how the Shellac interactive loop should look and work once the problem is complete. It can be used as a reference and aspects of it appear in the test cases.
>> shellac (shellac) help SHELLAC COMMANDS help : show this message exit : exit the program jobs : list all background jobs that are currently running pause <secs> : pause for the given number of seconds, fractional values supported wait <jobnum> : wait for given background job to finish, error if no such job is present tokens [arg1] ... : print out all the tokens on this input line to see how they apper command [arg1] ... : Non-built-in is run as a job (shellac) (shellac) tokens x y z 4 tokens in input line tokens[0]: tokens tokens[1]: x tokens[2]: y tokens[3]: z (shellac) jobs 0 total jobs (shellac) pause 1.2347 Pausing for 1.235 seconds (shellac) wait 2 ERROR: No job '2' to wait for (shellac) exit
3.3 Printing Help
The required help message for Shellac can be produced using the following, somewhat involved string.
void print_help(){
char *helpstr = "\
SHELLAC COMMANDS\n\
help : show this message\n\
exit : exit the program\n\
jobs : list all background jobs that are currently running\n\
pause <secs> : pause for the given number of seconds, fractional values supported\n\
wait <jobnum> : wait for given background job to finish, error if no such job is present\n\
tokens [arg1] ... : print out all the tokens on this input line to see how they apper\n\
command [arg1] ... : Non-built-in is run as a job\n\
";
printf("%s",helpstr);
}
Note the use of the Backslash character which allows continuation of a
string in the next line of code. You may freely copy this function
into your shellac_main.c.
3.4 Other Supported Commands
As the help message indicates, you'll want to add cases for each built in commands. Samples of the behavior of each of these are in the sample runs but are outlined briefly here.
help
Print the help message; print_help() is ideal for this.
exit
Break out of the interactive loop and quit Shellac.
jobs
Print out all jobs presently running. Currently this will only print the message
0 total jobs
as no job launching functionality is implemented.
pause <secs>
Sleep shellac for an specified amount of time using the provided
pause_for() function in shellac_util.c. This functionality must
be present but is not likely to be tested.
wait <jobnum>
Block Shellac until the indicated job is complete. This functionality involves background jobs and will be completed in Problem 3.
tokens <arg1> <arg2> ...
Print out a listing of the tokens in the input line which will look like the following:
(shellac) tokens ls -ld -1 test-data/ 5 tokens in input line tokens[0]: tokens tokens[1]: ls tokens[2]: -ld tokens[3]: -1 tokens[4]: test-data/
This allows testing of whether the implementation is using the
provided tokenize_string() function correctly to split up the input.
It doesn't have much practical purpose other than for testing /
debugging.
cmd <arg1> <arg2> ...
Run a job (child process) according to the command line given. This functionality is the subject of later problems and does not need to be implemented yet.
3.5 Input Lines and Tokenization
The C programming language is not known for its ease of handling string processing. Tasks that are straight-forward in other languages like splitting a string on spaces are tedious and error-prone in C. You'll get some sense of this with the need to get an entire input line and split it on spaces in Shellac.
Use the fgets() function to get whole input lines in the interactive
loop. Its basic usage is as follows.
char *fgets(char dest[], int size, FILE *infile);
{
FILE *infile = ...;
char dest[64]; // a rather small size but...
char *result = fgets(dest, 64, infile);
...;
}
fgets()returnsNULLif there is no more input which is an alternative way to cause Shellac to exit (no more input to read)- If
fgets()doesn't returnNULLit will return a pointer directly to the input stored in the character buffer supplied,dest[]in the example above.
Shellac needs to analyze at least 0th space-separated string (token)
on any input line separately. Additionally, some built-in commands
need more than the 0th token while the need to start child processes
requires completely splitting up the input line into space separated
strings. This should be done using the provided tokenize_string()
function from shellac_util.c. It's use is demonstrated in the
function docstring:
EXAMPLE USAGE:
{
char input[] = "gcc -o myprog source.c > output.txt";
char *tokens[255];
int ntok;
tokenize_string(input, tokens, &ntok);
// ntoks: 6;
// tokens[0]: "gcc";
// tokens[1]: "-o";
// tokens[2]: "mpyprog";
// tokens[3]: "source.c";
// tokens[4]: ">";
// tokens[5]: "output.txt";
// input[] is now "gcc\0-o\0myprog\0source.c\0>\0output.txt"
}
Note that the tokens[] array points into the input[] array and the
function modifies it. Later these strings will be needed separately
from the input[] array so will copies will be made of them in
Problem 2.
For now, the tokenize_string() function along with a printing loop
in main() is sufficient to finish the tokens <arg1> ... builtin
command.
3.6 Exit on End of Input
One can exit Shellac using the exit command as in
(shellac) exit >>
Alternatively, in a scripted setting, one can simply stop providing
input lines and Shellac should detect the end of input (end of file)
and exit as well. In interactive settings, one can press the keystroke
Control-d to indicate the end of input. The behavior should be to
print a message and exit as in:
(shellac) # press control-d End of input >> # exited shellac and back to normal prompt
Use the return value of the input function used to get input lines in order to detect the end of input.
3.7 Command Echoing
To be compatible with tests, Shellac must support command echoing as
we have done in previous interactive programs (e.g. Project 2's
Treeset and Discussion02's List application). When Shellac is invoked
as shellac --echo with the echoing option passed, each interactive
command is printed back to the screen. Some quick examples:
>> shellac ## run without echoing (shellac) help SHELLAC COMMANDS help : show this message exit : exit the program jobs : list all background jobs that are currently running pause <secs> : pause for the given number of seconds, fractional values supported wait <jobnum> : wait for given background job to finish, error if no such job is present tokens [arg1] ... : print out all the tokens on this input line to see how they apper command [arg1] ... : Non-built-in is run as a job (shellac) exit >> shellac --echo ## run WITH echoing (shellac) help help ## "help" command is echoed SHELLAC COMMANDS help : show this message exit : exit the program jobs : list all background jobs that are currently running pause <secs> : pause for the given number of seconds, fractional values supported wait <jobnum> : wait for given background job to finish, error if no such job is present tokens [arg1] ... : print out all the tokens on this input line to see how they apper command [arg1] ... : Non-built-in is run as a job (shellac) exit ## exit command is echoed exit >>
Command Echoing in Scripting
Echoing commands allows "scripting" of Shellac with scripted sections looking like interactive sessions. Note differences below in the below
>> cat cmds.txt # a text file with some commands for shellac help tokens a b c exit ######### WITH ECHOING ########### >> shellac --echo < cmds.txt # shellac reads from input file, echoes commands (shellac) help # nice output ensues SHELLAC COMMANDS help : show this message exit : exit the program jobs : list all background jobs that are currently running pause <secs> : pause for the given number of seconds, fractional values supported wait <jobnum> : wait for given background job to finish, error if no such job is present tokens [arg1] ... : print out all the tokens on this input line to see how they apper command [arg1] ... : Non-built-in is run as a job (shellac) tokens a b c 4 tokens in input line tokens[0]: tokens tokens[1]: a tokens[2]: b tokens[3]: c (shellac) exit ######### NO ECHOING ########### >> shellac < cmds.txt # read from input but without command echoing (shellac) SHELLAC COMMANDS # prompt / output gets mingled help : show this message exit : exit the program jobs : list all background jobs that are currently running pause <secs> : pause for the given number of seconds, fractional values supported wait <jobnum> : wait for given background job to finish, error if no such job is present tokens [arg1] ... : print out all the tokens on this input line to see how they apper command [arg1] ... : Non-built-in is run as a job (shellac) 4 tokens in input line tokens[0]: tokens tokens[1]: a tokens[2]: b tokens[3]: c (shellac) >> # and things look weird
Implementing Echoing
Echoing is typically done by checking the command line arguments to
shellac_main for the string --echo and then setting some sort of
variable to indicate that echoing should be done. Each iteration of
the main loop will read a command and if echoing is turned on, the
input line will be printed back to screen. If echoing is not on, then
the command is not printed.
4 Problem 2: Basic Job Functionality
4.1 Overview
Completing this problem will add the capability of running commands in
Shellac. At its core, this is just a fork() / exec()
combination. However, to lay the groundwork for more convenience in
the shell, the functionality is broken into functions that manipulate
two structs defined in shellac.h. Once these functions are complete,
shellac_main.c can be modified to allow running a single job.
Sample Session
>> shellac (shellac) echo Hello shell implementation! === JOB 0 STARTING: echo === Hello shell implementation! === JOB 0 COMPLETED echo [#34912]: EXIT(0) === (shellac) gcc --version === JOB 0 STARTING: gcc === gcc (GCC) 13.2.1 20230801 Copyright (C) 2023 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. === JOB 0 COMPLETED gcc [#34913]: EXIT(0) === (shellac) ls / === JOB 0 STARTING: ls === bin dev home lib64 mnt proc run srv sys usr boot etc lib lost+found opt root sbin swapfile tmp var === JOB 0 COMPLETED ls [#34937]: EXIT(0) === (shellac) jobs 0 total jobs (shellac) test-data/table.sh 4 === JOB 0 STARTING: test-data/table.sh === i^1= 1 i^2= 1 i^3= 1 i^1= 2 i^2= 4 i^3= 8 i^1= 3 i^2= 9 i^3= 27 i^1= 4 i^2= 16 i^3= 64 === JOB 0 COMPLETED test-data/table.sh [#34951]: EXIT(0) === (shellac) grep string not-there.txt === JOB 0 STARTING: grep === grep: not-there.txt: No such file or directory === JOB 0 COMPLETED grep [#34955]: EXIT(2) === (shellac) exit
4.2 Shellac Structs
The following structs are defined in shellac.h and encapsulate the
idea of a child command / job and tracking a collection of them in
Shellac.
// job_t: struct to represent a running job/child process.
typedef struct {
char jobname[MAX_LINE]; // name of command like "ls" or "gcc"
char *argv[ARG_MAX+1]; // argv for running child, NULL terminated
int argc; // number of elements on command line
pid_t pid; // PID of child
int retval; // return value of child, -1 if not finished
int condition; // one of the JOBCOND_xxx values whic indicates state of job
char *output_file; // name of output file or NULL if stdout
char *input_file; // name of input file or NULL if stdin
char is_background; // 1 for background job (& on command line), 0 otherwise
} job_t;
// shellac_t: struct for tracking state of shellac program
typedef struct {
job_t *jobs[MAX_JOBS]; // array of pointers to job_t structs; may have NULLs internally
int job_count; // count of non-null job_t entries
} shellac_t;
The job_t struct tracks a single "job" or command that is being
run. The shellac_t struct contains an array of job_t structs so
that several jobs can be run simultaneously in Shellac.
Functions that operate on the structs are in the shellac_job.c and
shellac_control.c
4.3 Outline of shellac_job.c
Most but not all of the functions in shellac_job.c must be completed
for Problem 2 so that jobs can be initialized, run, and updated as
they complete. Below is an outline of the required functions. More may
be added as needed.
// shellac_job.c: functions related the job_t struct abstracting a
// running command. Most functions maninpulate jot_t structs.
#include "shellac.h"
void job_print(job_t *job);
// Prints a representation of the job. Used primarily in testing but
// useful as well for debugging. Several provided utility functions
// from shellac_util.c are useful to simplify the formatting process:
// strnull() simplifies printing nice "NULL" strings and
// job_condition_str() simplifies creating a string based on condition
// codes.
//
// SPECIAL CASE: If the pid of the child is <= 0, print it just as 0
// or -2, or whatever negative number it is without the leading #
// (hash symbol).
//
// SAMPLE OUTPUT FORMAT:
// job {
// .jobname = 'diff'
// .pid = #2378
// .retval = 1
// .condition = EXIT(1)
// .output_file = diff_output.txt
// .input_file = NULL
// .is_background = 1
// .argc = 3
// .argv[] = {
// [ 0] = diff
// [ 1] = file1.txt
// [ 2] = file2.txt
// [ 3] = NULL
// }
// }
job_t *job_new(char *argv[]);
// Create a new job based on the argv[] provided. The parameter argv[]
// will be NULL terminated to allow detecing the end of the
// array. Allocates heap memory for a job_t struct and creates heap
// copies of argv[] via string duplication. The last element in
// job.argv[] will be NULL terminated as the paramter array is. The
// initial condition of the job is INIT with -1 for its pid and
// retval. The jobname is argv[0] and. Normal foreground jobs have
// is_background set to 0.
//
// PROBLEM 3: If argv[] contains input or output redirection (> outfile
// OR < infile) or the background symbol &, then removes these from
// the argv[] and sets appropriate other fields. If problems are found
// with input/output redirection such as a ">" with no following file,
// prints an error and return NULL.
//
// HINTS for PROBLEM 3: The provided array_shift() function from the
// shellac_util.c may prove useful to shift over input / output
// redirection though other methods are possible. Note that the ">"
// "<" and "&" strings are removed from the command line so must be
// handled wth care: either don't duplicate them or free() any
// duplicates of them before returning.
void job_free(job_t *job);
// Deallocates a job structure. Deallocates the strings in the argv[]
// array. Deallocates any input / output file associated with
// fields. Finally de-allocates the struct itself.
void job_start(job_t *job);
// Forks a process and executes the command described in the job as a
// process. Changes the condition field to "RUN".
//
// PROBLEM 3: If input/output redirection is indicated by fields of
// the job, sets this up using dup2() calls. For output redirection,
// ensures any output files are created and if they already exist, are
// "clobbered" via appropriate options to open(). If input/output
// redirection fails, the child process should exit with
// JOBCOND_FAIL_OUTP or JOBCOND_FAIL_INPT. If a job fails to exec(),
// the child should exit() with code JOBCOND_FAIL_EXEC. In this case,
// there is no need for the child process to attempt to de-allocate
// any memory as the OS will recoup all its resources on exit.
int job_update_status(job_t *job);
// Checks on the job for a status update. This utilizes a wait() or
// waitpid() system call. If the job has completed, updates its
// condition to reflect either EXIT or FAIL. For exits, uses macros to
// extract the exit status and assigns it the retval field.
//
// PROBLEM 2: For foreground (default) jobs, blocks the parent process
// until the child is completed. Returns 1 for a condition change
// (e.g. RUN to EXIT / FAIL).
//
// PROBLEM 3: For background jobs, uses the WNOHANG option to avoid
// blocking the parent. If the job is finished, updates its retval,
// condition, and returns 1. If the job is not finished, just returns
// 0.
//
// For erroneous calls such as calls on a NULL job or on a non-running
// job without a pid, the behavior of this function is implementation
// dependent (may segfault, may exit with an error message, etc.) This
// situation is not tested.
4.4 Outline of shellac_control.c
Some of the functions in shellac_control.c must be completed as they
would be used to add/update jobs in the main interactive loop. Below
is a listing of the required functions that show up in unit tests.
// shellac_control.c: functions related the shellac_t struct that controls // multiple jobs #include "shellac.h" void shellac_init(shellac_t *shellac); // Initialize all fields of the shellac jobs[] array to NULL and set // the job_count to 0 int shellac_add_job(shellac_t *shellac, job_t *job); // Add a single job to the jobs[] array. Search for the first NULL // entry in the array and add that on. Does basic error checking to // see if array is full and prints an error message of some kind of // so. Returns the index in the jobs array (job number) where the new // job is added. If the jobs array is full, prints an error message // and returns -1. int shellac_remove_job(shellac_t *shellac, int jobnum); // Remove the indicated job from the jobs array and replace its entry // with NULL. Decrements the job count. De-allocates memory associated // with the job via a call to job_free(). Does basic error checking so // that if the specified jobnum is already NULL, prints an error to // that effect. void shellac_start_job(shellac_t *shellac, int jobnum); // Starts the specified job number. First prints a message about // starting the job of the format // // === JOB %d STARTING: %s ===\n // // with jobnum and jobname filled in. Then uses a call to job_start() // to start the job. void shellac_print_jobs(shellac_t *shellac); // Prints the job number and jobname of all non-NULL jobs in the jobs // array. void shellac_free_jobs(shellac_t *shellac); // Traverses the jobs array and de-allocates any non-null jobs. void shellac_update_one(shellac_t *shellac, int jobnum); // Updates a single job via a cal to job_update_status(). If that // functions return value indicates that the job completed, prints a // message of the form // // === JOB %d COMPLETED %s [#%d]: %s ===\n // // with jobnum, name, pid number, and ending condition reported. Uses // the job_condition_str() functon to create the condition string. For // completed jobs, de-allocates them and removes them from the jobs // array. // // Examples of the printed message: // === JOB 0 COMPLETED bash [#1000]: EXIT(0) === // === JOB 5 COMPLETED gcc [#22830]: EXIT(1) === // === JOB 1 COMPLETED cat [#22833]: FAIL(INPT) === void shellac_update_all(shellac_t *shellac); // Iterates through all jobs in the jobs array and updates them. Used // mainly to check for the completion of background jobs at the end of // each interactive loop iteration. void shellac_wait_one(shellac_t *shellac, int jobnum); // Change the status of a background job to foreground // (e.g. is_background becomes 0) then update that job to wait for // it. Does basic error checking so that if the jobnum indicated // doesn't exit, an error message of some sort is printed.
4.5 Implementation Notes
Overall
- Complete most of the functions in
shellac_job.cthough not all functionality needs to be present yet: some notes are given in the documentation strings for functions on which aspects of functions must work for Problem 3 only. - Similarly complete most of the functions in
shellac_control.c. Not all are tested in Problem 2 but having basic implementations will allow the automated tests to run. - Finally modify
shellac_main.cto add in handling of commands to launch foreground jobs.
Dprintf() for Debugging
It may be worthwhile to utilize the Dprintf() function provided
in shellac_util.c. This will print a printf() style message
for debugging ONLY if a debug environment is detected via an
environment variable. For example including the following line
Dprintf("forking off child process\n");
won't print anything unless Shellac is run as follows:
DEBUG=1 ./shellac
in which case when the debug message happens, it will print as
|DEBUG| forking off child process
This can be useful to include debug messages that can be enabled as you work but will be silenced during testing.
shellac_job.c
- The
job_tis tracks information on a running child process. It's most important parts are theargv[]field which is used to name the job and eventually passed toexec()as well as itspidwhich is assigned when the job is started. - When creating a job via
job_new(), the parameterargv[]should have all strings duplicated viastrdup()calls. Later these duplicated strings are de-allocatd injob_free(). Duplicating the strings allows the job struct to move independently of the parameter strings: the passed parameters can change by reading a new input line without affecting the running job. Notice that during printing in
job_print(), the fieldargv[]is printed up to theargcindex which should always be NULL:job { .jobname = 'diff' ... .argc = 3 .argv[] = { [ 0] = diff [ 1] = file1.txt [ 2] = file2.txt [ 3] = NULL } }This is because
argv[]will eventually be passed to anexecvp()call that needs the array to be NULL terminated and the printing helps verify that this is set up correctly in the struct.- For Problem 2's version of
job_new(), there is no need to check for input/output redirection via<and>or for the background symbol&. However, this will be revisited in Problem 3. job_start()amounts tofork()'ing a process. The parent, which remains Shellac, should return immediately after capturing the PID of the child process. The child process should recognize itself as such and then use a call toexecvp()to become a command that is described injobnameandargv[].- In
job_start()do some error checking for failures tofork()andexec(). Specifically, if the child fails to exec, exit withJOBCOND_FAIL_EXECto notify Shellac of the failure. job_update_status()essentially wraps thewaitpid()system call. Its behavior on foreground jobs (is_background==0) is to block the parent until the child finishes. After waiting, check the child with macros likeWIFEXITED()adjust theconditionof the job toJOBCOND_EXITfor a normal exit orJOBCOND_FAIL_OTHERif the program did not exit normally. For normal exits, extract the exit status withWEXITSTATUS()and set the job'sretvalto this.
shellac_control.c
- Most of the functions in
shellac_control.cwork on theshellac_tstruct, a glorified array ofjob_tstructs. - The struct maintains a
jobcountof how many jobs are tracked. In problem 2, this will mainly be 1 only which is stored in the 0th array slot. However, build towards tracking multiple jobs. The
jobs[]array in ashellac_tmay be sparsely populated as injobcount = 3 jobs[0] = NULL jobs[1] = job {.jobname="ls", .... } jobs[2] = job {.jobname="gcc", .... } jobs[3] = NULL jobs[4] = NULL jobs[5] = job {.jobname="wc", .... } jobs[6] = NULL ...Notice that there are 3 non-null entries but they are not contiguous so that they have job numbers 1,2,5. This is intentional and is not essential for Problem 2's ultimate goal to run single jobs but will become important in Problem 3. Some of the functionality such as
shellac_print_jobs()relies on the aboveNULLstructure so that printing the above array should yield:[1] ls [2] gcc [5] wc 3 total jobs
This is not tested until Problem 3 but is simple to enough to implement now.
- Perform error checking on changes to the
shellac_t'sjobs[]array. Check that the array is not full on adding and that requests to remove a job or start one at a given index are non-NULL.
Modifications to Shellac main()
- Once the most of the functions are complete, add a
shellac_tstruct toshellac_main.c. On detecting a not-builtin command likelsorgcc, use functions to create a Job, add it to theshellac_tand immediately update it. - Immediately waiting on a job treaties as a "foreground" job: don't print the prompt or accept another command until the running job completes. Alternatively, background jobs will be supported by the end of Problem 3.
5 Problem 3: I/O Redirection and Background Jobs
5.1 Overview
Completing this problem adds input and output redirection to the shell as well as the ability to run a job in the background, features most standard command line shells possess. It will make modifications to all the source files to affect this outcome.
Each of these is supported with the following syntax and they may be used in any combination.
| Description | Format |
|---|---|
Output redirection via > |
cmd arg1 arg2 ... > outfile.txt |
Input redirection via < |
cmd arg1 arg2 ... < infile.txt |
Background job via & |
cmd arg1 arg2 ... & |
| Combined out/in redirect | cmd arg1 arg2 ... > outfile.txt < infile.txt |
| Combined in redirect + BG | cmd arg1 arg2 ... < infile.txt & |
| etc. |
5.2 Sample Session
(shellac) echo hello world === JOB 0 STARTING: echo === hello world === JOB 0 COMPLETED echo [#35434]: EXIT(0) === (shellac) echo hello redirection > somefile.txt === JOB 0 STARTING: echo === === JOB 0 COMPLETED echo [#35435]: EXIT(0) === (shellac) cat somefile.txt === JOB 0 STARTING: cat === hello redirection === JOB 0 COMPLETED cat [#35438]: EXIT(0) === (shellac) wc < somefile.txt === JOB 0 STARTING: wc === 1 2 18 === JOB 0 COMPLETED wc [#35440]: EXIT(0) === (shellac) seq 20 > nums.txt === JOB 0 STARTING: seq === === JOB 0 COMPLETED seq [#35464]: EXIT(0) === (shellac) grep 2 < nums.txt > found.txt === JOB 0 STARTING: grep === === JOB 0 COMPLETED grep [#35467]: EXIT(0) === (shellac) cat found.txt === JOB 0 STARTING: cat === 2 12 20 === JOB 0 COMPLETED cat [#35468]: EXIT(0) === (shellac) test-data/sleepprint.sh 5 hello I'm awake & === JOB 0 STARTING: test-data/sleepprint.sh === (shellac) test-data/sleepprint.sh 5 hello I'm awake & === JOB 1 STARTING: test-data/sleepprint.sh === (shellac) test-data/sleepprint.sh 5 hello I'm awake & === JOB 2 STARTING: test-data/sleepprint.sh === (shellac) jobs [0] test-data/sleepprint.sh [1] test-data/sleepprint.sh [2] test-data/sleepprint.sh 3 total jobs (shellac) hello I'm awake hello I'm awake hello I'm awake === JOB 0 COMPLETED test-data/sleepprint.sh [#35479]: EXIT(0) === === JOB 1 COMPLETED test-data/sleepprint.sh [#35481]: EXIT(0) === === JOB 2 COMPLETED test-data/sleepprint.sh [#35483]: EXIT(0) === (shellac) jobs 0 total jobs (shellac) exit
5.3 I/O Redirection
Revisit
job_new()to searchargv[]for the symbols<and>followed by a file. The>or<and the file following it should be removed from theargv[]array for the command and have theoutput_fileorinput_fileset appropriately. Here is an example from the tests:{ char *args[] = { "ls", ">", "output.txt", NULL }; job_t *job = job_new(args); job_print(job); job_free(job); } ---OUTPUT--- job { .jobname = 'ls' .pid = -1 .retval = -1 .condition = INIT .output_file = output.txt .input_file = NULL .is_background = 0 .argc = 1 .argv[] = { [ 0] = ls [ 1] = NULL } }Note how
argcis only 1: both>andoutput.txthave been removed fromargv[]. This removal can be done either during the copying loop from the parameterargv[]to the struct fieldargrv[]or afterwards. Careful to free any memory associated with extraneous strings like>which won't have pointers to them anymore.- Revisit
job_start()and add cases afterfork()has been called so that a child process re-arranges file descriptors.open()a file for reading on input redirection or writing on output redirection. Usedup2()to overwrite either Standard In/Out (or both) with the opened file. When opening for writing, make sure to use options that will accomplish these there items
O_WRONLY: for writingO_CREAT: which will create a file if it is not presentO_TRUNC: which will "truncate" any existing file to 0 size so that it is completely overwritten rather than writing bytes to only its beginning
As well, use the version 3 arg version of
open(fname, opts, perms)and set the permissions toS_IRUSR|S_IWUSRto enable further reading/writing of the created file.- Do some basic error checking: if a file can't be opened, detect it
and exit the child process with either
JOBCOND_FAIL_INPTorJOBCOND_FAIL_OUTPto indicate a problem with the redirection. - Check that the changes in the Shellac main loop have taken effect and then try the unit tests.
5.4 Background Jobs
- Revisit
job_new()to check for the&background symbol. When detected, set theis_backgroundfield to 1. Revisit
job_update_status()to respect theis_backgroundfieldis_background==0should block the parent on awaitpid()until the child finishesis_background==1should return immediately to the parent onwaitpid()if the child is not yet finished. This can be accomplished with theWNOHANGoption towaitpid().
Return a 0 when a child job is not complete yet.
- Complete any missing functionality in
shellac_control.cfor functions likeshellac_add()so that multiple jobs can be added. As well, complete theshellac_update_all()function which waits for all jobs in theshellacstruct. Use repeated calls toshellac_update_one()for this. Note that nowjob_update()may return a 0 to indicate that the job is not done yet in which case it should not be removed from the job array. - Add a call to
shellac_update_all()at the end of themain()interactive loop so that at the end of each iteration, background jobs are checked for completion. This allows reporting on the completion every time another command is used or when just pressing "enter" to get another prompt. - Check that the changes in the Shellac main loop have taken effect and then try the unit tests.
6 Grading Criteria grading 100
| Weight | Criteria |
|---|---|
| AUTOMATED TESTS: 1 point per test | |
| 65 | TOTAL |
| 10 | Problem 1: make test-prob1 runs tests from test_prob1.org for correctness |
| 27 | Problem 2: make test-prob2 runs tests from test_prob2.org for correctness |
| 28 | Problem 3: make test-prob3 runs tests from test_prob3.org for correctness |
| MANUAL INSPECTION | |
| 35 | TOTAL |
| 10 | Manual Inspection of shellac_main.c |
Clear checks for the --echo option on the command line for Shellac |
|
Use of the shellac_t struct to track jobs |
|
| Clear main loop to read input and execute commands | |
Use of fgets() function to read input lines |
|
Checks for end of input based on the return value from fgets() |
|
Use of tokenize_string() to break up input lines into tokens |
|
Clear set of cases looking for built-in commands like help and exit |
|
Case for tokens built-in with code to print tokens |
|
Cases for the jobs, wait, pause commands with appropriate function calls |
|
| Case for non-builtin which starts jobs and updates it immediately | |
Checks on background jobs at the end of each loop iteration via shellac_update_all() |
|
| Functions adhere to CMSC 216 C Coding Style Guide: overall sane indentation, reasonable variable naming, | |
| presence of some commentary to guide readers | |
| 15 | Manual Inspection of shellac_job.c |
| Problem 2: All functions present with attempted implementations. | |
Problem 3: Functionality for input/output redirection present in job_new() / job_start() |
|
Problem 3: Functionality for background present in job_new() / job_update() |
|
| Functions adhere to CMSC 216 C Coding Style Guide: overall sane indentation, reasonable variable naming, | |
| presence of some commentary to guide readers | |
| 10 | Manual Inspection of shellac_control.c |
| Problem 2: All functions present with attempted implementations. | |
| Problem 3: Functionality for background present via support for adding/removing elements to jobs array | |
| Functions adhere to CMSC 216 C Coding Style Guide: overall sane indentation, reasonable variable naming, | |
| presence of some commentary to guide readers |
7 MAKEUP CREDIT
A MAKEUP CREDIT problem may be added which will require additional functionality to be implemented. Grading criteria and tests for Makeup Credit will be separate from those that appear for the above problems.
8 Assignment Submission
8.1 Submit to Gradescope
Refer to the Project 1 instructions and adapt them for details of how to submit to Gradescope. In summary they are
- Command Line Submission
- Type
make submitto create a zip file and upload it to Gradescope; enter your login information when prompted. - Manual Submission
- Type
make zipto createpX-complete.zip, transfer this file to a local device, then upload it to the appropriate assignment on Gradescope via the sites web interface.
8.2 Late Policies
You may wish to review the policy on late project submission which will cost 1 Engagement Point per day late. No projects will be accepted more than 48 hours after the deadline.
https://www.cs.umd.edu/~profk/216/syllabus.html#late-submission