diff --git a/Cargo.toml b/Cargo.toml index e8bf3d9..ae6540f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ name = "comlexr" description = "Dynamically build Command objects with conditional expressions" repository = "https://gitlab.com/wunker-bunker/comlexr" version = "1.1.0" -edition = "2018" +edition = "2021" +rust-version = "1.60" license = "MIT" [lib] @@ -12,7 +13,7 @@ proc-macro = true [dependencies] proc-macro2 = "1" quote = "1" -syn = { version = "2", features = ["full"] } +syn = { version = "2", features = ["full", "derive"] } [dev-dependencies] rstest = "0.24" diff --git a/README.md b/README.md index 6747ac7..7c0c028 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,62 @@ let command = cmd!( assert_eq!(format!("{command:?}"), r#""echo" "test" "2" "4" "6""#.to_string()); ``` +### Set Current Directory +Specify the directory to run the command. + +```rust +use comlexr::cmd; + +let command = cmd!( + cd "~/"; + "echo", + "test", +); + +assert_eq!(format!("{command:?}"), r#"cd "~/" && "echo" "test""#); +``` + +### Setting Environment Variables +Set environment variables for the command. + +```rust +use comlexr::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 Env Variable Order Matters +Environment variable declarations **MUST** come after the current directory declaration. + +```rust +use comlexr::cmd; + +let command = cmd!( + cd "~/"; + env { + "TEST": "test", + }; + "echo", + "test", +); + +assert_eq!( + format!("{command:?}"), + r#"cd "~/" && TEST="test" "echo" "test""# +); +``` + ## Features - Conditional expressions (`if`, `if let`) - Iteration constructs (`for`, `for in`) diff --git a/bacon.toml b/bacon.toml index b2fffcd..6810121 100644 --- a/bacon.toml +++ b/bacon.toml @@ -46,7 +46,7 @@ command = [ env.RUSTFLAGS="-Zmacro-backtrace" need_stdout = true default_watch = false -watch = ["src", "Cargo.toml", "tests"] +watch = ["src", "Cargo.toml", "tests", "README.md"] [jobs.test-all] command = [ @@ -56,7 +56,7 @@ command = [ env.RUSTFLAGS="-Zmacro-backtrace" need_stdout = true default_watch = false -watch = ["src", "Cargo.toml", "tests"] +watch = ["src", "Cargo.toml", "tests", "README.md"] [jobs.doc] command = ["cargo", "doc", "--color", "always", "--no-deps"] diff --git a/src/cmd.rs b/src/cmd.rs index 9b6fed4..bcad7be 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -7,22 +7,30 @@ use syn::{ }; pub struct Command { + cd: CurrentDir, + env_vars: EnvVars, program: Value, args: Option>, } impl Parse for Command { fn parse(input: syn::parse::ParseStream) -> syn::Result { + 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::()?; Ok(Self { + cd, + env_vars, program, args: Some(Punctuated::parse_terminated(input)?), }) @@ -32,22 +40,27 @@ impl Parse for Command { impl ToTokens for Command { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let Self { program, args } = self; + let Self { + cd, + env_vars, + program, + args, + } = self; let program = quote! { ::std::process::Command::new(#program) }; - let args = args.as_ref().map(Punctuated::iter); + let args = args + .as_ref() + .map(Punctuated::iter) + .map_or_else(Vec::new, Iterator::collect); - tokens.extend(args.map_or_else( - || quote! { #program }, - |args| { - quote! { - { - let mut _c = #program; - #(#args)* - _c - } - } - }, - )); + tokens.extend(quote! { + { + let mut _c = #program; + #cd + #env_vars + #(#args)* + _c + } + }); } } @@ -400,6 +413,99 @@ impl ToTokens for SingleArg { } } +struct EnvVars(Option>); + +impl Parse for EnvVars { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let fork = input.fork(); + let ident = fork.cursor().ident(); + + match ident { + Some((ident, _)) if ident == "env" => { + _ = fork.parse::()?; + let envs; + braced!(envs in fork); + Punctuated::parse_terminated(&envs) + .and_then(|envs| { + _ = fork.parse::()?; + 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 { + let key = input.parse()?; + _ = input.parse::()?; + 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); + +impl Parse for CurrentDir { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let fork = input.fork(); + let ident = fork.cursor().ident(); + + match ident { + Some((ident, _)) if ident == "cd" => { + _ = fork.parse::(); + fork.parse() + .and_then(|value| { + _ = fork.parse::()?; + 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);)* }); + } +} + enum Value { Lit(syn::Lit), Ident(syn::Ident), diff --git a/src/lib.rs b/src/lib.rs index 5464b4a..501ffa1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,57 @@ mod cmd; /// assert_eq!(format!("{command:?}"), r#""echo" "test""#.to_string()); /// ``` /// +/// ## Current Directory +/// ``` +/// use comlexr::cmd; +/// +/// let command = cmd!( +/// cd "~/"; +/// "echo", +/// "test", +/// ); +/// +/// assert_eq!(format!("{command:?}"), r#"cd "~/" && "echo" "test""#); +/// ``` +/// +/// ## Environment Vars +/// ``` +/// use comlexr::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::cmd; +/// +/// let command = cmd!( +/// cd "~/"; +/// env { +/// "TEST": "test", +/// }; +/// "echo", +/// "test", +/// ); +/// +/// assert_eq!( +/// format!("{command:?}"), +/// r#"cd "~/" && TEST="test" "echo" "test""# +/// ); +/// +/// ``` +/// /// ## Conditional Arguments /// ``` /// use comlexr::cmd; diff --git a/tests/cmd.rs b/tests/cmd.rs index 5551769..ba4bc41 100644 --- a/tests/cmd.rs +++ b/tests/cmd.rs @@ -11,6 +11,47 @@ fn expression() { assert_eq!(format!("{command:?}"), r#""echo" "test""#.to_string()); } +#[test] +fn current_dir() { + let command = cmd!( + cd "~/"; + "echo", + "test", + ); + + assert_eq!(format!("{command:?}"), r#"cd "~/" && "echo" "test""#); +} + +#[test] +fn env_vars() { + let command = cmd!( + env { + "TEST": "test", + }; + "echo", + "test", + ); + + assert_eq!(format!("{command:?}"), r#"TEST="test" "echo" "test""#); +} + +#[test] +fn cd_env_vars() { + let command = cmd!( + cd "~/"; + env { + "TEST": "test", + }; + "echo", + "test", + ); + + assert_eq!( + format!("{command:?}"), + r#"cd "~/" && TEST="test" "echo" "test""# + ); +} + #[rstest] #[case(false, false, r#""echo" "test""#)] #[case(true, false, r#""echo" "test" "single""#)]