3 Commits

Author SHA1 Message Date
6fa4b21c4f chore: Release comlexr version 1.2.0 2025-01-14 15:55:33 -05:00
Gerald Pinder
fc55ab350d Merge branch 'add-env-cd' into 'main'
feat: Add the ability to set current dir and env vars

See merge request wunker-bunker/comlexr!2
2025-01-14 20:53:54 +00:00
gerald.pinder
403c7be922 feat: Add the ability to set current dir and env vars 2025-01-14 15:39:52 -05:00
8 changed files with 283 additions and 21 deletions

View File

@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [1.2.0] - 2025-01-14
### 🚀 Features
- Add the ability to set current dir and env vars
## [1.1.0] - 2025-01-11 ## [1.1.0] - 2025-01-11
### 🚀 Features ### 🚀 Features
@@ -18,5 +24,6 @@ All notable changes to this project will be documented in this file.
- Add git hooks - Add git hooks
- Remove earthly check in release just script - Remove earthly check in release just script
- Add CHANGELOG.md - Add CHANGELOG.md
- Release comlexr version 1.1.0
<!-- generated by git-cliff --> <!-- generated by git-cliff -->

2
Cargo.lock generated
View File

@@ -34,7 +34,7 @@ dependencies = [
[[package]] [[package]]
name = "comlexr" name = "comlexr"
version = "1.1.0" version = "1.2.0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -2,8 +2,9 @@
name = "comlexr" name = "comlexr"
description = "Dynamically build Command objects with conditional expressions" description = "Dynamically build Command objects with conditional expressions"
repository = "https://gitlab.com/wunker-bunker/comlexr" repository = "https://gitlab.com/wunker-bunker/comlexr"
version = "1.1.0" version = "1.2.0"
edition = "2018" edition = "2021"
rust-version = "1.60"
license = "MIT" license = "MIT"
[lib] [lib]
@@ -12,7 +13,7 @@ proc-macro = true
[dependencies] [dependencies]
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "2", features = ["full"] } syn = { version = "2", features = ["full", "derive"] }
[dev-dependencies] [dev-dependencies]
rstest = "0.24" rstest = "0.24"

View File

@@ -8,7 +8,7 @@ Add `comlexr` to your project's `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
comlexr = "1.1.0" comlexr = "1.2.0"
``` ```
### Rust Edition ### Rust Edition
@@ -150,6 +150,62 @@ let command = cmd!(
assert_eq!(format!("{command:?}"), r#""echo" "test" "2" "4" "6""#.to_string()); 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 ## Features
- Conditional expressions (`if`, `if let`) - Conditional expressions (`if`, `if let`)
- Iteration constructs (`for`, `for in`) - Iteration constructs (`for`, `for in`)

View File

@@ -46,7 +46,7 @@ command = [
env.RUSTFLAGS="-Zmacro-backtrace" env.RUSTFLAGS="-Zmacro-backtrace"
need_stdout = true need_stdout = true
default_watch = false default_watch = false
watch = ["src", "Cargo.toml", "tests"] watch = ["src", "Cargo.toml", "tests", "README.md"]
[jobs.test-all] [jobs.test-all]
command = [ command = [
@@ -56,7 +56,7 @@ command = [
env.RUSTFLAGS="-Zmacro-backtrace" env.RUSTFLAGS="-Zmacro-backtrace"
need_stdout = true need_stdout = true
default_watch = false default_watch = false
watch = ["src", "Cargo.toml", "tests"] watch = ["src", "Cargo.toml", "tests", "README.md"]
[jobs.doc] [jobs.doc]
command = ["cargo", "doc", "--color", "always", "--no-deps"] command = ["cargo", "doc", "--color", "always", "--no-deps"]

View File

@@ -7,22 +7,30 @@ use syn::{
}; };
pub struct Command { pub struct Command {
cd: CurrentDir,
env_vars: EnvVars,
program: Value, program: Value,
args: Option<Punctuated<LogicArg, Token![,]>>, args: Option<Punctuated<LogicArg, Token![,]>>,
} }
impl Parse for Command { impl Parse for Command {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let cd = input.parse()?;
let env_vars = input.parse()?;
let program = input.parse()?; let program = input.parse()?;
if input.is_empty() { if input.is_empty() {
Ok(Self { Ok(Self {
cd,
env_vars,
program, program,
args: None, args: None,
}) })
} else { } else {
_ = input.parse::<Token![,]>()?; _ = input.parse::<Token![,]>()?;
Ok(Self { Ok(Self {
cd,
env_vars,
program, program,
args: Some(Punctuated::parse_terminated(input)?), args: Some(Punctuated::parse_terminated(input)?),
}) })
@@ -32,22 +40,27 @@ impl Parse for Command {
impl ToTokens for Command { impl ToTokens for Command {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 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 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( tokens.extend(quote! {
|| quote! { #program }, {
|args| { let mut _c = #program;
quote! { #cd
{ #env_vars
let mut _c = #program; #(#args)*
#(#args)* _c
_c }
} });
}
},
));
} }
} }
@@ -400,6 +413,99 @@ impl ToTokens for SingleArg {
} }
} }
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);)* });
}
}
enum Value { enum Value {
Lit(syn::Lit), Lit(syn::Lit),
Ident(syn::Ident), Ident(syn::Ident),

View File

@@ -28,6 +28,57 @@ mod cmd;
/// assert_eq!(format!("{command:?}"), r#""echo" "test""#.to_string()); /// 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 /// ## Conditional Arguments
/// ``` /// ```
/// use comlexr::cmd; /// use comlexr::cmd;

View File

@@ -11,6 +11,47 @@ fn expression() {
assert_eq!(format!("{command:?}"), r#""echo" "test""#.to_string()); 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] #[rstest]
#[case(false, false, r#""echo" "test""#)] #[case(false, false, r#""echo" "test""#)]
#[case(true, false, r#""echo" "test" "single""#)] #[case(true, false, r#""echo" "test" "single""#)]