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 InputToken
s on Pipe
tokens and constructing a
vector of SimpleCommand
s 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 InputToken
s 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 Stdio
s.
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).
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.