use std::fmt;
#[derive(Debug)]
pub struct Redirect {
pub fd: u8,
pub path: String,
}
#[derive(Debug)]
pub struct SimpleCommand {
pub argv: Vec<String>,
pub redirects: Vec<Redirect>,
}
#[derive(Debug)]
pub struct Pipeline {
pub commands: Vec<SimpleCommand>,
}
#[derive(Debug)]
pub struct ParseError(pub String);
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for ParseError {}
fn tokenize(line: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current = String::new();
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if c.is_whitespace() {
if !current.is_empty() {
tokens.push(std::mem::take(&mut current));
}
continue;
}
if c == '2' && chars.peek() == Some(&'>') {
chars.next();
if !current.is_empty() {
tokens.push(std::mem::take(&mut current));
}
tokens.push("2>".to_string());
continue;
}
if c == '>' || c == '<' || c == '|' {
if !current.is_empty() {
tokens.push(std::mem::take(&mut current));
}
tokens.push(c.to_string());
continue;
}
current.push(c);
}
if !current.is_empty() {
tokens.push(current);
}
tokens
}
fn split_by_pipe(tokens: Vec<String>) -> Vec<Vec<String>> {
let mut commands = Vec::new();
let mut current = Vec::new();
for t in tokens {
if t == "|" {
if !current.is_empty() {
commands.push(std::mem::take(&mut current));
}
} else {
current.push(t);
}
}
if !current.is_empty() {
commands.push(current);
}
if commands.is_empty() {
commands.push(Vec::new());
}
commands
}
fn parse_simple_command(tokens: &[String]) -> Result<SimpleCommand, ParseError> {
let mut argv = Vec::new();
let mut redirects = Vec::new();
let mut i = 0;
while i < tokens.len() {
let t = &tokens[i];
if t == ">" {
i += 1;
let path = tokens
.get(i)
.ok_or_else(|| ParseError("redirect '>' missing path".to_string()))?;
redirects.push(Redirect {
fd: 1,
path: path.clone(),
});
} else if t == "2>" {
i += 1;
let path = tokens
.get(i)
.ok_or_else(|| ParseError("redirect '2>' missing path".to_string()))?;
redirects.push(Redirect {
fd: 2,
path: path.clone(),
});
} else if t == "<" {
i += 1;
let path = tokens
.get(i)
.ok_or_else(|| ParseError("redirect '<' missing path".to_string()))?;
redirects.push(Redirect {
fd: 0,
path: path.clone(),
});
} else {
argv.push(t.clone());
}
i += 1;
}
Ok(SimpleCommand { argv, redirects })
}
pub fn parse_line(line: &str) -> Result<Pipeline, ParseError> {
let tokens = tokenize(line.trim());
let command_tokens_list = split_by_pipe(tokens);
let mut commands = Vec::new();
for ct in command_tokens_list {
commands.push(parse_simple_command(&ct)?);
}
Ok(Pipeline { commands })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_pwd() {
let p = parse_line("pwd").unwrap();
assert_eq!(p.commands.len(), 1);
assert_eq!(p.commands[0].argv, vec!["pwd"]);
}
#[test]
fn parse_exit_quit() {
parse_line("exit").unwrap();
parse_line("quit").unwrap();
}
#[test]
fn parse_redirect_stdout() {
let p = parse_line("echo hello > out").unwrap();
assert_eq!(p.commands[0].argv, vec!["echo", "hello"]);
assert_eq!(p.commands[0].redirects.len(), 1);
assert_eq!(p.commands[0].redirects[0].fd, 1);
assert_eq!(p.commands[0].redirects[0].path, "out");
}
#[test]
fn parse_redirect_stderr() {
let p = parse_line("cmd 2> err").unwrap();
assert_eq!(p.commands[0].redirects[0].fd, 2);
assert_eq!(p.commands[0].redirects[0].path, "err");
}
#[test]
fn parse_redirect_2_after_token() {
let p = parse_line("a2> out").unwrap();
assert_eq!(p.commands[0].argv, vec!["a"]);
assert_eq!(p.commands[0].redirects[0].fd, 2);
assert_eq!(p.commands[0].redirects[0].path, "out");
}
#[test]
fn parse_redirect_gt_after_token() {
let p = parse_line("a> out").unwrap();
assert_eq!(p.commands[0].argv, vec!["a"]);
assert_eq!(p.commands[0].redirects[0].fd, 1);
assert_eq!(p.commands[0].redirects[0].path, "out");
}
#[test]
fn parse_redirect_stdin() {
let p = parse_line("cat < in").unwrap();
assert_eq!(p.commands[0].redirects[0].fd, 0);
assert_eq!(p.commands[0].redirects[0].path, "in");
}
#[test]
fn parse_redirect_missing_path_err() {
assert!(parse_line("echo >").is_err());
assert!(parse_line("echo 2>").is_err());
assert!(parse_line("cat <").is_err());
}
#[test]
fn parse_error_display() {
let e = parse_line("echo >").unwrap_err();
assert!(e.to_string().contains("redirect") || e.0.contains("path"));
}
#[test]
fn parse_pipeline() {
let p = parse_line("a | b").unwrap();
assert_eq!(p.commands.len(), 2);
assert_eq!(p.commands[0].argv, vec!["a"]);
assert_eq!(p.commands[1].argv, vec!["b"]);
}
#[test]
fn parse_pipe_only_empty_command() {
let p = parse_line("|").unwrap();
assert_eq!(p.commands.len(), 1);
assert!(p.commands[0].argv.is_empty());
}
#[test]
fn parse_stdout_redirect_token() {
let p = parse_line("echo x > out").unwrap();
assert_eq!(p.commands[0].redirects[0].fd, 1);
assert_eq!(p.commands[0].redirects[0].path, "out");
}
}