1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, slice};
5
6pub mod prelude {
8 pub use crate::{Arg, RawArgs, is_flag_like, is_option_separator, is_positional};
9}
10
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
13pub struct Arg {
14 value: String,
15}
16
17impl Arg {
18 #[must_use]
20 pub fn new(value: impl Into<String>) -> Self {
21 Self {
22 value: value.into(),
23 }
24 }
25
26 #[must_use]
28 pub fn as_str(&self) -> &str {
29 &self.value
30 }
31
32 #[must_use]
34 pub fn into_string(self) -> String {
35 self.value
36 }
37
38 #[must_use]
40 pub fn is_option_separator(&self) -> bool {
41 is_option_separator(&self.value)
42 }
43
44 #[must_use]
46 pub fn is_flag_like(&self) -> bool {
47 is_flag_like(&self.value)
48 }
49
50 #[must_use]
52 pub fn is_positional(&self) -> bool {
53 is_positional(&self.value)
54 }
55}
56
57impl AsRef<str> for Arg {
58 fn as_ref(&self) -> &str {
59 self.as_str()
60 }
61}
62
63impl From<String> for Arg {
64 fn from(value: String) -> Self {
65 Self::new(value)
66 }
67}
68
69impl From<&str> for Arg {
70 fn from(value: &str) -> Self {
71 Self::new(value)
72 }
73}
74
75impl fmt::Display for Arg {
76 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
77 formatter.write_str(&self.value)
78 }
79}
80
81#[derive(Clone, Debug, Default, PartialEq, Eq)]
83pub struct RawArgs {
84 args: Vec<Arg>,
85}
86
87impl RawArgs {
88 #[must_use]
90 pub const fn new(args: Vec<Arg>) -> Self {
91 Self { args }
92 }
93
94 #[must_use]
96 pub const fn empty() -> Self {
97 Self { args: Vec::new() }
98 }
99
100 pub fn push(&mut self, arg: Arg) {
102 self.args.push(arg);
103 }
104
105 #[must_use]
107 pub const fn len(&self) -> usize {
108 self.args.len()
109 }
110
111 #[must_use]
113 pub const fn is_empty(&self) -> bool {
114 self.args.is_empty()
115 }
116
117 pub fn iter(&self) -> slice::Iter<'_, Arg> {
119 self.args.iter()
120 }
121
122 #[must_use]
124 pub fn into_vec(self) -> Vec<Arg> {
125 self.args
126 }
127}
128
129impl FromIterator<Arg> for RawArgs {
130 fn from_iter<T: IntoIterator<Item = Arg>>(iter: T) -> Self {
131 Self::new(iter.into_iter().collect())
132 }
133}
134
135impl FromIterator<String> for RawArgs {
136 fn from_iter<T: IntoIterator<Item = String>>(iter: T) -> Self {
137 iter.into_iter().map(Arg::from).collect()
138 }
139}
140
141impl IntoIterator for RawArgs {
142 type Item = Arg;
143 type IntoIter = std::vec::IntoIter<Arg>;
144
145 fn into_iter(self) -> Self::IntoIter {
146 self.args.into_iter()
147 }
148}
149
150impl<'a> IntoIterator for &'a RawArgs {
151 type Item = &'a Arg;
152 type IntoIter = slice::Iter<'a, Arg>;
153
154 fn into_iter(self) -> Self::IntoIter {
155 self.iter()
156 }
157}
158
159#[must_use]
161pub fn is_option_separator(token: &str) -> bool {
162 token == "--"
163}
164
165#[must_use]
167pub fn is_flag_like(token: &str) -> bool {
168 token.starts_with('-') && token != "-" && !is_option_separator(token)
169}
170
171#[must_use]
173pub fn is_positional(token: &str) -> bool {
174 !is_option_separator(token) && !is_flag_like(token)
175}
176
177#[cfg(test)]
178mod tests {
179 use super::{Arg, RawArgs, is_flag_like, is_option_separator, is_positional};
180
181 #[test]
182 fn classifies_argument_tokens() {
183 assert!(is_positional("file.txt"));
184 assert!(is_positional("-"));
185 assert!(is_option_separator("--"));
186 assert!(is_flag_like("-v"));
187 assert!(is_flag_like("--verbose"));
188 assert!(!is_flag_like("--"));
189 }
190
191 #[test]
192 fn stores_owned_arguments() {
193 let mut args = RawArgs::empty();
194 args.push(Arg::from("tool"));
195 args.push(Arg::from("README.md"));
196
197 let tokens: Vec<_> = args.iter().map(Arg::as_str).collect();
198
199 assert_eq!(args.len(), 2);
200 assert_eq!(tokens, vec!["tool", "README.md"]);
201 assert!(args.into_vec()[1].is_positional());
202 }
203}