feat: Pipe

This commit is contained in:
2025-01-29 00:04:00 -05:00
parent 6fa4b21c4f
commit 9d944c3218
15 changed files with 888 additions and 235 deletions

26
macro/Cargo.toml Normal file
View File

@@ -0,0 +1,26 @@
[package]
name = "comlexr_macro"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
description.workspace = true
repository.workspace = true
license.workspace = true
[package.metadata."docs.rs"]
all-features = true
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full", "derive"] }
[dev-dependencies]
tempfile.workspace = true
[lints]
workspace = true

522
macro/src/cmd.rs Normal file
View File

@@ -0,0 +1,522 @@
use quote::{quote, ToTokens};
use syn::{
braced, bracketed,
parse::{discouraged::Speculative, Parse},
punctuated::Punctuated,
token, Token,
};
use crate::macros::enum_to_tokens;
pub struct Command {
cd: CurrentDir,
env_vars: EnvVars,
program: Value,
args: Option<Punctuated<LogicArg, Token![,]>>,
}
impl Parse for Command {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let cd = input.parse()?;
let env_vars = input.parse()?;
let program = input.parse()?;
if input.is_empty() {
Ok(Self {
cd,
env_vars,
program,
args: None,
})
} else {
_ = input.parse::<Token![,]>()?;
Ok(Self {
cd,
env_vars,
program,
args: Some(Punctuated::parse_terminated(input)?),
})
}
}
}
impl ToTokens for Command {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self {
cd,
env_vars,
program,
args,
} = self;
let program = quote! { ::std::process::Command::new(#program) };
let args = args
.as_ref()
.map(Punctuated::iter)
.map_or_else(Vec::new, Iterator::collect);
tokens.extend(quote! {
{
let mut _c = #program;
#cd
#env_vars
#(#args)*
_c
}
});
}
}
enum LogicArg {
Expr(SingleArg),
ForIter(ForIter),
ForIn(ForIn),
IfLet(IfLet),
If(If),
Match(Match),
Closure(Closure),
}
impl Parse for LogicArg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.peek(Token![for]) {
let pat_fork = input.fork();
_ = pat_fork.parse::<Token![for]>()?;
if pat_fork.call(syn::Pat::parse_single).is_ok() && pat_fork.peek(Token![in]) {
input.parse().map(Self::ForIn)
} else {
input.parse().map(Self::ForIter)
}
} else if input.peek(Token![if]) {
if input.peek2(Token![let]) {
input.parse().map(Self::IfLet)
} else {
input.parse().map(Self::If)
}
} else if input.peek(Token![match]) {
input.parse().map(Self::Match)
} else if input.peek(Token![||]) {
input.parse().map(Self::Closure)
} else {
input.parse().map(Self::Expr)
}
}
}
enum_to_tokens! {LogicArg: Expr, ForIter, ForIn, IfLet, If, Match, Closure}
struct ForIter {
expr: Value,
}
impl Parse for ForIter {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
_ = input.parse::<Token![for]>()?;
let expr = input.parse()?;
Ok(Self { expr })
}
}
impl ToTokens for ForIter {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let expr = &self.expr;
tokens.extend(quote! {
for _a in #expr {
_c.arg(_a);
}
});
}
}
struct ForIn {
pattern: syn::Pat,
iter: Value,
args: Arguments,
}
impl Parse for ForIn {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
_ = input.parse::<Token![for]>()?;
let pattern = input.call(syn::Pat::parse_single)?;
_ = input.parse::<Token![in]>()?;
let iter = input.parse()?;
_ = input.parse::<Token![=>]>()?;
let args = input.parse()?;
Ok(Self {
pattern,
iter,
args,
})
}
}
impl ToTokens for ForIn {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self {
pattern,
iter,
args,
} = self;
tokens.extend(quote! {
for #pattern in #iter {
#args
}
});
}
}
struct IfLet {
pattern: syn::Pat,
expr: Value,
args: Arguments,
}
impl Parse for IfLet {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
_ = input.parse::<Token![if]>()?;
_ = input.parse::<Token![let]>()?;
let pattern = input.call(syn::Pat::parse_single)?;
_ = input.parse::<Token![=]>()?;
let expr = input.parse()?;
_ = input.parse::<Token![=>]>()?;
let args = input.parse()?;
Ok(Self {
pattern,
expr,
args,
})
}
}
impl ToTokens for IfLet {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self {
pattern,
expr,
args,
} = &self;
tokens.extend(quote! {
if let #pattern = #expr {
#args
}
});
}
}
struct If {
expr: Value,
args: Arguments,
}
impl Parse for If {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
_ = input.parse::<Token![if]>()?;
let expr = input.parse()?;
_ = input.parse::<Token![=>]>()?;
let args = input.parse()?;
Ok(Self { expr, args })
}
}
impl ToTokens for If {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self { expr, args } = self;
tokens.extend(quote! {
if #expr {
#args
}
});
}
}
struct Match {
expr: Value,
match_arms: Punctuated<MatchArm, Token![,]>,
}
impl Parse for Match {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
_ = input.parse::<Token![match]>()?;
let expr = input.parse()?;
let arms;
braced!(arms in input);
let match_arms = Punctuated::parse_terminated(&arms)?;
Ok(Self { expr, match_arms })
}
}
impl ToTokens for Match {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self { expr, match_arms } = self;
let match_arms = match_arms.iter();
tokens.extend(quote! {
match #expr {
#(#match_arms)*
}
});
}
}
struct MatchArm {
pattern: syn::Pat,
if_expr: Option<Value>,
args: Arguments,
}
impl Parse for MatchArm {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let pattern = input.call(syn::Pat::parse_multi)?;
let if_expr = if input.peek(Token![if]) {
_ = input.parse::<Token![if]>()?;
Some(input.parse()?)
} else {
None
};
_ = input.parse::<Token![=>]>()?;
let args = input.parse()?;
Ok(Self {
pattern,
if_expr,
args,
})
}
}
impl ToTokens for MatchArm {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self {
pattern,
if_expr,
args,
} = self;
tokens.extend(if_expr.as_ref().map_or_else(
|| {
quote! {
#pattern => {
#args
}
}
},
|if_expr| {
quote! {
#pattern if #if_expr => {
#args
}
}
},
));
}
}
struct Closure(syn::Expr);
impl Parse for Closure {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
_ = input.parse::<Token![||]>()?;
input.parse().map(Self)
}
}
impl ToTokens for Closure {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self(block) = self;
tokens.extend(quote! {
let _fn = || #block;
for _a in _fn() {
_c.arg(_a);
}
});
}
}
enum Arguments {
Single(SingleArg),
Multi(MultiArg),
}
impl Parse for Arguments {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.peek(token::Bracket) {
input.parse().map(Self::Multi)
} else {
input.parse().map(Self::Single)
}
}
}
enum_to_tokens! {Arguments: Single, Multi}
struct MultiArg(Punctuated<SingleArg, Token![,]>);
impl Parse for MultiArg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let args;
bracketed!(args in input);
Punctuated::parse_terminated(&args).map(Self)
}
}
impl ToTokens for MultiArg {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let args = self.0.iter().collect::<Vec<_>>();
tokens.extend(quote! { #(#args)* });
}
}
struct SingleArg(Value);
impl Parse for SingleArg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
input.parse().map(Self)
}
}
impl ToTokens for SingleArg {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let arg = &self.0;
tokens.extend(quote! {
_c.arg(#arg);
});
}
}
struct EnvVars(Option<Punctuated<EnvVar, Token![,]>>);
impl Parse for EnvVars {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let fork = input.fork();
let ident = fork.cursor().ident();
match ident {
Some((ident, _)) if ident == "env" => {
_ = fork.parse::<syn::Ident>()?;
let envs;
braced!(envs in fork);
Punctuated::parse_terminated(&envs)
.and_then(|envs| {
_ = fork.parse::<Token![;]>()?;
input.advance_to(&fork);
Ok(Self(Some(envs)))
})
.or_else(|_| Ok(Self(None)))
}
_ => Ok(Self(None)),
}
}
}
impl ToTokens for EnvVars {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self(envs) = self;
let envs = envs
.as_ref()
.map_or_else(Vec::new, |envs| envs.iter().collect());
tokens.extend(quote! {
#(#envs)*
});
}
}
struct EnvVar {
key: Value,
value: Value,
}
impl Parse for EnvVar {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let key = input.parse()?;
_ = input.parse::<Token![:]>()?;
let value = input.parse()?;
Ok(Self { key, value })
}
}
impl ToTokens for EnvVar {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self { key, value } = self;
tokens.extend(quote! { _c.env(#key, #value); });
}
}
struct CurrentDir(Option<Value>);
impl Parse for CurrentDir {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let fork = input.fork();
let ident = fork.cursor().ident();
match ident {
Some((ident, _)) if ident == "cd" => {
_ = fork.parse::<syn::Ident>();
fork.parse()
.and_then(|value| {
_ = fork.parse::<Token![;]>()?;
input.advance_to(&fork);
Ok(Self(Some(value)))
})
.or_else(|_| Ok(Self(None)))
}
_ => Ok(Self(None)),
}
}
}
impl ToTokens for CurrentDir {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self(cd) = self;
let cd = cd.iter();
tokens.extend(quote! { #(_c.current_dir(#cd);)* });
}
}
pub enum Value {
Lit(syn::Lit),
Ident(syn::Ident),
Expr(syn::Expr),
}
impl Parse for Value {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let expr_fork = input.fork();
expr_fork
.parse()
.map(|expr| {
input.advance_to(&expr_fork);
Self::Expr(expr)
})
.or_else(|_| {
if input.peek(syn::Ident) {
input.parse().map(Self::Ident)
} else if input.peek(syn::Lit) {
input.parse().map(Self::Lit)
} else {
Err(syn::Error::new(
input.span(),
"Expected an expression, ident, or literal",
))
}
})
}
}
enum_to_tokens! {Value: Lit, Ident, Expr}

200
macro/src/lib.rs Normal file
View File

@@ -0,0 +1,200 @@
extern crate quote;
extern crate syn;
use quote::quote;
use syn::parse_macro_input;
mod cmd;
mod macros;
mod pipe;
/// Generates a command expression by combining static strings, conditional logic, loops,
/// pattern matching, and closures to dynamically build and format command-line arguments.
///
/// The `cmd!` macro supports flexible syntax constructs such as:
/// - **Static arguments**: Basic string arguments.
/// - **Conditional inclusion**: Using `if` or `if let` to conditionally include arguments.
/// - **Iterative inclusion**: Using `for` loops to include multiple arguments from collections.
/// - **Pattern matching**: Using `match` expressions for dynamic argument selection.
/// - **Closures**: Dynamically generating arguments at runtime.
///
/// # Examples
///
/// ## Basic Usage
/// ```
/// # use comlexr_macro::cmd;
/// let command = cmd!("echo", "test");
/// assert_eq!(format!("{command:?}"), r#""echo" "test""#.to_string());
/// ```
///
/// ## Current Directory
/// ```
/// # use comlexr_macro::cmd;
/// let command = cmd!(
/// cd "~/";
/// "echo",
/// "test",
/// );
///
/// assert_eq!(format!("{command:?}"), r#"cd "~/" && "echo" "test""#);
/// ```
///
/// ## Environment Vars
/// ```
/// # use comlexr_macro::cmd;
/// const NEW_VAR: &str = "NEW_VAR";
///
/// let command = cmd!(
/// env {
/// "TEST": "test",
/// NEW_VAR: "new_var"
/// };
/// "echo",
/// "test",
/// );
///
/// assert_eq!(format!("{command:?}"), r#"NEW_VAR="new_var" TEST="test" "echo" "test""#);
/// ```
///
/// ### Current Directory and Environment Variable Order
/// ```
/// # use comlexr_macro::cmd;
/// let command = cmd!(
/// cd "~/";
/// env {
/// "TEST": "test",
/// };
/// "echo",
/// "test",
/// );
///
/// assert_eq!(
/// format!("{command:?}"),
/// r#"cd "~/" && TEST="test" "echo" "test""#
/// );
///
/// ```
///
/// ## Conditional Arguments
/// ```
/// # use comlexr_macro::cmd;
/// let include_arg = true;
///
/// let command = cmd!("echo", "test", if include_arg => "optional_arg");
/// assert_eq!(format!("{command:?}"), r#""echo" "test" "optional_arg""#.to_string());
/// ```
///
/// ## Conditional Pattern Matching
/// ```
/// # use comlexr_macro::cmd;
/// let single_option = Some("single");
/// let multi_option: Option<&str> = None;
///
/// let command = cmd!(
/// "echo",
/// "test",
/// if let Some(arg) = single_option => arg,
/// if let Some(arg) = multi_option => [
/// "multi",
/// arg,
/// ],
/// );
/// assert_eq!(format!("{command:?}"), r#""echo" "test" "single""#.to_string());
/// ```
///
/// ## Iterative Argument Inclusion
/// ```
/// # use comlexr_macro::cmd;
/// let args = &["arg1", "arg2"];
///
/// let command = cmd!("echo", for args);
/// assert_eq!(format!("{command:?}"), r#""echo" "arg1" "arg2""#.to_string());
/// ```
///
/// ## Iteration with `for in`
/// ```
/// # use comlexr_macro::cmd;
/// let single_iter = &["arg1", "arg2"];
/// let multi_iter = &["multi1", "multi2"];
///
/// let command = cmd!(
/// "echo",
/// "test",
/// for arg in single_iter => arg,
/// for arg in multi_iter => [
/// "multi",
/// arg,
/// ],
/// );
/// assert_eq!(format!("{command:?}"), r#""echo" "test" "arg1" "arg2" "multi" "multi1" "multi" "multi2""#.to_string());
/// ```
///
/// ## Match Statements
/// ```
/// # use comlexr_macro::cmd;
/// enum TestArgs {
/// Arg1,
/// Arg2,
/// Arg3,
/// }
///
/// let match_arg = TestArgs::Arg2;
/// let command = cmd!(
/// "echo",
/// "test",
/// match match_arg {
/// TestArgs::Arg1 => "arg1",
/// TestArgs::Arg2 => ["arg1", "arg2"],
/// TestArgs::Arg3 => ["arg1", "arg2", "arg3"],
/// }
/// );
/// assert_eq!(format!("{command:?}"), r#""echo" "test" "arg1" "arg2""#.to_string());
/// ```
///
/// ## Dynamic Closures
/// ```
/// # use comlexr_macro::cmd;
/// let numbers = vec![1, 2, 3];
/// let multiplier = 2;
///
/// let command = cmd!("echo", || numbers.into_iter().map(|n| format!("{}", n * multiplier)));
/// assert_eq!(format!("{command:?}"), r#""echo" "2" "4" "6""#.to_string());
/// ```
#[proc_macro]
pub fn cmd(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let command = parse_macro_input!(input as cmd::Command);
quote! { #command }.into()
}
/// Chain the stdout of commands to stdin. Execution is lazy so commands aren't run until `status()` or `output()` is called.
///
/// ```ignore
/// use comlexr::{pipe, cmd};
///
/// let dir = tempfile::tempdir().unwrap();
/// let file = dir.path().join("out");
/// let mut pipe = pipe!(cmd!("echo", "test") | cmd!("sed", "s|e|oa|") | cmd!("tee", &file));
///
/// let output = pipe.output().unwrap();
/// assert!(output.status.success());
/// assert_eq!(String::from_utf8_lossy(&output.stdout), "toast\n");
/// ```
///
/// Or pass data via stdin.
///
/// NOTE: Data must implement `AsRef<[u8]>`.
///
/// ```ignore
/// use comlexr::{pipe, cmd};
///
/// let mut pipe = pipe!(stdin = "test"; cmd!("sed", "s|e|oa|"));
///
/// let output = pipe.output().unwrap();
/// assert!(output.status.success());
/// assert_eq!(String::from_utf8_lossy(&output.stdout), "toast");
/// ```
#[proc_macro]
pub fn pipe(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let pipe = parse_macro_input!(input as pipe::Pipe);
quote! { #pipe }.into()
}

13
macro/src/macros.rs Normal file
View File

@@ -0,0 +1,13 @@
macro_rules! enum_to_tokens {
($enum:ident: $($variant:ident),*) => {
impl ToTokens for $enum {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
tokens.extend(match self {
$(Self::$variant(variant) => quote! { #variant },)*
});
}
}
};
}
pub(crate) use enum_to_tokens;

174
macro/src/pipe.rs Normal file
View File

@@ -0,0 +1,174 @@
use quote::{quote, ToTokens};
use syn::{
parse::{discouraged::Speculative, Parse},
punctuated::Punctuated,
spanned::Spanned,
Error, Token,
};
use crate::{cmd::Value, macros::enum_to_tokens};
pub struct Pipe {
stdin: Option<Value>,
commands: Punctuated<CommandExpr, Token![|]>,
}
impl Parse for Pipe {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let stdin = match input.cursor().ident() {
Some((ident, _)) if ident == "stdin" && input.peek2(Token![=]) => {
_ = input.parse::<syn::Ident>()?;
_ = input.parse::<Token![=]>()?;
let stdin = input.parse()?;
_ = input.parse::<Token![;]>()?;
Some(stdin)
}
_ => None,
};
let commands = Punctuated::parse_separated_nonempty(input)?;
if commands.is_empty() {
return Err(Error::new_spanned(
commands,
"At least one command is required",
));
}
Ok(Self { stdin, commands })
}
}
impl ToTokens for Pipe {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self { stdin, commands } = self;
let command_count = commands.len();
let last_command_span = commands.last().unwrap().span();
let mut commands_iter = commands.iter();
let first_command = commands_iter.next().unwrap();
let initial = quote! {
let mut _c_0 = #first_command;
_c_0.stdout(::std::process::Stdio::piped());
_c_0.stdin(::std::process::Stdio::piped());
let mut _child_0 = _c_0.spawn()?;
if let Some(stdin) = stdin {
_child_0
.stdin
.as_mut()
.ok_or(::comlexr::ExecutorError::NoStdIn)?
.write_all(stdin.as_ref())?;
}
};
let stdin = stdin.as_ref().map_or_else(
|| {
quote! {
None::<&[u8]>
}
},
|stdin| {
quote! {
Some(#stdin)
}
},
);
let commands = commands_iter.enumerate().map(|(index, command)| {
let previous_span = if index == 0 {
first_command.span()
} else {
commands[index - 1].span()
};
let prev_com_ident = syn::Ident::new(&format!("_c_{index}"), previous_span);
let prev_child_ident = syn::Ident::new(&format!("_child_{index}"), previous_span);
let com_ident = syn::Ident::new(&format!("_c_{}", index + 1), command.span());
let child_ident = syn::Ident::new(&format!("_child_{}", index + 1), command.span());
quote! {
let _output = #prev_child_ident.wait_with_output()?;
if !_output.status.success() {
return Err(::comlexr::ExecutorError::FailedCommand{
command: #prev_com_ident,
exit_code: _output.status.code().unwrap_or(1),
});
}
let mut #com_ident = #command;
#com_ident.stdout(::std::process::Stdio::piped());
#com_ident.stdin(::std::process::Stdio::piped());
let mut #child_ident = #com_ident.spawn()?;
#child_ident
.stdin
.as_mut()
.ok_or(::comlexr::ExecutorError::NoStdIn)?
.write_all(&_output.stdout)?;
}
});
let last_child_ident =
syn::Ident::new(&format!("_child_{}", command_count - 1), last_command_span);
tokens.extend(quote! {
::comlexr::Executor::new(
#stdin,
|stdin| -> ::std::result::Result<
::std::process::Child,
::comlexr::ExecutorError,
> {
use ::std::io::Write;
#initial
#(#commands)*
Ok(#last_child_ident)
}
)
});
}
}
enum CommandExpr {
Macro(syn::ExprMacro),
Reference(syn::ExprReference),
Function(syn::ExprCall),
Block(syn::ExprBlock),
}
impl Parse for CommandExpr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let fork = input.fork();
fork.parse()
.map(|value| {
input.advance_to(&fork);
Self::Macro(value)
})
.or_else(|_| {
let fork = input.fork();
let value = fork.parse()?;
input.advance_to(&fork);
Ok(Self::Function(value))
})
.or_else(|_: syn::Error| {
let fork = input.fork();
let value = fork.parse()?;
input.advance_to(&fork);
Ok(Self::Block(value))
})
.or_else(|_: syn::Error| {
let fork = input.fork();
let value = fork.parse()?;
input.advance_to(&fork);
Ok(Self::Reference(value))
})
.map_err(|_: syn::Error| {
syn::Error::new(
input.span(),
"Only references, function calls, macro calls, and blocks are allowed",
)
})
}
}
enum_to_tokens! {CommandExpr: Macro, Reference, Function, Block}