Skip to content

Commit 098cbcd

Browse files
feat!: Add double-dash (--) support
Also, migrate from ariadne to miette. The core engine is completely rewritten and much lighter, and tests that weren't run at all have been removed from now. Some documentation was added.
1 parent 0fa3643 commit 098cbcd

40 files changed

+1126
-3958
lines changed

Cargo.lock

Lines changed: 296 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,23 @@ keywords = ["cli", "args", "parser", "facet"]
1111
categories = ["command-line-interface"]
1212

1313
[dependencies]
14-
facet-reflect = { version = "0.28.3" }
15-
facet-core = { version = "0.28.3" }
14+
facet-reflect = { version = "0.29" }
15+
facet-core = { version = "0.29" }
1616
log = "0.4.28"
17-
owo-colors = "4.2.2"
18-
ariadne = { version = "0.5.1", optional = true }
17+
heck = "0.5.0"
18+
miette = { version = "7.6.0", default-features = false }
1919

2020
[dev-dependencies]
2121
cargo-husky = { version = "1.5.0", default-features = false, features = [
2222
"user-hooks",
2323
] }
2424
eyre = "0.6.12"
25-
facet = { version = "0.28.3" }
26-
facet-pretty = { version = "0.28.0" }
27-
facet-testhelpers = { version = "0.28" }
25+
facet = { version = "0.29" }
26+
facet-pretty = { version = "0.29" }
27+
facet-testhelpers = { version = "0.29" }
2828
insta = "1.43.2"
29+
miette = { version = "7.6.0", default-features = false, features = ["fancy"] }
2930

3031
[features]
3132
default = ["rich-diagnostics"]
32-
rich-diagnostics = ["dep:ariadne"]
33+
rich-diagnostics = []

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ Ok(())
3131
# }
3232
```
3333

34+
## Behavior
35+
36+
The behavior of facet-args is still in flux, but here are the broad strokes:
37+
38+
* We're always parsing to a struct (not an enum, vec etc.)
39+
* The struct we're parsing to is always owned — no borrowing happening here, it
40+
gets too complicated with `&'slice [&'text str]`
41+
* Arguments are either `positional` or `named` — fields lacking either annotation are ignored
42+
* Accepted syntaxes for short flags are: `short = 'v'` and `short = "v"` (where v can be any letter)
43+
* `positional` args of type `Vec` (or anything that has a `Def::List`) will soak up all the positional
44+
arguments — if followed by `positional` arguments of type `String` for example, those will never
45+
get filled
46+
* After parsing every available argument, uninitialized struct fields are filled with their default value
47+
if they have `facet(default)` set: this includes `Vec`.
48+
3449
## Sponsors
3550

3651
Thanks to all individual sponsors:

README.md.in

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,18 @@ eprintln!("args: {}", args.pretty());
2222
Ok(())
2323
# }
2424
```
25+
26+
## Behavior
27+
28+
The behavior of facet-args is still in flux, but here are the broad strokes:
29+
30+
* We're always parsing to a struct (not an enum, vec etc.)
31+
* The struct we're parsing to is always owned — no borrowing happening here, it
32+
gets too complicated with `&'slice [&'text str]`
33+
* Arguments are either `positional` or `named` — fields lacking either annotation are ignored
34+
* Accepted syntaxes for short flags are: `short = 'v'` and `short = "v"` (where v can be any letter)
35+
* `positional` args of type `Vec` (or anything that has a `Def::List`) will soak up all the positional
36+
arguments — if followed by `positional` arguments of type `String` for example, those will never
37+
get filled
38+
* After parsing every available argument, uninitialized struct fields are filled with their default value
39+
if they have `facet(default)` set: this includes `Vec`.

src/arg.rs

Lines changed: 16 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1-
use crate::deserialize::{Subspan, SubspanMeta};
2-
use alloc::borrow::Cow;
3-
41
#[derive(Debug)]
52
pub(crate) enum ArgType<'a> {
6-
LongFlag(Cow<'a, str>),
3+
// A `--` separator after which we only expect positional arguments
4+
DoubleDash,
5+
6+
// `--{arg}`
7+
LongFlag(&'a str),
8+
9+
// `-{a}`
710
ShortFlag(&'a str),
11+
12+
// `{a}`
813
Positional,
14+
15+
// End of the argument list
916
None,
1017
}
1118

1219
impl<'a> ArgType<'a> {
1320
pub(crate) fn parse(arg: &'a str) -> Self {
1421
if let Some(key) = arg.strip_prefix("--") {
15-
ArgType::LongFlag(Self::kebab_to_snake(key))
22+
if key.is_empty() {
23+
ArgType::DoubleDash
24+
} else {
25+
ArgType::LongFlag(key)
26+
}
1627
} else if let Some(key) = arg.strip_prefix('-') {
1728
ArgType::ShortFlag(key)
1829
} else if !arg.is_empty() {
@@ -21,67 +32,4 @@ impl<'a> ArgType<'a> {
2132
ArgType::None
2233
}
2334
}
24-
25-
pub(crate) fn kebab_to_snake(input: &str) -> Cow<'_, str> {
26-
if !input.contains('-') {
27-
return Cow::Borrowed(input);
28-
}
29-
Cow::Owned(input.replace('-', "_"))
30-
}
31-
}
32-
33-
// This trait implementation allows for using a Subspan together with an arg string
34-
impl<'a> From<(&'a Subspan, &'a str)> for ArgType<'a> {
35-
/// Converts a subspan and argument string into the appropriate ArgType.
36-
///
37-
/// - For KeyValue metadata: key part (offset 0) is parsed normally (ShortFlag or LongFlag),
38-
/// value part (offset > 0) is treated as Positional
39-
/// - For Delimiter metadata: treated as Positional
40-
/// - For no metadata: parsed normally
41-
fn from((subspan, arg): (&'a Subspan, &'a str)) -> Self {
42-
if subspan.offset >= arg.len() {
43-
return ArgType::None;
44-
}
45-
46-
let end = core::cmp::min(subspan.offset + subspan.len, arg.len());
47-
let part = &arg[subspan.offset..end];
48-
49-
// Check metadata for special handling
50-
if let Some(meta) = &subspan.meta {
51-
match meta {
52-
SubspanMeta::KeyValue => {
53-
// For KeyValue, if it's the value part (offset > 0),
54-
// treat it as a positional argument regardless of content
55-
if subspan.offset > 0 {
56-
return if !part.is_empty() {
57-
ArgType::Positional
58-
} else {
59-
ArgType::None
60-
};
61-
}
62-
// Otherwise parse key part normally with parse()
63-
}
64-
SubspanMeta::Delimiter(_) => {
65-
// For delimited values, treat as positional
66-
return if !part.is_empty() {
67-
ArgType::Positional
68-
} else {
69-
ArgType::None
70-
};
71-
}
72-
}
73-
}
74-
75-
// Default parsing for keys and non-special cases
76-
ArgType::parse(part)
77-
}
78-
}
79-
80-
/// Extracts a substring from arg based on a subspan
81-
pub(crate) fn extract_subspan<'a>(subspan: &Subspan, arg: &'a str) -> &'a str {
82-
if subspan.offset >= arg.len() {
83-
return "";
84-
}
85-
let end = core::cmp::min(subspan.offset + subspan.len, arg.len());
86-
&arg[subspan.offset..end]
8735
}

0 commit comments

Comments
 (0)