Last Updated: 2025-11-17 Mon 16:52

CMSC216 Project 4: A Shell Called Shellac

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 an exec()-family function fails, the child process should immediately exit with the exit code JOBCOND_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 the jobs[] array.
  • Documentation strings for some shellac_control.c functions have had the incorrect mention of argv[] corrected to jobs[] which is the proper field to deal with in those functions.
  • A few mentions of incorrect function names have been corrected.
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.

  1. An indefinite (likely while()) loop
  2. At the top of each loop, print the (shellac) prompt
  3. Read a line of text using the fgets() function (see notes on this later) and possibly print it if echoing is enabled
  4. Tokenize the string using the provided tokenize_string() function
  5. Analyze the 0th token for one of the built-in commands like help or exit and act accordingly
  6. If the token is not a builtin command, start a child process to execute the job (completed in Problem 2)
  7. The exit command 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() returns NULL if there is no more input which is an alternative way to cause Shellac to exit (no more input to read)
  • If fgets() doesn't return NULL it 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

  1. Complete most of the functions in shellac_job.c though 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.
  2. 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.
  3. Finally modify shellac_main.c to 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

  1. The job_t is tracks information on a running child process. It's most important parts are the argv[] field which is used to name the job and eventually passed to exec() as well as its pid which is assigned when the job is started.
  2. When creating a job via job_new(), the parameter argv[] should have all strings duplicated via strdup() calls. Later these duplicated strings are de-allocatd in job_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.
  3. Notice that during printing in job_print(), the field argv[] is printed up to the argc index 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 an execvp() call that needs the array to be NULL terminated and the printing helps verify that this is set up correctly in the struct.

  4. 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.
  5. job_start() amounts to fork()'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 to execvp() to become a command that is described in jobname and argv[].
  6. In job_start() do some error checking for failures to fork() and exec(). Specifically, if the child fails to exec, exit with JOBCOND_FAIL_EXEC to notify Shellac of the failure.
  7. job_update_status() essentially wraps the waitpid() 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 like WIFEXITED() adjust the condition of the job to JOBCOND_EXIT for a normal exit or JOBCOND_FAIL_OTHER if the program did not exit normally. For normal exits, extract the exit status with WEXITSTATUS() and set the job's retval to this.

shellac_control.c

  1. Most of the functions in shellac_control.c work on the shellac_t struct, a glorified array of job_t structs.
  2. The struct maintains a jobcount of 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.
  3. The jobs[] array in a shellac_t may be sparsely populated as in

       jobcount = 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 above NULL structure 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.

  4. Perform error checking on changes to the shellac_t's jobs[] 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_t struct to shellac_main.c. On detecting a not-builtin command like ls or gcc, use functions to create a Job, add it to the shellac_t and 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

  1. Revisit job_new() to search argv[] for the symbols < and > followed by a file. The > or < and the file following it should be removed from the argv[] array for the command and have the output_file or input_file set 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 argc is only 1: both > and output.txt have been removed from argv[]. This removal can be done either during the copying loop from the parameter argv[] to the struct field argrv[] or afterwards. Careful to free any memory associated with extraneous strings like > which won't have pointers to them anymore.

  2. Revisit job_start() and add cases after fork() 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. Use dup2() to overwrite either Standard In/Out (or both) with the opened file.
  3. When opening for writing, make sure to use options that will accomplish these there items

    • O_WRONLY: for writing
    • O_CREAT: which will create a file if it is not present
    • O_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 to S_IRUSR|S_IWUSR to enable further reading/writing of the created file.

  4. Do some basic error checking: if a file can't be opened, detect it and exit the child process with either JOBCOND_FAIL_INPT or JOBCOND_FAIL_OUTP to indicate a problem with the redirection.
  5. Check that the changes in the Shellac main loop have taken effect and then try the unit tests.

5.4 Background Jobs

  1. Revisit job_new() to check for the & background symbol. When detected, set the is_background field to 1.
  2. Revisit job_update_status() to respect the is_background field

    • is_background==0 should block the parent on a waitpid() until the child finishes
    • is_background==1 should return immediately to the parent on waitpid() if the child is not yet finished. This can be accomplished with the WNOHANG option to waitpid().

    Return a 0 when a child job is not complete yet.

  3. Complete any missing functionality in shellac_control.c for functions like shellac_add() so that multiple jobs can be added. As well, complete the shellac_update_all() function which waits for all jobs in the shellac struct. Use repeated calls to shellac_update_one() for this. Note that now job_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.
  4. Add a call to shellac_update_all() at the end of the main() 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.
  5. 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 submit to create a zip file and upload it to Gradescope; enter your login information when prompted.
Manual Submission
Type make zip to create pX-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


Web Accessibility
Author: Chris Kauffman (profk@umd.edu)
Date: 2025-11-17 Mon 16:52