19 Commits

Author SHA1 Message Date
Dependabot
5b9f8d32d4 chore(deps): Bump syn from 2.0.100 to 2.0.104 2025-07-07 19:47:50 -04:00
Dependabot
697b412a68 chore(deps): Bump proc-macro2 from 1.0.94 to 1.0.95 2025-07-07 19:46:37 -04:00
Dependabot
e0543c0d41 chore(deps): Bump tempfile from 3.19.1 to 3.20.0 2025-07-07 19:44:50 -04:00
Dependabot
f5993a72ec chore(deps): Bump thiserror from 1.0.69 to 2.0.12 2025-07-07 19:43:38 -04:00
Dependabot
0c02a26b5d chore(deps): Bump rstest from 0.24.0 to 0.25.0 2025-07-07 23:41:21 +00:00
720f1fd358 chore: Add dependabot config 2025-07-07 19:40:04 -04:00
66166b47f0 chore: Release 2025-05-18 12:53:54 -04:00
b72ca67302 chore: Clippy fixes 2025-05-18 12:53:36 -04:00
2f5575d6ef chore: Release 2025-04-06 15:06:45 -04:00
da51c0b41e chore: Upgrade deps 2025-04-06 15:06:13 -04:00
0cdd0bbc07 chore: Release 2025-04-06 15:05:33 -04:00
a5d442f7ff feat: Default env variables 2025-04-06 14:21:17 -04:00
ac948f193b chore: Release 2025-01-30 22:02:21 -05:00
2d7d6cf641 fix: Better enforce typing for commands 2025-01-30 22:02:07 -05:00
c58cbff4b2 chore: Release 2025-01-30 21:34:21 -05:00
9d944c3218 feat: Pipe 2025-01-30 21:04:54 -05:00
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
18 changed files with 1338 additions and 232 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target /target
/expand.rs /expand.rs
/.sccache

3
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,3 @@
include:
- project: wunker-bunker/ci-pipelines
file: cargo-lib.yml

19
.gitlab/dependabot.yml Normal file
View File

@@ -0,0 +1,19 @@
version: 2
update-options:
rebase-strategy:
strategy: auto
on-approval: true
with-assignee: dependabot-wunker-bunker
schedule:
interval: weekly
auto-merge: true
approvers:
- gmpinder
reviewers:
- gmpinder
assignees:
- dependabot-wunker-bunker
open-pull-requests-limit: -1
updates:
- package-ecosystem: cargo
directory: /

6
.helix/languages.toml Normal file
View File

@@ -0,0 +1,6 @@
[language-server.rust-analyzer.config]
cargo.features = "all"
[language-server.rust-analyzer.config.check]
command = "clippy"
args = ["--no-deps", "--workspace"]

View File

@@ -2,6 +2,59 @@
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.4.2] - 2025-05-18
### ⚙️ Miscellaneous Tasks
- Clippy fixes
## [1.4.1] - 2025-04-06
### ⚙️ Miscellaneous Tasks
- Upgrade deps
- Release
## [1.4.0] - 2025-04-06
### 🚀 Features
- Default env variables
### ⚙️ Miscellaneous Tasks
- Release
## [1.3.1] - 2025-01-31
### 🐛 Bug Fixes
- Better enforce typing for commands
### ⚙️ Miscellaneous Tasks
- Release
## [1.3.0] - 2025-01-31
### 🚀 Features
- Pipe
### ⚙️ Miscellaneous Tasks
- Release
## [1.2.0] - 2025-01-14
### 🚀 Features
- Add the ability to set current dir and env vars
### ⚙️ Miscellaneous Tasks
- Release comlexr version 1.2.0
## [1.1.0] - 2025-01-11 ## [1.1.0] - 2025-01-11
### 🚀 Features ### 🚀 Features
@@ -18,5 +71,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 -->

271
Cargo.lock generated
View File

@@ -17,6 +17,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@@ -34,13 +40,23 @@ dependencies = [
[[package]] [[package]]
name = "comlexr" name = "comlexr"
version = "1.1.0" version = "1.4.2"
dependencies = [
"comlexr_macro",
"rstest",
"rusty-hook",
"tempfile",
"thiserror",
]
[[package]]
name = "comlexr_macro"
version = "1.4.2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rstest",
"rusty-hook",
"syn", "syn",
"tempfile",
] ]
[[package]] [[package]]
@@ -55,9 +71,25 @@ dependencies = [
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]] [[package]]
name = "fsio" name = "fsio"
@@ -117,6 +149,18 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "getrandom"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
]
[[package]] [[package]]
name = "glob" name = "glob"
version = "0.3.2" version = "0.3.2"
@@ -147,14 +191,26 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.7.0" version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.15.2", "hashbrown 0.15.2",
] ]
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "linux-raw-sys"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@@ -167,6 +223,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab250442c86f1850815b5d268639dff018c0627022bc1940eb2d642ca1ce12f0" checksum = "ab250442c86f1850815b5d268639dff018c0627022bc1940eb2d642ca1ce12f0"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@@ -181,31 +243,37 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "3.2.0" version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
dependencies = [ dependencies = [
"toml_edit", "toml_edit",
] ]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.93" version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.38" version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.1"
@@ -243,9 +311,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]] [[package]]
name = "rstest" name = "rstest"
version = "0.24.0" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d"
dependencies = [ dependencies = [
"futures-timer", "futures-timer",
"futures-util", "futures-util",
@@ -255,9 +323,9 @@ dependencies = [
[[package]] [[package]]
name = "rstest_macros" name = "rstest_macros"
version = "0.24.0" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"glob", "glob",
@@ -280,6 +348,19 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]] [[package]]
name = "rusty-hook" name = "rusty-hook"
version = "0.11.2" version = "0.11.2"
@@ -294,24 +375,24 @@ dependencies = [
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.24" version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.217" version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.217" version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -329,15 +410,48 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.96" version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "tempfile"
version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.5.11" version = "0.5.11"
@@ -355,20 +469,20 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.22" version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [ dependencies = [
"indexmap 2.7.0", "indexmap 2.9.0",
"toml_datetime", "toml_datetime",
"winnow", "winnow",
] ]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.14" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
@@ -377,10 +491,101 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "winnow" name = "wasi"
version = "0.6.24" version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]

View File

@@ -1,27 +1,21 @@
[package] [workspace]
name = "comlexr" members = ["macro"]
[workspace.package]
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.4.2"
edition = "2018" edition = "2021"
rust-version = "1.60"
license = "MIT" license = "MIT"
[lib] [workspace.dependencies]
proc-macro = true tempfile = "3.20"
[dependencies] [workspace.lints.rust]
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
[dev-dependencies]
rstest = "0.24"
rusty-hook = "0.11"
[lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"
[lints.clippy] [workspace.lints.clippy]
correctness = "deny" correctness = "deny"
suspicious = "deny" suspicious = "deny"
perf = "deny" perf = "deny"
@@ -29,8 +23,34 @@ style = "deny"
nursery = "deny" nursery = "deny"
pedantic = "deny" pedantic = "deny"
[package]
name = "comlexr"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
description.workspace = true
repository.workspace = true
license.workspace = true
[package.metadata.release] [package.metadata.release]
pre-release-hook = ["git", "cliff", "-o", "CHANGELOG.md", "--tag", "{{version}}"] pre-release-hook = ["git", "cliff", "-o", "CHANGELOG.md", "--tag", "{{version}}"]
pre-release-replacements = [ pre-release-replacements = [
{ file = "README.md", search = "comlexr = \"\\d+.\\d+.\\d+\"", replace = "comlexr = \"{{version}}\"" } { file = "README.md", search = "comlexr = \"\\d+.\\d+.\\d+\"", replace = "comlexr = \"{{version}}\"" }
] ]
[package.metadata."docs.rs"]
all-features = true
[dependencies]
comlexr_macro = { version = "=1.4.2", path = "./macro" }
thiserror = "2.0.12"
[dev-dependencies]
rstest = "0.25"
rusty-hook = "0.11"
tempfile.workspace = true
[lints]
workspace = true

113
README.md
View File

@@ -8,11 +8,11 @@ Add `comlexr` to your project's `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
comlexr = "1.1.0" comlexr = "1.4.2"
``` ```
### Rust Edition ## MSRV
This project uses Rust **2018 edition** to ensure compatibility and stable language features. The minimum supported Rust version is `1.60.0` for broader support.
## Usage ## Usage
@@ -150,14 +150,119 @@ 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""#);
```
#### Conditional
You can have a default value set for an environment variable.
```rust
use comlexr::cmd;
const NEW_VAR: &str = "NEW_VAR";
std::env::set_var("TEST", "realvalue");
let command = cmd!(
env {
"TEST":? "test",
NEW_VAR: "new_var"
};
"echo",
"test",
);
assert_eq!(format!("{command:?}"), r#"NEW_VAR="new_var" "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""#
);
```
### Piping commands
When using the `pipe` feature, you can make use of `pipe!` to chain the stdout of commands to stdin. Execution is lazy so commands aren't run until `status()` or `output()` is called.
```rust
use comlexr::{pipe, cmd};
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("out");
let mut pipe = pipe!(cmd!("echo", "test") | cmd!("tee", &file));
let status = pipe.status().unwrap();
assert!(status.success());
```
### Sending variables to stdin
You can also send data to the stdin of the first command in the chain.
```rust
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");
```
## Features ## Features
- Conditional expressions (`if`, `if let`) - Conditional expressions (`if`, `if let`)
- Iteration constructs (`for`, `for in`) - Iteration constructs (`for`, `for in`)
- Pattern matching (`match`) - Pattern matching (`match`)
- Support for closures and dynamic expressions - Support for closures and dynamic expressions
- Piping stdout from one command to the stdin of another
## Examples ## Examples
See the [tests](./tests/) directory for more examples on how to use `comlexr` effectively in your project. See the [tests](./tests/) directory for more examples on how to use `comlexr` effectively in your project.
## License ## License
This project is licensed under the [MIT License](LICENSE). This project is licensed under the [MIT License](./LICENSE).

View File

@@ -11,32 +11,32 @@ default_job = "clippy-all"
command = ["cargo", "check", "--color", "always"] command = ["cargo", "check", "--color", "always"]
need_stdout = false need_stdout = false
default_watch = false default_watch = false
watch = ["src", "Cargo.toml"] watch = ["src", "Cargo.toml", "macro"]
[jobs.check-all] [jobs.check-all]
command = ["cargo", "check", "--all-features", "--color", "always"] command = ["cargo", "check", "--all-features", "--color", "always"]
need_stdout = false need_stdout = false
default_watch = false default_watch = false
watch = ["src", "Cargo.toml"] watch = ["src", "Cargo.toml", "macro"]
[jobs.clippy] [jobs.clippy]
command = [ command = [
"cargo", "clippy", "cargo", "clippy", "--workspace",
"--color", "always", "--color", "always",
] ]
need_stdout = false need_stdout = false
default_watch = false default_watch = false
watch = ["src", "Cargo.toml"] watch = ["src", "Cargo.toml", "macro"]
[jobs.clippy-all] [jobs.clippy-all]
command = [ command = [
"cargo", "clippy", "cargo", "clippy", "--workspace",
"--all-features", "--all-features",
"--color", "always", "--color", "always",
] ]
need_stdout = false need_stdout = false
default_watch = false default_watch = false
watch = ["src", "Cargo.toml"] watch = ["src", "Cargo.toml", "macro"]
[jobs.test] [jobs.test]
command = [ command = [
@@ -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", "macro"]
[jobs.test-all] [jobs.test-all]
command = [ command = [
@@ -56,18 +56,18 @@ 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", "macro"]
[jobs.doc] [jobs.doc]
command = ["cargo", "doc", "--color", "always", "--no-deps"] command = ["cargo", "doc", "--color", "always", "--no-deps", "--workspace", "--all-features"]
need_stdout = false need_stdout = false
default_watch = false default_watch = false
watch = ["src", "Cargo.toml"] watch = ["src", "Cargo.toml", "macro"]
# If the doc compiles, then it opens in your browser and bacon switches # If the doc compiles, then it opens in your browser and bacon switches
# to the previous job # to the previous job
[jobs.doc-open] [jobs.doc-open]
command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] command = ["cargo", "doc", "--color", "always", "--no-deps", "--open", "--workspace", "--all-features"]
need_stdout = false need_stdout = false
on_success = "back" # so that we don't open the browser at each change on_success = "back" # so that we don't open the browser at each change

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

View File

@@ -6,23 +6,33 @@ use syn::{
token, Token, token, Token,
}; };
use crate::macros::enum_to_tokens;
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 +42,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| {
quote! {
{ {
let mut _c = #program; let mut _c = #program;
#cd
#env_vars
#(#args)* #(#args)*
_c _c
} }
} });
},
));
} }
} }
@@ -88,19 +103,7 @@ impl Parse for LogicArg {
} }
} }
impl ToTokens for LogicArg { enum_to_tokens! {LogicArg: Expr, ForIter, ForIn, IfLet, If, Match, Closure}
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_ },
Self::Match(match_) => quote! { #match_ },
Self::Closure(closure) => quote! { #closure },
});
}
}
struct ForIter { struct ForIter {
expr: Value, expr: Value,
@@ -355,14 +358,7 @@ impl Parse for Arguments {
} }
} }
impl ToTokens for Arguments { enum_to_tokens! {Arguments: Single, Multi}
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<SingleArg, Token![,]>); struct MultiArg(Punctuated<SingleArg, Token![,]>);
@@ -400,7 +396,126 @@ impl ToTokens for SingleArg {
} }
} }
enum Value { 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,
conditional: bool,
}
impl Parse for EnvVar {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let key = input.parse()?;
_ = input.parse::<Token![:]>()?;
let conditional = if input.lookahead1().peek(Token![?]) {
input.parse::<Token![?]>()?;
true
} else {
false
};
let value = input.parse()?;
Ok(Self {
key,
value,
conditional,
})
}
}
impl ToTokens for EnvVar {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self {
key,
value,
conditional,
} = self;
if *conditional {
tokens.extend(quote! {
if ::std::env::var(#key).ok().is_none() {
_c.env(#key, #value);
}
});
} else {
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), Lit(syn::Lit),
Ident(syn::Ident), Ident(syn::Ident),
Expr(syn::Expr), Expr(syn::Expr),
@@ -430,12 +545,4 @@ impl Parse for Value {
} }
} }
impl ToTokens for Value { enum_to_tokens! {Value: Lit, Ident, Expr}
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 },
});
}
}

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

@@ -0,0 +1,220 @@
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""#);
/// ```
///
/// #### Conditional
/// You can have a default value set for an environment variable.
///
/// ```rust
/// # use comlexr_macro::cmd;
/// const NEW_VAR: &str = "NEW_VAR";
/// std::env::set_var("TEST", "realvalue");
///
/// let command = cmd!(
/// env {
/// "TEST":? "test",
/// NEW_VAR: "new_var"
/// };
/// "echo",
/// "test",
/// );
///
/// assert_eq!(format!("{command:?}"), r#"NEW_VAR="new_var" "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;

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

@@ -0,0 +1,191 @@
use quote::{quote, ToTokens};
use syn::{
parse::{discouraged::Speculative, Parse},
punctuated::Punctuated,
spanned::Spanned,
Error, Token,
};
use crate::cmd::Value;
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: std::boxed::Box::new(#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",
)
})
}
}
impl ToTokens for CommandExpr {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
tokens.extend(match self {
Self::Macro(macr) => quote! {
::std::process::Command = #macr
},
Self::Reference(refer) => quote! {
&mut ::std::process::Command = #refer
},
Self::Function(fun) => quote! {
::std::process::Command = #fun
},
Self::Block(block) => quote! {
::std::process::Command = #block
},
});
}
}

View File

@@ -1,126 +1,8 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
extern crate quote; extern crate comlexr_macro;
extern crate syn;
use quote::quote; pub use comlexr_macro::*;
use syn::parse_macro_input; pub use pipe::*;
mod cmd; 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::cmd;
///
/// let command = cmd!("echo", "test");
/// assert_eq!(format!("{command:?}"), r#""echo" "test""#.to_string());
/// ```
///
/// ## Conditional Arguments
/// ```
/// use comlexr::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::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::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::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::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::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()
}

76
src/pipe.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::{
ffi::OsStr,
process::Command,
process::{Child, ExitStatus, Output},
};
use thiserror::Error;
/// Errors that can be created when running the `PipeExecutor`
#[derive(Debug, Error)]
pub enum ExecutorError {
/// IO Error
#[error(transparent)]
Io(#[from] std::io::Error),
/// Failed Command Error
#[error("Failed to run command '{} {}', exit code {exit_code}",
.command
.get_program()
.to_string_lossy(),
.command
.get_args()
.map(OsStr::to_string_lossy)
.collect::<Vec<_>>()
.join(" "),
)]
FailedCommand {
command: Box<Command>,
exit_code: i32,
},
/// No stdin Error
#[error("Unable to get mutable stdin")]
NoStdIn,
}
/// A lazy piped command executor.
pub struct Executor<'a, S>
where
S: AsRef<[u8]> + ?Sized,
{
stdin: Option<&'a S>,
piped_commands: Box<dyn FnMut(Option<&'a S>) -> Result<Child, ExecutorError> + 'a>,
}
impl<'a, S> Executor<'a, S>
where
S: AsRef<[u8]> + ?Sized,
{
/// Construct a `PipeExecutor`.
pub fn new(
stdin: Option<&'a S>,
piped_commands: impl FnMut(Option<&'a S>) -> Result<Child, ExecutorError> + 'a,
) -> Self {
Self {
stdin,
piped_commands: Box::new(piped_commands),
}
}
/// Retrieves the `ExitStatus` of the last command.
///
/// # Errors
/// Will error if the exit status of any chained commands fail.
pub fn status(&mut self) -> Result<ExitStatus, ExecutorError> {
Ok((self.piped_commands)(self.stdin)?.wait()?)
}
/// Retrieves the `Output` of the last command.
///
/// # Errors
/// Will error if the exit status of any chained commands fail.
pub fn output(&mut self) -> Result<Output, ExecutorError> {
Ok((self.piped_commands)(self.stdin)?.wait_with_output()?)
}
}

View File

@@ -1,6 +1,8 @@
extern crate comlexr; extern crate comlexr;
extern crate rstest; extern crate rstest;
use std::env;
use comlexr::cmd; use comlexr::cmd;
use rstest::rstest; use rstest::rstest;
@@ -11,6 +13,78 @@ 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 conditional_env_vars() {
env::set_var("TEST", "realvalue");
env::set_var("TEST2", "won't see");
let command = cmd!(
env {
"TEST":? "test",
"TEST2": "test2"
};
"echo",
"test",
);
assert_eq!(format!("{command:?}"), r#"TEST2="test2" "echo" "test""#);
let command = cmd!(
env {
"TEST": "test",
"TEST2": "test2"
};
"echo",
"test",
);
assert_eq!(
format!("{command:?}"),
r#"TEST="test" TEST2="test2" "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""#)]

104
tests/pipe.rs Normal file
View File

@@ -0,0 +1,104 @@
use core::panic;
use std::ffi::OsStr;
use comlexr::{cmd, pipe};
use tempfile::tempdir;
#[test]
fn pipe_status() {
let dir = tempdir().unwrap();
let file = dir.path().join("out");
let mut pipe = pipe!(cmd!("echo", "test") | cmd!("tee", &file));
let status = pipe.status().unwrap();
assert!(status.success());
}
#[test]
fn pipe_stdin_output() {
let dir = tempdir().unwrap();
let file = dir.path().join("out");
let mut pipe = pipe!(stdin = "test"; cmd!("sed", "s|e|oa|") | cmd!("tee", &file));
println!("{}", file.display());
let output = pipe.output().unwrap();
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "toast");
}
#[test]
fn pipe_stdin_output_single_command() {
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");
}
#[test]
fn pipe_expect_fail() {
let mut pipe = pipe!(stdin = "test"; cmd!("false") | cmd!("sed", "s|e|oa|"));
let error = pipe.output().unwrap_err();
match error {
comlexr::ExecutorError::FailedCommand { command, exit_code } => {
assert_eq!(command.get_program(), OsStr::new("false"));
assert_eq!(exit_code, 1);
}
err => panic!("Expected exit code, got: {err}"),
}
}
#[test]
fn pipe_expect_fail_command_not_exist() {
let mut pipe = pipe!(stdin = "test"; cmd!("no_command") | cmd!("sed", "s|e|oa|"));
let error = pipe.output().unwrap_err();
match error {
comlexr::ExecutorError::Io(err) => assert_eq!(err.raw_os_error().unwrap(), 2),
err => panic!("Expected IO Error, got: {err}"),
}
}
#[test]
fn pipe_block() {
let mut pipe = pipe!(stdin = "test"; {
let c = cmd!("sed", "s|e|oa|");
println!("{c:?}");
c
});
let output = pipe.output().unwrap();
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "toast");
}
#[test]
fn pipe_ref() {
let mut command = {
let c = cmd!("sed", "s|e|oa|");
println!("{c:?}");
c
};
let mut pipe = pipe!(stdin = "test"; &mut command);
let output = pipe.output().unwrap();
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "toast");
}
#[test]
fn pipe_fn() {
let command = || {
let c = cmd!("sed", "s|e|oa|");
println!("{c:?}");
c
};
let mut pipe = pipe!(stdin = "test"; command());
let output = pipe.output().unwrap();
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "toast");
}