Created
October 19, 2018 19:53
-
-
Save jacobmischka/eca1a13cf2d3c2eedb4c3e8ea080dd2a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::env; | |
use std::io::{self, Write}; | |
use std::process::{self, Command}; | |
fn main() -> io::Result<()> { | |
let mut shell = Shell::new(); | |
shell.spawn() | |
} | |
struct Shell { | |
input_history: Vec<String>, | |
last_exit_status: Option<ShellExitStatus> | |
} | |
#[derive(Debug)] | |
enum ShellCommand { | |
Command(String), | |
OrCommand(String), | |
AndCommand(String) | |
} | |
impl Shell { | |
fn new() -> Shell { | |
Shell{ | |
input_history: Vec::new(), | |
last_exit_status: None | |
} | |
} | |
fn spawn(&mut self) -> io::Result<()> { | |
loop { | |
let input = self.read_line()?; | |
if input.is_empty() { | |
return Ok(()); | |
} | |
if input.trim().is_empty() { | |
continue; | |
} | |
self.input_history.push(input.clone()); | |
let commands = self.get_commands(&input)?; | |
for command in commands { | |
let result = match command { | |
ShellCommand::Command(input) => { | |
self.exec(&input) | |
} | |
ShellCommand::OrCommand(input) => { | |
if let Some(ref last_status) = self.last_exit_status { | |
if !last_status.success() { | |
self.exec(&input) | |
} else { | |
continue; | |
} | |
} else { | |
continue; | |
} | |
} | |
ShellCommand::AndCommand(input) => { | |
if let Some(ref last_status) = self.last_exit_status { | |
if last_status.success() { | |
self.exec(&input) | |
} else { | |
continue; | |
} | |
} else { | |
continue; | |
} | |
} | |
}; | |
match result { | |
Ok(output) => { | |
io::stdout().write(&output.stdout)?; | |
io::stderr().write(&output.stderr)?; | |
self.last_exit_status = Some(output.status); | |
} | |
Err(x) => { | |
eprintln!("Err! {:?}", x); | |
} | |
} | |
} | |
} | |
} | |
fn get_commands(&self, input: &str) -> io::Result<Vec<ShellCommand>> { | |
let mut commands: Vec<ShellCommand> = Vec::new(); | |
let patterns = [";", "||", "&&"]; | |
let mut matches: Vec<_> = patterns.into_iter().flat_map(|pattern| { | |
input.match_indices(pattern) | |
}).collect(); | |
matches.push((0 as usize, ";")); | |
matches.sort_unstable_by(|a, b| { | |
a.0.cmp(&b.0) | |
}); | |
matches.push((input.len() as usize, ";")); | |
for i in 0..(matches.len() - 1) { | |
let start = matches[i].0; | |
let end = matches[i + 1].0; | |
let subcommand = &input[start..end].replace(matches[i].1, ""); | |
let subcommand = subcommand.trim(); | |
commands.push(match matches[i].1 { | |
";" => ShellCommand::Command(subcommand.to_string()), | |
"||" => ShellCommand::OrCommand(subcommand.to_string()), | |
"&&" => ShellCommand::AndCommand(subcommand.to_string()), | |
_ => panic!("Unrecognized separator") | |
}); | |
}; | |
Ok(commands) | |
} | |
fn read_line(&self) -> io::Result<String> { | |
print!("$ "); | |
io::stdout().flush()?; | |
let mut input = String::new(); | |
io::stdin().read_line(&mut input)?; | |
Ok(input) | |
} | |
fn exec(&self, input: &str) -> io::Result<ShellOutput> { | |
let mut pieces = input.split_whitespace(); | |
let cmd = pieces.next().unwrap(); | |
let args: Vec<&str> = pieces.collect(); | |
match cmd { | |
"cd" => { | |
match env::set_current_dir(args[0]) { | |
Ok(_) => { | |
Ok(ShellOutput { | |
status: ShellExitStatus { code: Some(0) }, | |
stdout: Vec::new(), | |
stderr: Vec::new() | |
}) | |
} | |
Err(x) => { | |
Err(x) | |
} | |
} | |
} | |
"exit" => { | |
process::exit(0); | |
} | |
_ => { | |
match Command::new(&cmd).args(&args).output() { | |
Ok(output) => Ok(ShellOutput::from(output)), | |
Err(x) => Err(x) | |
} | |
} | |
} | |
} | |
} | |
struct ShellOutput { | |
status: ShellExitStatus, | |
stdout: Vec<u8>, | |
stderr: Vec<u8> | |
} | |
impl From<process::Output> for ShellOutput { | |
fn from(output: process::Output) -> Self { | |
ShellOutput { | |
status: ShellExitStatus { | |
code: output.status.code() | |
}, | |
stdout: output.stdout, | |
stderr: output.stderr | |
} | |
} | |
} | |
struct ShellExitStatus { | |
code: Option<i32> | |
} | |
impl ShellExitStatus { | |
fn success(&self) -> bool { | |
self.code == Some(0) | |
} | |
} | |
#[cfg(test)] | |
mod tests { | |
use super::*; | |
#[test] | |
fn echo_works() { | |
let output = Shell::new().exec("echo hey").unwrap(); | |
assert_eq!(&String::from_utf8_lossy(&output.stdout), "hey\n"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment