Part 2. Building a pipeline (25 points)

Now that your split_input() function takes a string and returns a vector of InputTokens, the second step is to build a pipeline. To do this, you’re going to create a SimpleCommand struct and a Pipeline struct.

The Pipeline struct will contain a vector of SimpleCommands to run.

At a high level, the Pipeline constructor,

#![allow(unused)]
fn main() {
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
enum InputToken { Word(String), Pipe }
struct Pipeline;
impl Pipeline {
    pub fn new(tokens: &[InputToken]) -> Result<Self> { todo!() }
}
}

takes a slice of InputTokens and constructs a Pipeline by splitting the tokens on InputToken::Pipe. Then, for each sequence of tokens between the InputToken::Pipe, new() will construct a SimpleCommand.

Example

For example, the command cat Cargo.toml | wc -l is split into the following tokens.

[Word("cat"), Word("Cargo.toml"), Pipe, Word("wc"), Word("-l")]

When these tokens are passed to Pipeline::new(tokens), it constructs the following Pipeline.

Pipeline {
    commands: [
        SimpleCommand {
            path: "cat",
            args: [
                "Cargo.toml",
            ],
        },
        SimpleCommand {
            path: "wc",
            args: [
                "-l",
            ],
        },
    ],
}

Your task

Create a new pipeline module by creating a pipeline.rs file and adding the appropriate mod line to main.rs.

Create a SimpleCommand struct that holds a path (or command name) of the program to execute and a vector of String arguments. (In the next lab, we’ll expand this struct.)

Next, create a public Pipeline struct containing a vector of SimpleCommands.

Implement the Pipeline::new() constructor shown above.

Tip

We can use the .split() method on a slice to split it on InputToken::Pipe. The result is an iterator over sub-slices.

for command_tokens in tokens.split(|token| token == &InputToken::Pipe) {
    let mut words: Vec<String> = Vec::new();

    for token in command_tokens {
        todo!("Push each word into words");
    }
    if words.is_empty() {
        return Err("Command missing".into());
    }
    todo!("Create a new SimpleCommand from the words");
}

words.remove(0) will remove and return the first word from words. You can use this to create the SimpleCommand.

Update your main() function to print the debug representation of the Pipeline that results from the input tokens you parsed in the previous part.

Create some unit tests to test the construction of a Pipeline. You should write at least two additional tests, beyond what is provided here. Here’s one to get you started using the example above.

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn simple_pipeline() {
        let tokens = [
            InputToken::Word("cat".to_string()),
            InputToken::Word("Cargo.toml".to_string()),
            InputToken::Pipe,
            InputToken::Word("wc".to_string()),
            InputToken::Word("-l".to_string()),
        ];
        let pipeline = Pipeline::new(&tokens).expect("Failed to create a Pipeline");
        assert_eq!(
            pipeline,
            Pipeline {
                commands: vec![
                    SimpleCommand {
                        path: "cat".to_string(),
                        args: vec!["Cargo.toml".to_string()]
                    },
                    SimpleCommand {
                        path: "wc".to_string(),
                        args: vec!["-l".to_string()]
                    },
                ]
            }
        );
    }
}