operator based command spawning and direction in rust (wip)
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.lock | 16 | ||||
| -rw-r--r-- | Cargo.toml | 9 | ||||
| -rw-r--r-- | src/lib.rs | 167 | ||||
| -rw-r--r-- | src/output.rs | 42 | ||||
| -rw-r--r-- | src/processor.rs | 112 | ||||
| -rw-r--r-- | src/util.rs | 36 |
7 files changed, 383 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e027715 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "shal" +version = "0.1.0" +dependencies = [ + "anyhow", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9fab9d4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "shal" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.75" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1a5055f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,167 @@ +#![allow(non_camel_case_types)] +use std::{ + ffi::{OsStr, OsString}, + io::{self, Read, Write}, + ops::{BitOr, Shr}, + process::{self, Child, ChildStderr, ChildStdin, ChildStdout, Stdio}, +}; +mod output; +mod processor; +mod util; +pub use output::null; +use processor::Io; +pub use processor::{Process, Processor}; +pub use util::echo; + +/// generates a impl of $a | $b => [`Processor`]. +macro_rules! imp { + (Processor,$b:ty) => { + impl BitOr<$b> for Processor { + type Output = Processor; + fn bitor(mut self, rhs: $b) -> Self::Output { + self.add(rhs); + self + } + } + }; + ($a:ty,$b:ty) => { + impl BitOr<$b> for $a { + type Output = Processor; + fn bitor(self, rhs: $b) -> Self::Output { + let mut p = Processor::new(512); + p.add(self); + p.add(rhs); + p + } + } + }; + (output $from:ty) => { + impl Shr<&mut String> for $from { + type Output = anyhow::Result<()>; + fn shr(self, rhs: &mut String) -> Self::Output { + let mut p = Processor::new(512); + p.add(self); + p >> rhs + } + } + + impl Shr<()> for $from { + type Output = anyhow::Result<String>; + fn shr(self, _: ()) -> Self::Output { + let mut s = String::new(); + (self >> &mut s)?; + Ok(s) + } + } + }; +} + +// imp!(Command, grep); // cargo | grep +// imp!(echo, grep); // echo | grep +// imp!(Processor, grep); // a | b | grep +imp!(Command, Command); // command | command +imp!(echo, Command); // echo | command +imp!(Processor, Command); // a | b | command +imp!(output Command); +imp!(output echo); + +impl Shr<&mut String> for Processor { + type Output = anyhow::Result<()>; + + fn shr(mut self, rhs: &mut String) -> Self::Output { + self.add(output::StringOut(rhs as *mut _)); + self.complete()?; + Ok(()) + } +} + +impl Shr<()> for Processor { + type Output = anyhow::Result<String>; + + fn shr(self, _: ()) -> Self::Output { + let mut s = String::new(); + (self >> &mut s)?; + Ok(s) + } +} + +#[derive(Debug)] +pub struct Command { + stdout: ChildStdout, + stderr: ChildStderr, + stdin: ChildStdin, + proc: Child, +} + +unsafe impl Process for Command { + fn run(&mut self, on: processor::Io) -> anyhow::Result<usize> { + match on { + Io::First(b) => Ok(self.stdout.read(b)?), + Io::Middle(i, o) => { + self.stdin.write(i)?; + Ok(self.stdout.read(o)?) + } + Io::Last(i) => { + self.stdin.write(i)?; + Ok(0) + } + } + } + + fn done(&mut self, _: Option<bool>) -> anyhow::Result<bool> { + Ok(self.proc.try_wait()?.is_some()) + } +} + +#[derive(Debug)] +pub struct CommandBuilder { + command: OsString, + args: Vec<OsString>, +} + +impl CommandBuilder { + pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut CommandBuilder { + self.args.push(arg.as_ref().to_os_string()); + self + } + + pub fn args(&mut self, args: impl IntoIterator<Item = impl AsRef<OsStr>> + ExactSizeIterator) { + self.args.reserve(self.args.len() + args.len()); + for arg in args { + self.args.push(arg.as_ref().to_os_string()); + } + } + + pub fn spawn(&mut self) -> io::Result<Command> { + let mut proc = process::Command::new(&self.command) + .args(&self.args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .stdin(Stdio::piped()) + .spawn()?; + Ok(Command { + stdout: proc.stdout.take().unwrap(), + stderr: proc.stderr.take().unwrap(), + stdin: proc.stdin.take().unwrap(), + proc, + }) + } +} + +impl Command { + pub fn new(command: impl AsRef<OsStr>) -> CommandBuilder { + CommandBuilder { + command: command.as_ref().to_os_string(), + args: vec![], + } + } +} + +#[test] +fn usage() { + let o = ((Command::new("cargo").spawn().unwrap() + | Command::new("grep").arg("test").spawn().unwrap()) + >> ()) + .unwrap(); + assert_eq!(o, ""); +} diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..f2a7dc2 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,42 @@ + + +use crate::{processor::Io, Process}; + +trait Output: std::fmt::Debug { + fn take(&mut self, bytes: &[u8]) -> anyhow::Result<()>; +} + +unsafe impl<T: Output> Process for T { + fn run(&mut self, on: Io) -> anyhow::Result<usize> { + match on { + Io::Last(bytes) => self.take(bytes)?, + Io::First(_) | Io::Middle(_, _) => unreachable!("outputs must be at the end"), + } + Ok(0) + } +} + +#[derive(Debug)] +pub(crate) struct StringOut(pub *mut String); + +impl Output for StringOut { + fn take(&mut self, bytes: &[u8]) -> anyhow::Result<()> { + unsafe { &mut *self.0 }.push_str(std::str::from_utf8(bytes)?); + Ok(()) + } +} + +/// `/dev/null` +pub struct null {} + +impl std::fmt::Debug for null { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "/dev/null") + } +} + +impl Output for null { + fn take(&mut self, _: &[u8]) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/src/processor.rs b/src/processor.rs new file mode 100644 index 0000000..1d8b129 --- /dev/null +++ b/src/processor.rs @@ -0,0 +1,112 @@ +// Process("echo a")[Io::Front] => Process(grep("a"))[Io::Middle] => Process(File) +/// # Safety +/// +/// this will return the number of bytes passed to the buffer, and ret < buffer bounds +pub unsafe trait Process: std::fmt::Debug { + fn run(&mut self, on: Io) -> anyhow::Result<usize>; + fn done(&mut self, before: Option<bool>) -> anyhow::Result<bool> { + Ok(before.unwrap_or(false)) + } +} + +#[derive(Debug)] +pub struct Processor { + pub(crate) processes: Vec<Box<dyn Process>>, + buffer: Vec<u8>, + buffer_size: u32, +} + +pub enum Io<'a> { + /// echo "a" => next + First(&'a mut [u8]), + /// => file + Last(&'a [u8]), + /// => this => + Middle(&'a [u8], &'a mut [u8]), +} + +#[derive(Copy, Clone, Debug)] +enum Buf { + A, + B, +} + +impl Processor { + pub fn new(buffer: u32) -> Self { + Processor { + processes: Vec::with_capacity(2), + buffer: vec![0; buffer as usize * 2], + buffer_size: buffer, + } + } + + pub(crate) fn add(&mut self, p: impl Process + 'static) { + self.processes.push(Box::new(p)); + } + + pub fn complete(&mut self) -> anyhow::Result<()> { + while !self.done()? { + self.step()?; + } + Ok(()) + } + + pub fn done(&mut self) -> anyhow::Result<bool> { + let mut done = true; + let mut last = None; + for p in &mut self.processes { + let result = p.done(last)?; + done &= result; + last = Some(result); + } + Ok(done) + } + + pub fn step(&mut self) -> anyhow::Result<()> { + macro_rules! a { + () => { + &mut self.buffer[..self.buffer_size as usize] + }; + } + macro_rules! b { + () => { + &mut self.buffer[self.buffer_size as usize..] + }; + } + // [a,b,c,d,e] + // a (first(a_buf)) + // b (mid(a_buf, b_buf)) + // c (mid(b_buf, a_buf)) + // d (mid(a_buf, b_buf)) + // e (last(b_buf)) + let mut last = Buf::A; + let mut read = 0; + let size = self.processes.len(); + for (i, p) in self.processes.iter_mut().enumerate() { + match i { + 0 => read = p.run(Io::First(a!()))?, + n if n + 1 == size => { + p.run(Io::Last( + &match last { + Buf::A => a!(), + Buf::B => b!(), + }[..read], + ))?; + } + _ => match last { + Buf::A => { + let (a, b) = self.buffer.split_at_mut(self.buffer_size as usize); + read = p.run(Io::Middle(&a[..read], b))?; + last = Buf::B; + } + Buf::B => { + let (a, b) = self.buffer.split_at_mut(self.buffer_size as usize); + read = p.run(Io::Middle(&b[..read], a))?; + last = Buf::A; + } + }, + } + } + Ok(()) + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..b888a3c --- /dev/null +++ b/src/util.rs @@ -0,0 +1,36 @@ +#![allow(non_camel_case_types)] + +use std::io::Write; + +use crate::{processor::Io, Process}; + +impl std::fmt::Debug for echo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "echo {}", + String::from_utf8_lossy(self.what).escape_debug() + ) + } +} +pub struct echo { + what: &'static [u8], + written: usize, +} + +unsafe impl Process for echo { + fn run(&mut self, on: Io) -> anyhow::Result<usize> { + match on { + Io::First(mut w) => { + let writ = w.write(&self.what[..self.written])?; + self.written += writ; + Ok(writ) + } + Io::Middle(..) | Io::Last(_) => unreachable!(), + } + } + + fn done(&mut self, _: Option<bool>) -> anyhow::Result<bool> { + Ok(self.written == self.what.len()) + } +} |