Introduction

The goal of this assignment is to become familiar with low-level Unix/POSIX system calls related to processes, file access, and interprocess communication (pipes and redirection). You will write a basic shell which supports a few basic operations to understand the basics of how th e bash shell terminal works.

Takeaways

After completing this homework assignment, you should:
  • Understand process execution, pipes, and forks
  • Have an advanced understanding of Unix commands and the command line
  • Have gained experience with C libraries and system calls
  • Have enbanced your C programming abilities

Hints and Tips

  • Refer to the Lecture Slide examples in Module2 Part2 Slides ECF Signals.pdf for specific examples of how a signal handler is used. Refer to Lecture Slide examples in Module2 Part3. Slides System IO.pdf for examples and explanations of redirection and pipes.
  • Check the return codes of all system calls. This will help you catch errors. Examples of how to write error handling wrapper functions are in the lecture notes. Utilizing this type of coding practice will help you to catch errors as you program. However, your shell should not exit on error as shown in the examples.
  • Your shell should NEVER crash. We will be deducting points for cach time your program crashes during grading. Make sure that your code handles invalid input gracefully.
  • All grading for this assignment will be performed via Gradescope. This means that all of your error messages must use the defined ERROR strings in icssh. h. All output will expect to be in a very specific format. Any extra newlines, spaces, or debugging messages may cause test cases to fail. Use the -DDEBUG flag discussed in the extended debugging document (Resources section of Module 1) to avoid printing your debugging output all the time.
  • To debug using gdb, you will need to instruct the debugger on which process to follow, parent or child, upon fork. Information about the gdb settings can be found here.
  • The readline library, which is used to add "quality of life" features for the user typing in commands, does not free all of its internal allocated data structures upon exit of the program. We have created a suppression file to ignore these error when valgrind is run. Use the following valgrind flags:
--memcheck: leak- check=full -- show-reachable=yes --suppressions=rsrc/ icssh. supp

Getting Started

Download the base code for this assignment: hw3。tar. The structure is:

For this assignment, we have created the following base files for your use

  • Makefile - A file which defines how to cormpile and link a program for the command-line utility make. The makefile targets are all, debug, and clean. This will build the executable bin/53shell 
  • include/ - Header files for each part of the assignment. All headers include header guards. Do not modify any of the provided header fles. Declare any of your own helper functions in helpers .h.
  • 1ib/ - Pre-compiled libraries for linking with your programs. Source code is not provided for these functions. Descriptions of these functions are provided in the associated header files.
  • src/ - Your shell implementations MUST go in these fles. Place any helper functions you want to create in helpers.c

All code and implementation must be placed in the icssh.c and helpers.h/.c files. Do not create any additional C files and do not modify icssh. h. These files will be replaced during grading.

Restrictions & Important Information

The provided base code will compile and run a simple shell program. It demonstrates the basic concept of a shell program. However, it lacks some important features:

  • There is a limit of 32 distinct arguments per program (argc <= 32). Also, each argument string can be at most 63 characters in length.
  • Any printed error messages or print statements in this document MUST BE exactly as stated - USE THE MACROS provided in icssh.h. We will be grep-ing (man_ grep) your output for these strings during grading. If the strings are not found, you will not be awarded points.
  • Executables may be stored anywhere on your computer. For example, the program cat may be located in /bin/cat while grep may be located in /usr/bin/grep. You never have to specifically type /bin/cat though. The reason for this is that the execvp() syscall your shell will automatically search common paths for programs. As such your shell can only run programs which are located within one of the directories in the list specified by the PATH environment variable or the current directory (where your shell program was run from). 
  • In general, you may use almost any standard library function or system call to complete this homework. There is one important exception: you may not use the system( 3) function. This function is efectively a wrapper for a shell and using it would defeat the purpose of the whole assignment. Speaking more broadly, any library function or system call that is effetively a wrapper for another shell is prohibited in this assignment. Using any of these functions will result in an automatic zero for the assignment.

icssh. c file provides a basic shell which supports the following basic functionalities:

  • Installs a SIGSEGV signal handler.
  • Prints a command prompt and waits for user input: <53she11>$
  • Reads in the user command string (job commands) from the user via STDIN (User must press enter)
  • Tokenize and parse the entered job command into a linked list data structure, job_ info, of separate process commands
    • Only valid job commands will be returmed. All invalid commands will print an error message and returm NULL.
    •  The provided tokenizer will treat all characters between double-quotes as a single token.
  • For each valid command entered, the function debug_ print_ job will print the values of the returmed linked list data structure.
  • If the command is a built-in, execute the built in functionality.
    • The shell will terminate if the user types the built- in command, exit. ) If itis not a built-in command, fork a child process and attempt to run the entered command using execvp(). The parent process waits for termination of the child process before accepting another command.
  • Repeatedly take commands from the user, until the user enters exit.

Before you begin your implementation, examine the provided basecode in icssh.c. Compile (using the makefile) the program and play with the basic implementation to understand how it works.

Try entering the following basic commands and your own variants. Note how each of the commands is stored into the job_ _info and proc_ _info structs which are defined in include/icssh.h. Each valid command will be parsed into a job_ info struct. The job_ _info struct contains metadata for the overall command and a pointer to a linked list of proc_ _info struct(s) with the information about each executable sub command of the job.

  • ls
  • 1s -a
  • more rsrc/test . txt
  • echo "hello world!"
  • cat < rsrc/test. txt
  • echo < rsrc/test.txt > rsrc/out.txt &
  • echo 2> rsr/err.txt < rsrc/test.txt > rsrc/out.txt
  • echo "hello world"
  • grep he11o &
  • cat rsrc/test. txt
  • grep There| WC -1 > rsrc/out2. txt
  • echo "hello\nhello\nhello\n" 2> err.txt| grep There 2> err2.txt| wc -1 >
  • rsrc/out2.txt
The provided base code will not execute all of the above commands correctly. It will only execute the first job command listed before the first pipe symbol (|). It also only executes jobs as foreground processes. Support for the remaining operations is the assignment.

Invalid commands, such as some of the following, will not be parsed into the job_ info data structure. And the provided base code will print an error message and the function validate_ input will return a NULL pointer.

  • a.out -a .
  • echo < rsrc/test.txt < a.txt
  • cat rsrc/test. txt
  • grep There > b.txt
  • WC -1 > rsrc/out2.txt
  • cat rsrc/test. txt
  • grep There < b.txt
  • WC -1

Shell Implementation Requirements

In this homework, the goal is to add features to the base shell program to make it more useful and practical. Below are the requirements:
  • Support for built-in shell commands
  • Support for a foreground and background processes simultaneously
  • Support for I/O redirection (STDIN, STDOUT, STDERR) and piping between two programs
Most parts of the assignment are independent of each other. Each part or sub part adds a functionality to the shell. They can be implemented in any order.

Part 1: Built-in Shell Commands

A built- in command is executed directly in the shell itself (parent), rather than by the execution of an extemal program which is loaded and run by a child. The command exit which is implemented in the provided basecode is an example. If the first argument of any job command is a built-in, the built-in is run, ignoring any additional arguments.

1. Add the ability to change directories with the built-in cd command. See man_ chdir and this example. Change the current working directory to the specified directory (S cd dir). If directory is not supplied, the value of the HOME environment variable is used (S cd). Any additional arguments following the directory are ignored. Investigate how chdir works with paths which include ' .' and

If the directory change is successful, the absolute pathname (see man getcwd) of the new working directory is written to STDOUT. If the directory change is unsuccessful, print to STDERR the define statement DIR_ ERR, which is defined as " DIRECTORY ERROR: Directory does not exist. \n"

2. The command (<53she11>$ estatus) prints the exit status of the most recently reaped program (aka child process). 

An example:


We will add more built-in commands in the later sections.

Part 2: Background Jobs

A background job executes without blocking the shell from taking additional job commands. This means the command is run and the shell does not wait for it to finish before retuming to the user with a new prompt. This is accomplished by spawning a child process to execute the command/program and not waiting for the child to terminate. The standard operator to specify the command to be run as a background process is &. Only when this operator is added to the job command as the very last token will the shell recognize it as an indicator for background processes. The job_ _info struct returmed by the tokenizer will set the bg field to 1 (yes) if the job command is a background job. An example:

For this assignment you will support any number of background jobs at one time. The set of current running background processes must be stored by your implementation. For each job, maintain a bgentry_ t structure entry (specifed in hw3/ include/ icssh.h). This struct contains a pointer to the job (job_ info struct), the pid of the (first - see piping section) child process, and the time the command was entered in the shell (man. time).

You may use any data structure you wish. We suggest a linked list implementation. You can create your own or use the implementation we have provided in basecode. The data structure should maintain the list of background processes in sorted order based on the time the command was executed (bgentry_ _t seconds field).


When a background process terminates, the SIGCHLD signal will occur. Implement and install a SIGCHLD handler to catch the signal and set a conditional flag denoting that a child has been terminated. In the parent process, after receiving a new command via the prompt, check if the conditional flag is set. If set, then reap all terminated background child processes (1 at a time). Remove each process from the data structure and print the defined BG_ _TERM message (in hw3/include/icssh.h) to STDOUT. This string is defined as, "Background process %d: %s, has terminated. \n" where the %d is the pid of the program and %s is the command run. Once all terminated background child processes have been reaped, set the flag back to 0.

You will need to consider how to handle when a foreground and background process(es) are both running. SIGCHLD could occur for any process and your shell should act accordingly to reap/clean up either/both processes. Remember, only print the BG_ TERM message for background processes.

Add a new built-in command (<53she1l>$ bglist) which prints the list of background jobs currently running from oldest to newest. A print function for each background job entry is provided, print_ bgentry (see hw3/include/icssh. h for description). This function prints to STDERR.

For example, if there were three background processes running, bglist will print in the following format.

Part 3: Handling Signals

Review section 8.5 in your textbook before atempting this section of the assignment. Addionally, refer to the Lecture notes on signals.
Implement the fllowing:

1. A SIGCHLD handler (as described in the background processes section)

2. Modify the exccution of the built-in exit command in the provided basecode to kill all running background jobs before termination of the shell (see man kill). Print BG_ TERM for each of the killed processes. Reap all child and parent resources before exiting the parent.

Part 4: Redirection

The shell must support the following Unix I/O redirection options: >, 2>,and <. Consult with this webpage and/or here to review IO redirection usage. We summarize briefly here.

One of the most powerful features of a Unix shell is the ability to compose a series of simple applications to create a complex workflow. The feature that enables this composition is output redirection. Basic redirection is accomplished by three special characters: <, >, and| (explained in the next section).

The < operator indicates that you are to take standard input from the specifed file:


The input . txt file must exist and be openable. If not, an error occurs.
The》operator indicates that you are to write standard output to the specified fle:

The output . txt file is created or overvitten if it exists.

Next we have the 2> operator. While the > operator redirects standard output to a file, 2> will redirect the output of STDERR to a file. For instance, in the following example, if the executable printToStderr prints text to standard error, then it will be redirected to the file output . txt.


The output . txt file is created or overwritten if it exists.

For all of the examples above, you can run them in your VM bash terminal to observe the expected functionality.

In order to implement this functionality in your shell, you will need to do the following items (stated in no particular order):

  • open/create the required input or output files
  • fork() a child process and execvp() the command, and
  • use dup( ) or dup2() (see man dup) to make copies and set the proper file descriptors
It is important to determine which process (the parent or child) should do each of these iterms, the order in which to do them, and which file descriptors and when/who should be closed.

Error cases with redirection are still possible despite the job_ info structure. For example, the same file cannot be shared between any two types of redirection (eg.< f.txt > f.txt is INVALID). Upon INVALID combinations, print to STDERR the RD_ ERR which is the defined macro string: "REDIRECTION ERROR:
Invalid file combination.\n" in hw3/ include/icssh.h

Part 5: Piping
Your shell must also support piping. For this assignment, you will support one or two pipe operators in a single command (more for extra credit - see the end of the document). For simplicity, your program does not need to support a pipe along with any redirection operators in a single command. If you are new to the concept of piping start with this link.

The pipe operator, l, connects the standard output of the first program to the standard input of the next program. For example,

the echo program will print the string to standard out which is then fed to the grep program on standard in. For example,

this example does the same as the previous example but passes the STDOUT of the grep command to STDIN of the WC program (see man WC).

To accomplish this you will need to use pipe()/ pipe2() (see man. pipe ). The basic steps, using the first example above, are as follows:

  • create a single pipe to connect echo to grep
  • fork() two times, one for each command/program (echo and grep)
  • the child for echo must connect its STDOUT file descriptor to the write end of the pipe
  • the child for grep must connect its STDIN file descriptor to the read end of the pipe
  • the each child runs their command

Extra Credit

For this assignment we have included Extra Credit options. If you complete one or more of these options, you MUST indicate that you have done so via the HW#3 Canvas Survey. If it is not indicated on the Survey, it WILL NOT be graded.
  • (2 pts) Add support to change directory to the previous directory, cd - (See Bash for example of how this operates)
  • (2 pts) Create a new builtin command, asci53, which prints to the terminal a creative UCI or ICS53 ASCII art logo to STDOUT.
  • (5 pts) Support mutiple pipe operators (2+) in a single command.
  • (5 pts) Support 2+ pipe operators with any valid combination of redirection operators.
Valid example:

Note that any combination of redirection that OVERLAPS with the pipe operations is considered an RD_ ERR error. You will need to check for these combinations within the job_ _info structure.

For example, in the first case shown below rsrc/text . txt cannot be used as STDIN to a.out and for STDOUT to grep. In the second case, again the same file cannot be used for multiple redirections.

  • (5 pts) Implement the built-in command, fg. This command is used to bring background jobs to the foreground. The fg command has the following usages:
    • <53she11>$ fg this brings the most recently started background job to the foreground.
    • <53she1l>$ fg pid where pid is the pid of the background process to bring to the    foreground. If the provided pid does not exist, print to STDERR the PID_ ERR string (found in icssh.h)
    • If in either situation there is no background process, then print PID_ ERR.
    • More information about this command can be found (here).