Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fa4b21c4f | |||
|
|
fc55ab350d | ||
|
|
403c7be922 | ||
| b87fe80302 | |||
| a4a41c8dfe | |||
| 3a0dbacf12 | |||
| bcf4664f0e | |||
| 820afa2171 | |||
| 0858e04c41 | |||
| ebc4cc0bd4 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
/expand.rs
|
||||||
|
|||||||
5
.rusty-hook.toml
Normal file
5
.rusty-hook.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[hooks]
|
||||||
|
pre-push = "cargo fmt --check && cargo test && cargo clippy -- -D warnings"
|
||||||
|
|
||||||
|
[logging]
|
||||||
|
verbose = true
|
||||||
29
CHANGELOG.md
Normal file
29
CHANGELOG.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Add more support for patterns and conditional match
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Properly parse Expressions, Idents, and Literals
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Clean up parse logic for Value
|
||||||
|
- Add git hooks
|
||||||
|
- Remove earthly check in release just script
|
||||||
|
- Add CHANGELOG.md
|
||||||
|
- Release comlexr version 1.1.0
|
||||||
|
|
||||||
|
<!-- generated by git-cliff -->
|
||||||
110
Cargo.lock
generated
110
Cargo.lock
generated
@@ -23,22 +23,48 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ci_info"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24f638c70e8c5753795cc9a8c07c44da91554a09e4cf11a7326e8161b0a3c45e"
|
||||||
|
dependencies = [
|
||||||
|
"envmnt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "comlexr"
|
name = "comlexr"
|
||||||
version = "1.0.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rstest",
|
"rstest",
|
||||||
|
"rusty-hook",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "envmnt"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2d328fc287c61314c4a61af7cfdcbd7e678e39778488c7cb13ec133ce0f4059"
|
||||||
|
dependencies = [
|
||||||
|
"fsio",
|
||||||
|
"indexmap 1.9.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fsio"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1fd087255f739f4f1aeea69f11b72f8080e9c2e7645cd06955dad4a178a49e3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -82,18 +108,43 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getopts"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
@@ -101,7 +152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.15.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -110,6 +161,12 @@ version = "2.7.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nias"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab250442c86f1850815b5d268639dff018c0627022bc1940eb2d642ca1ce12f0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -223,12 +280,44 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusty-hook"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96cee9be61be7e1cbadd851e58ed7449c29c620f00b23df937cb9cbc04ac21a3"
|
||||||
|
dependencies = [
|
||||||
|
"ci_info",
|
||||||
|
"getopts",
|
||||||
|
"nias",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
|
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.217"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.217"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@@ -249,6 +338,15 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.6.8"
|
version = "0.6.8"
|
||||||
@@ -261,7 +359,7 @@ version = "0.22.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
|
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.7.0",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
@@ -272,6 +370,12 @@ version = "1.0.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.6.24"
|
version = "0.6.24"
|
||||||
|
|||||||
14
Cargo.toml
14
Cargo.toml
@@ -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.0.0"
|
version = "1.2.0"
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
rust-version = "1.60"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
@@ -12,10 +13,11 @@ 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"
|
||||||
|
rusty-hook = "0.11"
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
unsafe_code = "forbid"
|
unsafe_code = "forbid"
|
||||||
@@ -27,3 +29,9 @@ perf = "deny"
|
|||||||
style = "deny"
|
style = "deny"
|
||||||
nursery = "deny"
|
nursery = "deny"
|
||||||
pedantic = "deny"
|
pedantic = "deny"
|
||||||
|
|
||||||
|
[package.metadata.release]
|
||||||
|
pre-release-hook = ["git", "cliff", "-o", "CHANGELOG.md", "--tag", "{{version}}"]
|
||||||
|
pre-release-replacements = [
|
||||||
|
{ file = "README.md", search = "comlexr = \"\\d+.\\d+.\\d+\"", replace = "comlexr = \"{{version}}\"" }
|
||||||
|
]
|
||||||
|
|||||||
58
README.md
58
README.md
@@ -8,7 +8,7 @@ Add `comlexr` to your project's `Cargo.toml`:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
comlexr = "1.0.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`)
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
59
justfile
Normal file
59
justfile
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
export RUST_BACKTRACE := "1"
|
||||||
|
|
||||||
|
set dotenv-load := true
|
||||||
|
set positional-arguments := true
|
||||||
|
|
||||||
|
# default recipe to display help information
|
||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
# Clean up development files and images
|
||||||
|
clean:
|
||||||
|
cargo clean
|
||||||
|
|
||||||
|
# Run unit tests
|
||||||
|
test:
|
||||||
|
cargo test --workspace -- --show-output
|
||||||
|
|
||||||
|
# Run unit tests for all features
|
||||||
|
test-all-features:
|
||||||
|
cargo test --workspace --all-features -- --show-output
|
||||||
|
|
||||||
|
# Run clippy
|
||||||
|
lint:
|
||||||
|
cargo clippy
|
||||||
|
|
||||||
|
# Run clippy for all features
|
||||||
|
lint-all-features:
|
||||||
|
cargo clippy --all-features
|
||||||
|
|
||||||
|
# Expand the macros of a module for debugging
|
||||||
|
expand *args:
|
||||||
|
cargo expand $@ > ./expand.rs
|
||||||
|
$EDITOR ./expand.rs
|
||||||
|
|
||||||
|
# Installs cargo tools that help with development
|
||||||
|
tools:
|
||||||
|
rustup toolchain install stable nightly
|
||||||
|
rustup component add --toolchain stable rust-analyzer clippy rustfmt
|
||||||
|
cargo install --locked cargo-watch cargo-expand bacon
|
||||||
|
|
||||||
|
# Run cargo release and push the tag separately
|
||||||
|
release *args:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
# --workspace: updating all crates in the workspace
|
||||||
|
# --no-tag: do not push tag for each new version
|
||||||
|
# --no-confirm: don't look for user input, just run the command
|
||||||
|
# --execute: not a dry run
|
||||||
|
cargo release $1 -v \
|
||||||
|
--workspace \
|
||||||
|
--no-tag \
|
||||||
|
--no-confirm \
|
||||||
|
--execute
|
||||||
|
|
||||||
|
VERSION=$(cargo metadata --format-version 1 | jq -r '.packages[] | select(.name == "comlexr") .version')
|
||||||
|
echo "Pushing tag: v${VERSION}"
|
||||||
|
git tag "v${VERSION}"
|
||||||
|
git push origin "v${VERSION}"
|
||||||
190
src/cmd.rs
190
src/cmd.rs
@@ -1,23 +1,36 @@
|
|||||||
use quote::{quote, ToTokens};
|
use quote::{quote, ToTokens};
|
||||||
use syn::{braced, bracketed, parse::Parse, punctuated::Punctuated, token, Token};
|
use syn::{
|
||||||
|
braced, bracketed,
|
||||||
|
parse::{discouraged::Speculative, Parse},
|
||||||
|
punctuated::Punctuated,
|
||||||
|
token, Token,
|
||||||
|
};
|
||||||
|
|
||||||
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)?),
|
||||||
})
|
})
|
||||||
@@ -27,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| {
|
|
||||||
quote! {
|
|
||||||
{
|
{
|
||||||
let mut _c = #program;
|
let mut _c = #program;
|
||||||
|
#cd
|
||||||
|
#env_vars
|
||||||
#(#args)*
|
#(#args)*
|
||||||
_c
|
_c
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +77,10 @@ enum LogicArg {
|
|||||||
impl Parse for LogicArg {
|
impl Parse for LogicArg {
|
||||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||||
if input.peek(Token![for]) {
|
if input.peek(Token![for]) {
|
||||||
if input.peek3(Token![in]) {
|
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)
|
input.parse().map(Self::ForIn)
|
||||||
} else {
|
} else {
|
||||||
input.parse().map(Self::ForIter)
|
input.parse().map(Self::ForIter)
|
||||||
@@ -258,28 +279,54 @@ impl ToTokens for Match {
|
|||||||
|
|
||||||
struct MatchArm {
|
struct MatchArm {
|
||||||
pattern: syn::Pat,
|
pattern: syn::Pat,
|
||||||
|
if_expr: Option<Value>,
|
||||||
args: Arguments,
|
args: Arguments,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for MatchArm {
|
impl Parse for MatchArm {
|
||||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||||
let pattern = input.call(syn::Pat::parse_multi)?;
|
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![=>]>()?;
|
_ = input.parse::<Token![=>]>()?;
|
||||||
let args = input.parse()?;
|
let args = input.parse()?;
|
||||||
|
|
||||||
Ok(Self { pattern, args })
|
Ok(Self {
|
||||||
|
pattern,
|
||||||
|
if_expr,
|
||||||
|
args,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToTokens for MatchArm {
|
impl ToTokens for MatchArm {
|
||||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||||
let Self { pattern, args } = self;
|
let Self {
|
||||||
|
pattern,
|
||||||
|
if_expr,
|
||||||
|
args,
|
||||||
|
} = self;
|
||||||
|
|
||||||
tokens.extend(quote! {
|
tokens.extend(if_expr.as_ref().map_or_else(
|
||||||
|
|| {
|
||||||
|
quote! {
|
||||||
#pattern => {
|
#pattern => {
|
||||||
#args
|
#args
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
|if_expr| {
|
||||||
|
quote! {
|
||||||
|
#pattern if #if_expr => {
|
||||||
|
#args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,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),
|
||||||
@@ -374,13 +514,25 @@ enum Value {
|
|||||||
|
|
||||||
impl Parse for Value {
|
impl Parse for Value {
|
||||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||||
if input.peek(syn::Lit) {
|
let expr_fork = input.fork();
|
||||||
input.parse().map(Self::Lit)
|
expr_fork
|
||||||
} else if input.peek(syn::Ident) {
|
.parse()
|
||||||
|
.map(|expr| {
|
||||||
|
input.advance_to(&expr_fork);
|
||||||
|
Self::Expr(expr)
|
||||||
|
})
|
||||||
|
.or_else(|_| {
|
||||||
|
if input.peek(syn::Ident) {
|
||||||
input.parse().map(Self::Ident)
|
input.parse().map(Self::Ident)
|
||||||
|
} else if input.peek(syn::Lit) {
|
||||||
|
input.parse().map(Self::Lit)
|
||||||
} else {
|
} else {
|
||||||
input.parse().map(Self::Expr)
|
Err(syn::Error::new(
|
||||||
|
input.span(),
|
||||||
|
"Expected an expression, ident, or literal",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
74
src/lib.rs
74
src/lib.rs
@@ -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;
|
||||||
@@ -86,6 +137,29 @@ mod cmd;
|
|||||||
/// assert_eq!(format!("{command:?}"), r#""echo" "test" "arg1" "arg2" "multi" "multi1" "multi" "multi2""#.to_string());
|
/// 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
|
/// ## Dynamic Closures
|
||||||
/// ```
|
/// ```
|
||||||
/// use comlexr::cmd;
|
/// use comlexr::cmd;
|
||||||
|
|||||||
96
tests/cmd.rs
96
tests/cmd.rs
@@ -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""#)]
|
||||||
@@ -62,6 +103,31 @@ fn for_in(#[case] single_iter: &[&str], #[case] multi_iter: &[&str], #[case] exp
|
|||||||
assert_eq!(format!("{command:?}"), expected.to_string());
|
assert_eq!(format!("{command:?}"), expected.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TestStruct(&'static str);
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(&[], &[], r#""echo" "test""#)]
|
||||||
|
#[case(&[TestStruct("1"), TestStruct("2")], &[], r#""echo" "test" "1" "2""#)]
|
||||||
|
#[case(&[], &[TestStruct("3"), TestStruct("4")], r#""echo" "test" "multi" "3" "multi" "4""#)]
|
||||||
|
#[case(&[TestStruct("1"), TestStruct("2")], &[TestStruct("3"), TestStruct("4")], r#""echo" "test" "1" "2" "multi" "3" "multi" "4""#)]
|
||||||
|
fn for_in_pat(
|
||||||
|
#[case] single_iter: &[TestStruct],
|
||||||
|
#[case] multi_iter: &[TestStruct],
|
||||||
|
#[case] expected: &str,
|
||||||
|
) {
|
||||||
|
let command = cmd!(
|
||||||
|
"echo",
|
||||||
|
"test",
|
||||||
|
for TestStruct(arg) in single_iter => arg,
|
||||||
|
for TestStruct(arg) in multi_iter => [
|
||||||
|
"multi",
|
||||||
|
arg,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(format!("{command:?}"), expected.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(None, None, r#""echo" "test""#)]
|
#[case(None, None, r#""echo" "test""#)]
|
||||||
#[case(Some("arg"), None, r#""echo" "test" "arg""#)]
|
#[case(Some("arg"), None, r#""echo" "test" "arg""#)]
|
||||||
@@ -105,11 +171,37 @@ fn match_statement(#[case] match_arg: TestArgs, #[case] expected: &str) {
|
|||||||
assert_eq!(format!("{command:?}"), expected.to_string());
|
assert_eq!(format!("{command:?}"), expected.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(TestArgs::Arg1, true, r#""echo" "test" "arg1""#)]
|
||||||
|
#[case(TestArgs::Arg1, false, r#""echo" "test""#)]
|
||||||
|
#[case(TestArgs::Arg2, true, r#""echo" "test" "arg1" "arg2""#)]
|
||||||
|
#[case(TestArgs::Arg2, false, r#""echo" "test""#)]
|
||||||
|
#[case(TestArgs::Arg3, true, r#""echo" "test" "arg1" "arg2" "arg3""#)]
|
||||||
|
#[case(TestArgs::Arg3, false, r#""echo" "test""#)]
|
||||||
|
fn match_statement_conditional(
|
||||||
|
#[case] match_arg: TestArgs,
|
||||||
|
#[case] flag: bool,
|
||||||
|
#[case] expected: &str,
|
||||||
|
) {
|
||||||
|
let command = cmd!(
|
||||||
|
"echo",
|
||||||
|
"test",
|
||||||
|
match match_arg {
|
||||||
|
TestArgs::Arg1 if flag => "arg1",
|
||||||
|
TestArgs::Arg2 if flag => ["arg1", "arg2"],
|
||||||
|
TestArgs::Arg3 if flag => ["arg1", "arg2", "arg3"],
|
||||||
|
_ => [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(format!("{command:?}"), expected.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(None, None, r#""echo" "test""#)]
|
#[case(None, None, r#""echo" "test""#)]
|
||||||
#[case(Some("arg"), None, r#""echo" "test" "arg""#)]
|
#[case(Some("arg"), None, r#""echo" "test" "arg""#)]
|
||||||
#[case(None, Some("arg"), r#""echo" "test" "multi" "arg""#)]
|
#[case(None, Some("arg"), r#""echo" "test" "multi" "arg""#)]
|
||||||
#[case(Some("1"), Some("2"), r#""echo" "test" "1" "multi" "2""#)]
|
#[case(Some("1"), Some("2"), r#""echo" "test" "1" "multi=2""#)]
|
||||||
fn multi_match(#[case] single: Option<&str>, #[case] multi: Option<&str>, #[case] expected: &str) {
|
fn multi_match(#[case] single: Option<&str>, #[case] multi: Option<&str>, #[case] expected: &str) {
|
||||||
let command = cmd!(
|
let command = cmd!(
|
||||||
"echo",
|
"echo",
|
||||||
@@ -118,7 +210,7 @@ fn multi_match(#[case] single: Option<&str>, #[case] multi: Option<&str>, #[case
|
|||||||
(None, None) => [],
|
(None, None) => [],
|
||||||
(Some(single), None) => single,
|
(Some(single), None) => single,
|
||||||
(None, Some(multi)) => ["multi", multi],
|
(None, Some(multi)) => ["multi", multi],
|
||||||
(Some(single), Some(multi)) => [single, "multi", multi],
|
(Some(single), Some(multi)) => [single, format!("multi={multi}")],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user