diff --git a/Cargo.lock b/Cargo.lock index 2393fcf..09ea70e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,135 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "comlexr" version = "0.1.0" dependencies = [ "proc-macro2", "quote", + "rstest", "syn", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -29,6 +149,95 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "syn" version = "2.0.96" @@ -40,8 +249,34 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "winnow" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index f8dc816..6bb3291 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,24 @@ name = "comlexr" version = "0.1.0" edition = "2021" +[lib] +proc-macro = true + [dependencies] -proc-macro2 = "1.0.92" -quote = "1.0.38" -syn = { version = "2.0.96", features = ["full"] } +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full"] } + +[dev-dependencies] +rstest = "0.24" + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +correctness = "deny" +suspicious = "deny" +perf = "deny" +style = "deny" +nursery = "deny" +pedantic = "deny" diff --git a/bacon.toml b/bacon.toml new file mode 100644 index 0000000..b2fffcd --- /dev/null +++ b/bacon.toml @@ -0,0 +1,83 @@ +# This is a configuration file for the bacon tool +# +# Bacon repository: https://github.com/Canop/bacon +# Complete help on configuration: https://dystroy.org/bacon/config/ +# You can also check bacon's own bacon.toml file +# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml + +default_job = "clippy-all" + +[jobs.check] +command = ["cargo", "check", "--color", "always"] +need_stdout = false +default_watch = false +watch = ["src", "Cargo.toml"] + +[jobs.check-all] +command = ["cargo", "check", "--all-features", "--color", "always"] +need_stdout = false +default_watch = false +watch = ["src", "Cargo.toml"] + +[jobs.clippy] +command = [ + "cargo", "clippy", + "--color", "always", +] +need_stdout = false +default_watch = false +watch = ["src", "Cargo.toml"] + +[jobs.clippy-all] +command = [ + "cargo", "clippy", + "--all-features", + "--color", "always", +] +need_stdout = false +default_watch = false +watch = ["src", "Cargo.toml"] + +[jobs.test] +command = [ + "cargo", "+nightly", "test", "--color", "always", "--workspace", + "--", "--color", "always", +] +env.RUSTFLAGS="-Zmacro-backtrace" +need_stdout = true +default_watch = false +watch = ["src", "Cargo.toml", "tests"] + +[jobs.test-all] +command = [ + "cargo", "+nightly", "test", "--all-features", "--color", "always", "--workspace", + "--", "--color", "always" +] +env.RUSTFLAGS="-Zmacro-backtrace" +need_stdout = true +default_watch = false +watch = ["src", "Cargo.toml", "tests"] + +[jobs.doc] +command = ["cargo", "doc", "--color", "always", "--no-deps"] +need_stdout = false +default_watch = false +watch = ["src", "Cargo.toml"] + +# If the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +# You may define here keybindings that would be specific to +# a project, for example a shortcut to launch a specific job. +# Shortcuts to internal functions (scrolling, toggling, etc.) +# should go in your personal global prefs.toml file instead. +[keybindings] +# alt-m = "job:my-job" +c = "job:clippy" +shift-c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target +t = "job:test" +shift-t = "job:test-all" diff --git a/src/lib.rs b/src/lib.rs index 9630031..609632c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ -use syn::{bracketed, parse::Parse, punctuated::Punctuated, token, Token}; +use quote::{quote, ToTokens}; +use syn::{bracketed, parse::Parse, parse_macro_input, punctuated::Punctuated, token, Token}; struct Command { - program: Program, + program: Value, args: Option>, } @@ -10,119 +11,257 @@ impl Parse for Command { let program = input.parse()?; if input.is_empty() { - Ok(Command { + Ok(Self { program, args: None, }) } else { _ = input.parse::()?; - Ok(Command { + Ok(Self { program, - args: Some(input.parse_terminated(Arg::parse, Token![,])?), + args: Some(Punctuated::parse_terminated(input)?), }) } } } -struct Program(syn::LitStr); +impl ToTokens for Command { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { program, args } = self; + let program = quote! { ::std::process::Command::new(#program) }; + let args = args.as_ref().map(|args| args.iter()); -impl Parse for Program { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - input.parse().map(Self) + tokens.extend(args.map_or_else( + || quote! { #program }, + |args| { + quote! { + { + let mut command = #program; + #(#args)* + command + } + } + }, + )); } } enum Arg { - Expression(syn::Expr), - ForIter { - for_key: Token![for], - expr: syn::Expr, - }, - ForInIterMulti { - for_iter: ForIter, - arrow_token: Token![=>], - args: MultiArg, - }, - ForInIterSingle { - for_iter: ForIter, - arrow_token: Token![=>], - arg: SingleArg, - }, - IfLetMulti { - if_let: IfLet, - arrow_token: Token![=>], - args: MultiArg, - }, - IfLetSingle { - if_let: IfLet, - arrow_token: Token![=>], - arg: SingleArg, - }, - IfMulti { - expr: syn::Expr, - arrow_token: Token![=>], - args: MultiArg, - }, - IfSingle { - expr: syn::Expr, - arrow_token: Token![=>], - arg: SingleArg, - }, + Expr(SingleArg), + ForIter(ForIter), + ForIn(ForIn), + IfLet(IfLet), + If(If), } impl Parse for Arg { fn parse(input: syn::parse::ParseStream) -> syn::Result { - input.parse() + if input.peek(Token![for]) { + if input.peek3(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 { + input.parse().map(Self::Expr) + } } } -struct Pattern(syn::Pat); - -impl Parse for Pattern { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - input.call(syn::Pat::parse_single).map(Self) +impl ToTokens for Arg { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(match self { + Self::Expr(expr) => quote! { #expr }, + Self::ForIter(for_iter) => quote! { #for_iter }, + Self::ForIn(for_in) => quote! { #for_in }, + Self::IfLet(if_let) => quote! { #if_let }, + Self::If(if_) => quote! { #if_ }, + }); } } struct ForIter { - for_token: Token![for], - pattern: Pattern, - in_token: Token![in], - iter: syn::Expr, + expr: Value, } impl Parse for ForIter { fn parse(input: syn::parse::ParseStream) -> syn::Result { - Ok(ForIter { - for_token: input.parse()?, - pattern: input.parse()?, - in_token: input.parse()?, - iter: input.parse()?, + _ = input.parse::()?; + 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 arg in #expr { + command.arg(arg); + } + }); + } +} + +struct ForIn { + pattern: syn::Pat, + iter: Value, + args: SingleMultiArg, +} + +impl Parse for ForIn { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + _ = input.parse::()?; + let pattern = input.call(syn::Pat::parse_single)?; + _ = input.parse::()?; + let iter = input.parse()?; + _ = input.parse::]>()?; + 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 { - if_token: Token![if], - let_token: Token![let], - pattern: Pattern, - eq_token: Token![=], - expr: syn::Expr, + pattern: syn::Pat, + expr: Value, + args: SingleMultiArg, } impl Parse for IfLet { fn parse(input: syn::parse::ParseStream) -> syn::Result { - Ok(IfLet { - if_token: input.parse()?, - let_token: input.parse()?, - pattern: input.parse()?, - eq_token: input.parse()?, - expr: input.parse()?, + _ = input.parse::()?; + _ = input.parse::()?; + let pattern = input.call(syn::Pat::parse_single)?; + _ = input.parse::()?; + let expr = input.parse()?; + _ = input.parse::]>()?; + let args = input.parse()?; + + Ok(Self { + pattern, + expr, + args, }) } } -struct SingleArg(syn::Expr); +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: SingleMultiArg, +} + +impl Parse for If { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + _ = input.parse::()?; + let expr = input.parse()?; + _ = input.parse::]>()?; + 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 + } + }); + } +} + +enum SingleMultiArg { + Single(SingleArg), + Multi(MultiArg), +} + +impl Parse for SingleMultiArg { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + if input.peek(token::Bracket) { + input.parse().map(Self::Multi) + } else { + input.parse().map(Self::Single) + } + } +} + +impl ToTokens for SingleMultiArg { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(match self { + Self::Single(arg) => quote! { #arg }, + Self::Multi(args) => quote! { #args }, + }); + } +} + +struct MultiArg(Punctuated); + +impl Parse for MultiArg { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + 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::>(); + + tokens.extend(quote! { #(#args)* }); + } +} + +struct SingleArg(Value); impl Parse for SingleArg { fn parse(input: syn::parse::ParseStream) -> syn::Result { @@ -130,17 +269,45 @@ impl Parse for SingleArg { } } -struct MultiArg { - bracket: token::Bracket, - args: Punctuated, -} - -impl Parse for MultiArg { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let args; - Ok(MultiArg { - bracket: bracketed!(args in input), - args: input.parse_terminated(SingleArg::parse, Token![,])?, - }) +impl ToTokens for SingleArg { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let arg = &self.0; + tokens.extend(quote! { + command.arg(#arg); + }); } } + +enum Value { + Lit(syn::Lit), + Ident(syn::Ident), + Expr(syn::Expr), +} + +impl Parse for Value { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + if input.peek(syn::Lit) { + input.parse().map(Self::Lit) + } else if input.peek(syn::Ident) { + input.parse().map(Self::Ident) + } else { + input.parse().map(Self::Expr) + } + } +} + +impl ToTokens for Value { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(match self { + Self::Lit(lit) => quote! { #lit }, + Self::Ident(ident) => quote! { #ident }, + Self::Expr(expr) => quote! { #expr }, + }); + } +} + +#[proc_macro] +pub fn cmd(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let command = parse_macro_input!(input as Command); + quote! { #command }.into() +} diff --git a/tests/cmd.rs b/tests/cmd.rs new file mode 100644 index 0000000..b7cc246 --- /dev/null +++ b/tests/cmd.rs @@ -0,0 +1,79 @@ +use comlexr::cmd; +use rstest::rstest; + +#[test] +fn expression() { + let command = cmd!("echo", "test"); + + assert_eq!(format!("{command:?}"), r#""echo" "test""#.to_string()); +} + +#[rstest] +#[case(false, false, r#""echo" "test""#)] +#[case(true, false, r#""echo" "test" "single""#)] +#[case(false, true, r#""echo" "test" "multi" "arg""#)] +#[case(true, true, r#""echo" "test" "single" "multi" "arg""#)] +fn if_statement(#[case] single: bool, #[case] multi: bool, #[case] expected: &str) { + let command = cmd!( + "echo", + "test", + if single => "single", + if multi => [ + "multi", + "arg", + ], + ); + + assert_eq!(format!("{command:?}"), expected.to_string()); +} + +#[rstest] +#[case(&[], r#""echo" "test""#)] +#[case(&["1", "2"], r#""echo" "test" "1" "2""#)] +fn for_iter(#[case] iter: &[&str], #[case] expected: &str) { + let command = cmd!( + "echo", + "test", + for iter, + ); + + assert_eq!(format!("{command:?}"), expected.to_string()); +} + +#[rstest] +#[case(&[], &[], r#""echo" "test""#)] +#[case(&["1", "2"], &[], r#""echo" "test" "1" "2""#)] +#[case(&[], &["3", "4"], r#""echo" "test" "multi" "3" "multi" "4""#)] +#[case(&["1", "2"], &["3", "4"], r#""echo" "test" "1" "2" "multi" "3" "multi" "4""#)] +fn for_in(#[case] single_iter: &[&str], #[case] multi_iter: &[&str], #[case] expected: &str) { + let command = cmd!( + "echo", + "test", + for arg in single_iter => arg, + for arg in multi_iter => [ + "multi", + arg, + ], + ); + + assert_eq!(format!("{command:?}"), expected.to_string()); +} + +#[rstest] +#[case(None, None, r#""echo" "test""#)] +#[case(Some("arg"), None, r#""echo" "test" "arg""#)] +#[case(None, Some("arg"), r#""echo" "test" "multi" "arg""#)] +#[case(Some("1"), Some("2"), r#""echo" "test" "1" "multi" "2""#)] +fn if_let(#[case] single: Option<&str>, #[case] multi: Option<&str>, #[case] expected: &str) { + let command = cmd!( + "echo", + "test", + if let Some(arg) = single => arg, + if let Some(arg) = multi => [ + "multi", + arg, + ], + ); + + assert_eq!(format!("{command:?}"), expected.to_string()); +}