Skip to content

Instantly share code, notes, and snippets.

@jacobmischka
Created October 19, 2018 19:53
Show Gist options
  • Save jacobmischka/eca1a13cf2d3c2eedb4c3e8ea080dd2a to your computer and use it in GitHub Desktop.
Save jacobmischka/eca1a13cf2d3c2eedb4c3e8ea080dd2a to your computer and use it in GitHub Desktop.
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