Part 2. Processing redirections (25 points)

Now that you have a split_tokens() function that can parse redirections, it’s time to actually implement them! Fortunately, this is much easier than parsing the input.

Although your implementation from the previous part supported file descriptors 0-9, for this part, you only need to support file descriptors 0, 1, and 2.

In the last lab, implementing the pipeline consisted of two parts, (1) splitting the slice of InputTokens on Pipe tokens and constructing a vector of SimpleCommands in Pipeline::new(); and (2) spawning the processes in Pipeline::run().

Your task

You will need to update the constructor to handle the new InputToken variants and update the .run() method to perform redirections.

First, update SimpleCommand to hold a vector of redirections. It’s up to you how you want to model a redirection. I used a new enum with two variants, one for input and one for output. Both variants have fields for the file descriptor number and the path. The output variant also has a bool field which is true when appending.

What you should not do is have a vector of InputTokens for the redirections. It wouldn’t make sense to have a redirection of, InputToken::Word or InputToken::Pipe after all. Make some new type.

In the Pipeline::new() constructor, insert the appropriate redirections into each SimpleCommand’s redirections.

In the .run() method, to process the redirections, for each command, you need to construct a Stdio object for stdin, stdout, and stderr. You did part of this before when hooking up stdout of one command to stdin of the next command.

Initially, the Stdio for stdin should be the last stdout, the Stdio for stdout should be either Stdio::piped() or Stdio::inherit() and the Stdio for stderr should be Stdio::inherit(). This is what you had from the last lab but with an explicit Stdio for each of stdin/stdout/stderr.

Then, for each redirection in the SimpleCommand, you want to open/create a file. The File object can be converted to a Stdio using .into(). This can be assigned to variables for stdin/stdout/stderr using code like

match num {
    0 => stdin = file.into(),
    1 => stdout = file.into(),
    2 => stderr = file.into(),
    _ => return Err(format!("Redirecting {num} not supported").into()),
}

Finally, when creating the Command builder, you want to call .stdin(), .stdout(), and .stderr() and pass the three Stdios.

Hint

Remember that we can open a file for input using

let file = File::open(path).map_err(|err| format!("{path}: {err}"))?;

For output redirections, we need to create a file if it doesn’t exist and truncate it if it does. For append redirections, we need to create the file if it doesn’t exist and open it in append mode.

In append mode, all writes to the file are appended atomically. This means if multiple processes are trying to append to the file at the same time, you won’t get some bytes from one process and some bytes from the other process intermixed. A complete line of output will be written at a time.

To open files with different options, we need to use the OpenOptions builder. Read the documentation for OpenOptions. Pay particular attention to the methods .write(), .truncate(), and .append(). You can handle output and append redirections in a similar manner by passing different values to .truncate() and .append().

You can see an example using OpenOptions at the end of the slides for Lecture 19 (System Calls II).

Here's some sample output:
Welcome to the Oberlin Shell!
$echo "1 2 3" >foo
$<foo cat
1 2 3
$echo "4 5 6" >>foo
$<foo cat
1 2 3
4 5 6
$echo "7 8 9" >foo | <foo cat
7 8 9

When you exit the shell, if you use ls to print the contents of your directory, you should see a new file named foo. This file should contain the text “7 8 9”.

Finally, you will need to update your unit tests to work with the new SimpleCommand format.