summaryrefslogtreecommitdiffstats
path: root/src/runner/builtins
diff options
context:
space:
mode:
Diffstat (limited to 'src/runner/builtins')
-rw-r--r--src/runner/builtins/command.rs373
-rw-r--r--src/runner/builtins/mod.rs242
2 files changed, 615 insertions, 0 deletions
diff --git a/src/runner/builtins/command.rs b/src/runner/builtins/command.rs
new file mode 100644
index 0000000..16d8b40
--- /dev/null
+++ b/src/runner/builtins/command.rs
@@ -0,0 +1,373 @@
+use crate::runner::prelude::*;
+
+pub struct Command {
+ exe: crate::parse::Exe,
+ f: super::Builtin,
+ cfg: Cfg,
+}
+
+impl Command {
+ pub fn new(
+ exe: crate::parse::Exe,
+ io: Io,
+ ) -> Result<Self, crate::parse::Exe> {
+ if let Some(s) = exe.exe().to_str() {
+ if let Some(f) = super::BUILTINS.get(s) {
+ Ok(Self {
+ exe,
+ f,
+ cfg: Cfg::new(io),
+ })
+ } else {
+ Err(exe)
+ }
+ } else {
+ Err(exe)
+ }
+ }
+
+ pub fn stdin(&mut self, fh: std::fs::File) {
+ self.cfg.io.set_stdin(fh);
+ }
+
+ pub fn stdout(&mut self, fh: std::fs::File) {
+ self.cfg.io.set_stdout(fh);
+ }
+
+ pub fn stderr(&mut self, fh: std::fs::File) {
+ self.cfg.io.set_stderr(fh);
+ }
+
+ // Safety: see pre_exec in tokio::process::Command (this is just a
+ // wrapper)
+ pub unsafe fn pre_exec<F>(&mut self, f: F)
+ where
+ F: 'static + FnMut() -> std::io::Result<()> + Send + Sync,
+ {
+ self.cfg.pre_exec(f);
+ }
+
+ pub fn apply_redirects(&mut self, redirects: &[crate::parse::Redirect]) {
+ self.cfg.io.apply_redirects(redirects);
+ }
+
+ pub fn spawn(self, env: &Env) -> Result<Child> {
+ let Self { f, exe, cfg } = self;
+ (f)(exe, env, cfg)
+ }
+}
+
+pub struct Cfg {
+ io: Io,
+ pre_exec: Option<
+ Box<dyn 'static + FnMut() -> std::io::Result<()> + Send + Sync>,
+ >,
+}
+
+impl Cfg {
+ fn new(io: Io) -> Self {
+ Self { io, pre_exec: None }
+ }
+
+ pub fn io(&self) -> &Io {
+ &self.io
+ }
+
+ // Safety: see pre_exec in tokio::process::Command (this is just a
+ // wrapper)
+ pub unsafe fn pre_exec<F>(&mut self, f: F)
+ where
+ F: 'static + FnMut() -> std::io::Result<()> + Send + Sync,
+ {
+ self.pre_exec = Some(Box::new(f));
+ }
+
+ pub fn setup_command(mut self, cmd: &mut crate::runner::Command) {
+ self.io.setup_command(cmd);
+ if let Some(pre_exec) = self.pre_exec.take() {
+ // Safety: pre_exec can only have been set by calling the pre_exec
+ // method, which is itself unsafe, so the safety comments at the
+ // point where that is called are the relevant ones
+ unsafe { cmd.pre_exec(pre_exec) };
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Io {
+ fds: std::collections::HashMap<
+ std::os::unix::io::RawFd,
+ std::sync::Arc<File>,
+ >,
+}
+
+impl Io {
+ pub fn new() -> Self {
+ Self {
+ fds: std::collections::HashMap::new(),
+ }
+ }
+
+ fn stdin(&self) -> Option<std::sync::Arc<File>> {
+ self.fds.get(&0).map(std::sync::Arc::clone)
+ }
+
+ pub fn set_stdin<T: std::os::unix::io::IntoRawFd>(&mut self, stdin: T) {
+ if let Some(file) = self.fds.remove(&0) {
+ File::maybe_drop(file);
+ }
+ self.fds.insert(
+ 0,
+ // Safety: we just acquired stdin via into_raw_fd, which acquires
+ // ownership of the fd, so we are now the sole owner
+ std::sync::Arc::new(unsafe { File::input(stdin.into_raw_fd()) }),
+ );
+ }
+
+ fn stdout(&self) -> Option<std::sync::Arc<File>> {
+ self.fds.get(&1).map(std::sync::Arc::clone)
+ }
+
+ pub fn set_stdout<T: std::os::unix::io::IntoRawFd>(&mut self, stdout: T) {
+ if let Some(file) = self.fds.remove(&1) {
+ File::maybe_drop(file);
+ }
+ self.fds.insert(
+ 1,
+ // Safety: we just acquired stdout via into_raw_fd, which acquires
+ // ownership of the fd, so we are now the sole owner
+ std::sync::Arc::new(unsafe {
+ File::output(stdout.into_raw_fd())
+ }),
+ );
+ }
+
+ fn stderr(&self) -> Option<std::sync::Arc<File>> {
+ self.fds.get(&2).map(std::sync::Arc::clone)
+ }
+
+ pub fn set_stderr<T: std::os::unix::io::IntoRawFd>(&mut self, stderr: T) {
+ if let Some(file) = self.fds.remove(&2) {
+ File::maybe_drop(file);
+ }
+ self.fds.insert(
+ 2,
+ // Safety: we just acquired stderr via into_raw_fd, which acquires
+ // ownership of the fd, so we are now the sole owner
+ std::sync::Arc::new(unsafe {
+ File::output(stderr.into_raw_fd())
+ }),
+ );
+ }
+
+ pub fn apply_redirects(&mut self, redirects: &[crate::parse::Redirect]) {
+ for redirect in redirects {
+ let to = match &redirect.to {
+ crate::parse::RedirectTarget::Fd(fd) => {
+ std::sync::Arc::clone(&self.fds[fd])
+ }
+ crate::parse::RedirectTarget::File(path) => {
+ let fd = redirect.dir.open(path).unwrap();
+ match redirect.dir {
+ crate::parse::Direction::In => {
+ // Safety: we just opened fd, and nothing else has
+ // or can use it
+ std::sync::Arc::new(unsafe { File::input(fd) })
+ }
+ crate::parse::Direction::Out
+ | crate::parse::Direction::Append => {
+ // Safety: we just opened fd, and nothing else has
+ // or can use it
+ std::sync::Arc::new(unsafe { File::output(fd) })
+ }
+ }
+ }
+ };
+ self.fds.insert(redirect.from, to);
+ }
+ }
+
+ pub fn read_line_stdin(&self) -> Result<(String, bool)> {
+ let mut line = vec![];
+ if let Some(file) = self.stdin() {
+ if let File::In(fh) = &*file {
+ // we have to read only a single character at a time here
+ // because stdin needs to be shared across all commands in the
+ // command list, some of which may be builtins and others of
+ // which may be external commands - if we read past the end of
+ // a line, then the characters past the end of that line will
+ // no longer be available to the next command, since we have
+ // them buffered in memory rather than them being on the stdin
+ // pipe.
+ for byte in fh.bytes() {
+ let byte = byte?;
+ line.push(byte);
+ if byte == b'\n' {
+ break;
+ }
+ }
+ }
+ }
+ let done = line.is_empty();
+ let mut line = String::from_utf8(line).unwrap();
+ if line.ends_with('\n') {
+ line.truncate(line.len() - 1);
+ }
+ Ok((line, done))
+ }
+
+ pub fn write_stdout(&self, buf: &[u8]) -> Result<()> {
+ if let Some(file) = self.stdout() {
+ if let File::Out(fh) = &*file {
+ Ok((&*fh).write_all(buf)?)
+ } else {
+ Ok(())
+ }
+ } else {
+ Ok(())
+ }
+ }
+
+ pub fn write_stderr(&self, buf: &[u8]) -> Result<()> {
+ if let Some(file) = self.stderr() {
+ if let File::Out(fh) = &*file {
+ Ok((&*fh).write_all(buf)?)
+ } else {
+ Ok(())
+ }
+ } else {
+ Ok(())
+ }
+ }
+
+ pub fn setup_command(mut self, cmd: &mut crate::runner::Command) {
+ if let Some(stdin) = self.fds.remove(&0) {
+ if let Ok(stdin) = std::sync::Arc::try_unwrap(stdin) {
+ let stdin = stdin.into_raw_fd();
+ if stdin != 0 {
+ // Safety: we just acquired stdin via into_raw_fd, which
+ // acquires ownership of the fd, so we are now the sole
+ // owner
+ cmd.stdin(unsafe { std::fs::File::from_raw_fd(stdin) });
+ self.fds.remove(&0);
+ }
+ }
+ }
+ if let Some(stdout) = self.fds.remove(&1) {
+ if let Ok(stdout) = std::sync::Arc::try_unwrap(stdout) {
+ let stdout = stdout.into_raw_fd();
+ if stdout != 1 {
+ // Safety: we just acquired stdout via into_raw_fd, which
+ // acquires ownership of the fd, so we are now the sole
+ // owner
+ cmd.stdout(unsafe { std::fs::File::from_raw_fd(stdout) });
+ self.fds.remove(&1);
+ }
+ }
+ }
+ if let Some(stderr) = self.fds.remove(&2) {
+ if let Ok(stderr) = std::sync::Arc::try_unwrap(stderr) {
+ let stderr = stderr.into_raw_fd();
+ if stderr != 2 {
+ // Safety: we just acquired stderr via into_raw_fd, which
+ // acquires ownership of the fd, so we are now the sole
+ // owner
+ cmd.stderr(unsafe { std::fs::File::from_raw_fd(stderr) });
+ self.fds.remove(&2);
+ }
+ }
+ }
+ }
+}
+
+impl Drop for Io {
+ fn drop(&mut self) {
+ for (_, file) in self.fds.drain() {
+ File::maybe_drop(file);
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum File {
+ In(std::fs::File),
+ Out(std::fs::File),
+}
+
+impl File {
+ // Safety: fd must not be owned by any other File object
+ pub unsafe fn input(fd: std::os::unix::io::RawFd) -> Self {
+ Self::In(std::fs::File::from_raw_fd(fd))
+ }
+
+ // Safety: fd must not be owned by any other File object
+ pub unsafe fn output(fd: std::os::unix::io::RawFd) -> Self {
+ Self::Out(std::fs::File::from_raw_fd(fd))
+ }
+
+ fn maybe_drop(file: std::sync::Arc<Self>) {
+ if let Ok(file) = std::sync::Arc::try_unwrap(file) {
+ if file.as_raw_fd() <= 2 {
+ let _ = file.into_raw_fd();
+ }
+ }
+ }
+}
+
+impl std::os::unix::io::AsRawFd for File {
+ fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
+ match self {
+ Self::In(fh) | Self::Out(fh) => fh.as_raw_fd(),
+ }
+ }
+}
+
+impl std::os::unix::io::IntoRawFd for File {
+ fn into_raw_fd(self) -> std::os::unix::io::RawFd {
+ match self {
+ Self::In(fh) | Self::Out(fh) => fh.into_raw_fd(),
+ }
+ }
+}
+
+pub enum Child {
+ Task(tokio::task::JoinHandle<std::process::ExitStatus>),
+ Wrapped(Box<crate::runner::Child>),
+}
+
+impl Child {
+ pub fn new_task<F>(f: F) -> Self
+ where
+ F: FnOnce() -> std::process::ExitStatus + Send + 'static,
+ {
+ Self::Task(tokio::task::spawn_blocking(f))
+ }
+
+ pub fn new_wrapped(child: crate::runner::Child) -> Self {
+ Self::Wrapped(Box::new(child))
+ }
+
+ pub fn id(&self) -> Option<u32> {
+ match self {
+ Self::Task(_) => None,
+ Self::Wrapped(child) => child.id(),
+ }
+ }
+
+ pub fn status(
+ self,
+ ) -> std::pin::Pin<
+ Box<
+ dyn std::future::Future<Output = Result<std::process::ExitStatus>>
+ + Send
+ + Sync,
+ >,
+ > {
+ Box::pin(async move {
+ match self {
+ Self::Task(task) => task.await.map_err(|e| anyhow!(e)),
+ Self::Wrapped(child) => child.status().await,
+ }
+ })
+ }
+}
diff --git a/src/runner/builtins/mod.rs b/src/runner/builtins/mod.rs
new file mode 100644
index 0000000..b714c58
--- /dev/null
+++ b/src/runner/builtins/mod.rs
@@ -0,0 +1,242 @@
+use crate::runner::prelude::*;
+
+pub mod command;
+pub use command::{Child, Command, File, Io};
+
+type Builtin = &'static (dyn for<'a> Fn(
+ crate::parse::Exe,
+ &'a Env,
+ command::Cfg,
+) -> Result<command::Child>
+ + Sync
+ + Send);
+
+#[allow(clippy::as_conversions)]
+static BUILTINS: once_cell::sync::Lazy<
+ std::collections::HashMap<&'static str, Builtin>,
+> = once_cell::sync::Lazy::new(|| {
+ let mut builtins = std::collections::HashMap::new();
+ builtins.insert("cd", &cd as Builtin);
+ builtins.insert("set", &set);
+ builtins.insert("unset", &unset);
+ builtins.insert("echo", &echo);
+ builtins.insert("read", &read);
+ builtins.insert("and", &and);
+ builtins.insert("or", &or);
+ builtins.insert("command", &command);
+ builtins.insert("builtin", &builtin);
+ builtins
+});
+
+macro_rules! bail {
+ ($cfg:expr, $exe:expr, $msg:expr $(,)?) => {
+ $cfg.io().write_stderr(
+ format!("{}: {}\n", $exe.exe().display(), $msg).as_bytes()
+ )
+ .unwrap();
+ return std::process::ExitStatus::from_raw(1 << 8);
+ };
+ ($cfg:expr, $exe:expr, $msg:expr, $($arg:tt)*) => {
+ $cfg.io().write_stderr(
+ format!("{}: ", $exe.exe().display()).as_bytes()
+ )
+ .unwrap();
+ $cfg.io().write_stderr(format!($msg, $($arg)*).as_bytes())
+ .unwrap();
+ $cfg.io().write_stderr(b"\n").unwrap();
+ return std::process::ExitStatus::from_raw(1 << 8);
+ };
+}
+
+// clippy can't tell that the type is necessary
+#[allow(clippy::unnecessary_wraps)]
+fn cd(
+ exe: crate::parse::Exe,
+ env: &Env,
+ cfg: command::Cfg,
+) -> Result<command::Child> {
+ let prev_pwd = env.prev_pwd();
+ let home = env.var("HOME");
+ Ok(command::Child::new_task(move || {
+ let dir = if let Some(dir) = exe.args().get(0) {
+ if dir.is_empty() {
+ ".".to_string().into()
+ } else if dir == "-" {
+ prev_pwd
+ } else {
+ dir.into()
+ }
+ } else {
+ let dir = home;
+ if let Some(dir) = dir {
+ dir.into()
+ } else {
+ bail!(cfg, exe, "could not find home directory");
+ }
+ };
+ if let Err(e) = std::env::set_current_dir(&dir) {
+ bail!(
+ cfg,
+ exe,
+ "{}: {}",
+ crate::format::io_error(&e),
+ dir.display()
+ );
+ }
+ std::process::ExitStatus::from_raw(0)
+ }))
+}
+
+#[allow(clippy::unnecessary_wraps)]
+fn set(
+ exe: crate::parse::Exe,
+ _env: &Env,
+ cfg: command::Cfg,
+) -> Result<command::Child> {
+ Ok(command::Child::new_task(move || {
+ let k = if let Some(k) = exe.args().get(0).map(String::as_str) {
+ k
+ } else {
+ bail!(cfg, exe, "usage: set key value");
+ };
+ let v = if let Some(v) = exe.args().get(1).map(String::as_str) {
+ v
+ } else {
+ bail!(cfg, exe, "usage: set key value");
+ };
+
+ std::env::set_var(k, v);
+ std::process::ExitStatus::from_raw(0)
+ }))
+}
+
+#[allow(clippy::unnecessary_wraps)]
+fn unset(
+ exe: crate::parse::Exe,
+ _env: &Env,
+ cfg: command::Cfg,
+) -> Result<command::Child> {
+ Ok(command::Child::new_task(move || {
+ let k = if let Some(k) = exe.args().get(0).map(String::as_str) {
+ k
+ } else {
+ bail!(cfg, exe, "usage: unset key");
+ };
+
+ std::env::remove_var(k);
+ std::process::ExitStatus::from_raw(0)
+ }))
+}
+
+// clippy can't tell that the type is necessary
+#[allow(clippy::unnecessary_wraps)]
+// mostly just for testing and ensuring that builtins work, i'll likely remove
+// this later, since the binary seems totally fine
+fn echo(
+ exe: crate::parse::Exe,
+ _env: &Env,
+ cfg: command::Cfg,
+) -> Result<command::Child> {
+ Ok(command::Child::new_task(move || {
+ macro_rules! write_stdout {
+ ($bytes:expr) => {
+ if let Err(e) = cfg.io().write_stdout($bytes) {
+ cfg.io()
+ .write_stderr(format!("echo: {}", e).as_bytes())
+ .unwrap();
+ return std::process::ExitStatus::from_raw(1 << 8);
+ }
+ };
+ }
+ let count = exe.args().len();
+ for (i, arg) in exe.args().iter().enumerate() {
+ write_stdout!(arg.as_bytes());
+ if i == count - 1 {
+ write_stdout!(b"\n");
+ } else {
+ write_stdout!(b" ");
+ }
+ }
+
+ std::process::ExitStatus::from_raw(0)
+ }))
+}
+
+#[allow(clippy::unnecessary_wraps)]
+fn read(
+ exe: crate::parse::Exe,
+ _env: &Env,
+ cfg: command::Cfg,
+) -> Result<command::Child> {
+ Ok(command::Child::new_task(move || {
+ let var = if let Some(var) = exe.args().get(0).map(String::as_str) {
+ var
+ } else {
+ bail!(cfg, exe, "usage: read var");
+ };
+
+ let (val, done) = match cfg.io().read_line_stdin() {
+ Ok((line, done)) => (line, done),
+ Err(e) => {
+ bail!(cfg, exe, e);
+ }
+ };
+
+ std::env::set_var(var, val);
+ std::process::ExitStatus::from_raw(if done { 1 << 8 } else { 0 })
+ }))
+}
+
+fn and(
+ mut exe: crate::parse::Exe,
+ env: &Env,
+ cfg: command::Cfg,
+) -> Result<command::Child> {
+ exe.shift();
+ if env.latest_status().success() {
+ let mut cmd = crate::runner::Command::new(exe, cfg.io().clone());
+ cfg.setup_command(&mut cmd);
+ Ok(command::Child::new_wrapped(cmd.spawn(env)?))
+ } else {
+ let status = env.latest_status();
+ Ok(command::Child::new_task(move || status))
+ }
+}
+
+fn or(
+ mut exe: crate::parse::Exe,
+ env: &Env,
+ cfg: command::Cfg,
+) -> Result<command::Child> {
+ exe.shift();
+ if env.latest_status().success() {
+ let status = env.latest_status();
+ Ok(command::Child::new_task(move || status))
+ } else {
+ let mut cmd = crate::runner::Command::new(exe, cfg.io().clone());
+ cfg.setup_command(&mut cmd);
+ Ok(command::Child::new_wrapped(cmd.spawn(env)?))
+ }
+}
+
+fn command(
+ mut exe: crate::parse::Exe,
+ env: &Env,
+ cfg: command::Cfg,
+) -> Result<command::Child> {
+ exe.shift();
+ let mut cmd = crate::runner::Command::new_binary(&exe);
+ cfg.setup_command(&mut cmd);
+ Ok(command::Child::new_wrapped(cmd.spawn(env)?))
+}
+
+fn builtin(
+ mut exe: crate::parse::Exe,
+ env: &Env,
+ cfg: command::Cfg,
+) -> Result<command::Child> {
+ exe.shift();
+ let mut cmd = crate::runner::Command::new_builtin(exe, cfg.io().clone());
+ cfg.setup_command(&mut cmd);
+ Ok(command::Child::new_wrapped(cmd.spawn(env)?))
+}