feat: Pipe
This commit is contained in:
26
macro/Cargo.toml
Normal file
26
macro/Cargo.toml
Normal 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
522
macro/src/cmd.rs
Normal 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
200
macro/src/lib.rs
Normal 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
13
macro/src/macros.rs
Normal 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
174
macro/src/pipe.rs
Normal 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}
|
||||
Reference in New Issue
Block a user