feat(runtime): implement builtins, add help
This commit is contained in:
parent
6c759e57cc
commit
092360bce6
7 changed files with 274 additions and 78 deletions
|
@ -6,6 +6,7 @@ use oyster_runtime::{Shell, Status};
|
|||
|
||||
fn main() {
|
||||
let mut shell = Shell::new().unwrap();
|
||||
shell.builtins_mut().add_defaults();
|
||||
let mut exit_code = Status::SUCCESS;
|
||||
|
||||
loop {
|
||||
|
|
68
crates/oyster_runtime/src/builtins/mod.rs
Normal file
68
crates/oyster_runtime/src/builtins/mod.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use crate::Shell;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Builtin {
|
||||
pub name: &'static str,
|
||||
pub description: &'static str,
|
||||
pub fun: fn(shell: &mut Shell, args: &[Cow<str>]),
|
||||
}
|
||||
|
||||
pub static HELP: Builtin = {
|
||||
pub fn help(shell: &mut Shell, _args: &[Cow<str>]) {
|
||||
println!(
|
||||
r"oyster help:
|
||||
|
||||
These are the loaded builtins:"
|
||||
);
|
||||
|
||||
for builtin in shell.builtins().iter() {
|
||||
println!(" {: <8} {}", builtin.name, builtin.description);
|
||||
}
|
||||
}
|
||||
|
||||
Builtin {
|
||||
name: "help",
|
||||
description: "prints help for different builtins",
|
||||
fun: help,
|
||||
}
|
||||
};
|
||||
|
||||
/// Used to register and retrieve builtins.
|
||||
#[derive(Default)]
|
||||
pub struct BuiltinMap(HashMap<&'static str, Builtin>);
|
||||
|
||||
impl BuiltinMap {
|
||||
/// Register a new builtin.
|
||||
pub fn add(&mut self, builtin: Builtin) {
|
||||
self.0.insert(builtin.name, builtin);
|
||||
}
|
||||
|
||||
/// Get a builtin with a given name, if it exists.
|
||||
pub fn get(&self, name: &str) -> Option<&Builtin> {
|
||||
self.0.get(name)
|
||||
}
|
||||
|
||||
/// Returns an iterator over all currently registered builtins.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Builtin> {
|
||||
self.0.iter().map(|(_, v)| v)
|
||||
}
|
||||
|
||||
/// Add default builtins.
|
||||
pub fn add_defaults(&mut self) {
|
||||
self.add(HELP);
|
||||
}
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
/// Retrieve builtins.
|
||||
pub fn builtins(&self) -> &BuiltinMap {
|
||||
&self.builtins
|
||||
}
|
||||
|
||||
/// Retrieve builtins, mutably.
|
||||
pub fn builtins_mut(&mut self) -> &mut BuiltinMap {
|
||||
&mut self.builtins
|
||||
}
|
||||
}
|
|
@ -1,13 +1,18 @@
|
|||
//! The runtime for executing oyster programs.
|
||||
//! Panics when an invalid ast gets passed.
|
||||
|
||||
pub mod builtins;
|
||||
mod pipeline;
|
||||
|
||||
use std::io;
|
||||
|
||||
use nix::sys::{
|
||||
use builtins::BuiltinMap;
|
||||
use nix::{
|
||||
errno::Errno,
|
||||
sys::{
|
||||
signal::{self, SaFlags, SigAction, SigHandler, Signal},
|
||||
signalfd::SigSet,
|
||||
},
|
||||
};
|
||||
use oyster_parser::ast;
|
||||
use thiserror::Error;
|
||||
|
@ -38,11 +43,16 @@ pub enum RuntimeError {
|
|||
#[error("failed to spawn process: {0}")]
|
||||
SpawnFailed(#[source] io::Error),
|
||||
|
||||
#[error("failed to fork: {0}")]
|
||||
ForkFailed(#[source] Errno),
|
||||
|
||||
#[error("waitpid error: {0}")]
|
||||
WaidPid(nix::Error),
|
||||
}
|
||||
|
||||
pub struct Shell;
|
||||
pub struct Shell {
|
||||
builtins: BuiltinMap,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
pub fn new() -> Result<Shell, nix::Error> {
|
||||
|
@ -53,7 +63,9 @@ impl Shell {
|
|||
signal::sigaction(Signal::SIGTTOU, &ignore)?;
|
||||
}
|
||||
|
||||
Ok(Shell)
|
||||
Ok(Shell {
|
||||
builtins: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run<'a>(&mut self, code: &'a ast::Code) -> Result<Status, RuntimeError> {
|
||||
|
|
|
@ -3,9 +3,11 @@ use std::{
|
|||
ffi::{OsStr, OsString},
|
||||
fs::File,
|
||||
io,
|
||||
os::unix::{io::FromRawFd, process::CommandExt},
|
||||
process::{Child, Command, Stdio},
|
||||
slice::Iter,
|
||||
os::unix::{
|
||||
io::{AsRawFd, FromRawFd, RawFd},
|
||||
process::CommandExt,
|
||||
},
|
||||
process::{self, Command, Stdio},
|
||||
};
|
||||
|
||||
use nix::{
|
||||
|
@ -17,16 +19,23 @@ use nix::{
|
|||
signalfd::SigSet,
|
||||
wait::{self, WaitPidFlag, WaitStatus},
|
||||
},
|
||||
unistd::{self, Pid},
|
||||
unistd::{self, ForkResult, Pid},
|
||||
};
|
||||
use oyster_parser::ast::{self, Redirect};
|
||||
|
||||
use crate::{RuntimeError, Shell, Status};
|
||||
use crate::{builtins::Builtin, RuntimeError, Shell, Status};
|
||||
|
||||
/// The specific kind of the command.
|
||||
enum CommandKind {
|
||||
External,
|
||||
Builtin(Builtin),
|
||||
}
|
||||
|
||||
/// A command that's ready to be run.
|
||||
struct PreparedCommand<'a> {
|
||||
kind: CommandKind,
|
||||
cmd: Cow<'a, str>,
|
||||
args: WordBuilder<'a>,
|
||||
args: Vec<Cow<'a, str>>,
|
||||
redirect: Redirect,
|
||||
stdin: Option<File>,
|
||||
stdout: Option<File>,
|
||||
|
@ -35,13 +44,33 @@ struct PreparedCommand<'a> {
|
|||
|
||||
impl<'a> PreparedCommand<'a> {
|
||||
/// Create a new PreparedCommand.
|
||||
fn new(command: &'a ast::Command) -> Self {
|
||||
let mut words = WordBuilder(command.0.iter());
|
||||
fn new(command: &'a ast::Command, shell: &Shell) -> Self {
|
||||
let mut words = command.0.iter().map(|word| {
|
||||
let mut words = word.0.iter();
|
||||
|
||||
let mut s = match words.next().expect("words need to have >1 parts") {
|
||||
ast::WordPart::Text(text) => Cow::from(*text),
|
||||
};
|
||||
|
||||
for part in words {
|
||||
match part {
|
||||
ast::WordPart::Text(text) => s.to_mut().push_str(text),
|
||||
}
|
||||
}
|
||||
|
||||
s
|
||||
});
|
||||
let cmd = words.next().expect("words need to have >1 parts");
|
||||
let args = words;
|
||||
let args = words.collect();
|
||||
let redirect = command.1;
|
||||
|
||||
let kind = match shell.builtins().get(&cmd) {
|
||||
Some(builtin) => CommandKind::Builtin(builtin),
|
||||
None => CommandKind::External,
|
||||
};
|
||||
|
||||
PreparedCommand {
|
||||
kind,
|
||||
cmd,
|
||||
args,
|
||||
redirect,
|
||||
|
@ -52,36 +81,47 @@ impl<'a> PreparedCommand<'a> {
|
|||
}
|
||||
|
||||
/// Run this command with the given context.
|
||||
fn spawn(self, pgid: &mut Pid) -> Result<Child, RuntimeError> {
|
||||
let args = self.args.map(|w| match w {
|
||||
fn spawn(self, shell: &mut Shell, pgid: &mut Pid) -> Result<(), RuntimeError> {
|
||||
let pre_exec = {
|
||||
let pgid = *pgid;
|
||||
move || {
|
||||
let _ = unistd::setpgid(Pid::from_raw(0), pgid);
|
||||
let _ = unistd::tcsetpgrp(libc::STDIN_FILENO, unistd::getpid());
|
||||
|
||||
let default = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
|
||||
unsafe {
|
||||
let _ = signal::sigaction(signal::Signal::SIGTSTP, &default);
|
||||
let _ = signal::sigaction(signal::Signal::SIGTTOU, &default);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
let args = self.args.iter().map(|w| match w {
|
||||
Cow::Borrowed(s) => Cow::Borrowed(OsStr::new(s)),
|
||||
Cow::Owned(s) => Cow::Owned(OsString::from(s)),
|
||||
});
|
||||
|
||||
let child = match self.kind {
|
||||
CommandKind::External => {
|
||||
let mut cmd = Command::new(self.cmd.as_ref());
|
||||
cmd.args(args);
|
||||
cmd.stdin(self.stdin.map_or(Stdio::inherit(), Stdio::from));
|
||||
cmd.stdout(self.stdout.map_or(Stdio::inherit(), Stdio::from));
|
||||
cmd.stderr(self.stderr.map_or(Stdio::inherit(), Stdio::from));
|
||||
cmd.process_group(pgid.as_raw());
|
||||
|
||||
unsafe {
|
||||
cmd.pre_exec(move || {
|
||||
let _ = unistd::tcsetpgrp(libc::STDIN_FILENO, unistd::getpid());
|
||||
|
||||
let default = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
|
||||
let _ = signal::sigaction(signal::Signal::SIGTSTP, &default);
|
||||
let _ = signal::sigaction(signal::Signal::SIGTTOU, &default);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
cmd.pre_exec(pre_exec);
|
||||
}
|
||||
|
||||
let child = match cmd.spawn() {
|
||||
Ok(child) => child,
|
||||
Err(err) => {
|
||||
return Err(match err.kind() {
|
||||
io::ErrorKind::NotFound => RuntimeError::CommandNotFound(self.cmd.into_owned()),
|
||||
io::ErrorKind::NotFound => {
|
||||
RuntimeError::CommandNotFound(self.cmd.into_owned())
|
||||
}
|
||||
io::ErrorKind::PermissionDenied => {
|
||||
RuntimeError::PermissionDenied(self.cmd.into_owned())
|
||||
}
|
||||
|
@ -90,15 +130,38 @@ impl<'a> PreparedCommand<'a> {
|
|||
}
|
||||
};
|
||||
|
||||
let child_id = Pid::from_raw(child.id() as i32);
|
||||
if *pgid == Pid::from_raw(0) {
|
||||
*pgid = child_id;
|
||||
Pid::from_raw(child.id() as i32)
|
||||
}
|
||||
CommandKind::Builtin(builtin) => {
|
||||
match unsafe { unistd::fork() }.map_err(RuntimeError::ForkFailed)? {
|
||||
ForkResult::Parent { child } => child,
|
||||
ForkResult::Child => {
|
||||
fn redirect(old: Option<File>, new: RawFd) {
|
||||
if let Some(old) = old {
|
||||
let _ = unistd::dup2(old.as_raw_fd(), new);
|
||||
}
|
||||
}
|
||||
|
||||
redirect(self.stdin, libc::STDIN_FILENO);
|
||||
redirect(self.stdout, libc::STDOUT_FILENO);
|
||||
redirect(self.stderr, libc::STDERR_FILENO);
|
||||
|
||||
let _ = pre_exec();
|
||||
(builtin.fun)(shell, &self.args);
|
||||
process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// prevent race conditions
|
||||
let _ = unistd::setpgid(child_id, *pgid);
|
||||
if *pgid == Pid::from_raw(0) {
|
||||
*pgid = child;
|
||||
}
|
||||
|
||||
Ok(child)
|
||||
// prevent race conditions
|
||||
let _ = unistd::setpgid(child, *pgid);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,10 +172,14 @@ impl Shell {
|
|||
) -> Result<Status, RuntimeError> {
|
||||
let mut pgid = Pid::from_raw(0);
|
||||
let status = (|| {
|
||||
let mut cmds = pipeline.0.iter().map(PreparedCommand::new);
|
||||
let mut cmds = pipeline.0.iter();
|
||||
|
||||
let mut last_cmd = cmds.next().expect("pipelines need to have >1 commands");
|
||||
for mut cmd in cmds {
|
||||
let mut last_cmd = cmds
|
||||
.next()
|
||||
.map(|cmd| PreparedCommand::new(cmd, self))
|
||||
.expect("pipelines need to have >1 commands");
|
||||
for cmd in cmds {
|
||||
let mut cmd = PreparedCommand::new(cmd, self);
|
||||
let (output, input) = create_pipe()?;
|
||||
cmd.stdin = Some(output);
|
||||
|
||||
|
@ -121,12 +188,12 @@ impl Shell {
|
|||
Redirect::Stdout => last_cmd.stdout = Some(input),
|
||||
}
|
||||
|
||||
last_cmd.spawn(&mut pgid)?;
|
||||
last_cmd.spawn(self, &mut pgid)?;
|
||||
|
||||
last_cmd = cmd;
|
||||
}
|
||||
|
||||
last_cmd.spawn(&mut pgid)?;
|
||||
last_cmd.spawn(self, &mut pgid)?;
|
||||
|
||||
wait_pgid(pgid)
|
||||
})();
|
||||
|
@ -170,28 +237,3 @@ fn create_pipe() -> Result<(File, File), RuntimeError> {
|
|||
|
||||
Ok(unsafe { (File::from_raw_fd(output), File::from_raw_fd(input)) })
|
||||
}
|
||||
|
||||
/// Build words from WordParts.
|
||||
struct WordBuilder<'a>(Iter<'a, ast::Word<'a>>);
|
||||
|
||||
impl<'a> Iterator for WordBuilder<'a> {
|
||||
type Item = Cow<'a, str>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.0.next().map(|word| {
|
||||
let mut words = word.0.iter();
|
||||
|
||||
let mut s = match words.next().expect("words need to have >1 parts") {
|
||||
ast::WordPart::Text(text) => Cow::from(*text),
|
||||
};
|
||||
|
||||
for part in words {
|
||||
match part {
|
||||
ast::WordPart::Text(text) => s.to_mut().push_str(text),
|
||||
}
|
||||
}
|
||||
|
||||
s
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ use nix::{
|
|||
sys,
|
||||
unistd::{self, ForkResult},
|
||||
};
|
||||
use oyster_runtime::Shell;
|
||||
use oyster_runtime::{builtins::Builtin, Shell};
|
||||
|
||||
// TODO: test signal return codes
|
||||
|
||||
|
@ -170,3 +170,66 @@ fn multipart_word() {
|
|||
|
||||
assert_snapshot!(actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_builtin() {
|
||||
let ast = {
|
||||
use oyster_parser::ast::*;
|
||||
|
||||
Code(vec![Statement::Pipeline(Pipeline(vec![Command(
|
||||
vec![Word(vec![WordPart::Text("test_builtin")])],
|
||||
Redirect::None,
|
||||
)]))])
|
||||
};
|
||||
|
||||
let actual = collect_output(|| {
|
||||
let mut shell = Shell::new().unwrap();
|
||||
shell.builtins_mut().add(Builtin {
|
||||
name: "test_builtin",
|
||||
description: "test",
|
||||
fun: |_, _| {
|
||||
// XXX: this is a workaround around libtest's use of io::set_output_capture
|
||||
let _ = write!(std::io::stdout(), "this is a test\n");
|
||||
},
|
||||
});
|
||||
shell.run(&ast).unwrap();
|
||||
});
|
||||
|
||||
assert_snapshot!(actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_redirection() {
|
||||
let ast = {
|
||||
use oyster_parser::ast::*;
|
||||
|
||||
Code(vec![Statement::Pipeline(Pipeline(vec![
|
||||
Command(
|
||||
vec![Word(vec![WordPart::Text("test_builtin")])],
|
||||
Redirect::Stdout,
|
||||
),
|
||||
Command(
|
||||
vec![
|
||||
Word(vec![WordPart::Text("wc")]),
|
||||
Word(vec![WordPart::Text("-c")]),
|
||||
],
|
||||
Redirect::None,
|
||||
),
|
||||
]))])
|
||||
};
|
||||
|
||||
let actual = collect_output(|| {
|
||||
let mut shell = Shell::new().unwrap();
|
||||
shell.builtins_mut().add(Builtin {
|
||||
name: "test_builtin",
|
||||
description: "test",
|
||||
fun: |_, _| {
|
||||
// XXX: this is a workaround around libtest's use of io::set_output_capture
|
||||
let _ = write!(std::io::stdout(), "this is a test\n");
|
||||
},
|
||||
});
|
||||
shell.run(&ast).unwrap();
|
||||
});
|
||||
|
||||
assert_snapshot!(actual);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
source: crates/oyster_runtime/tests/it/pipeline.rs
|
||||
expression: actual
|
||||
---
|
||||
"15\r\n"
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
source: crates/oyster_runtime/tests/it/pipeline.rs
|
||||
expression: actual
|
||||
---
|
||||
"this is a test\r\n"
|
Loading…
Reference in a new issue