1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
use std::{
env,
ffi::{OsStr, OsString},
};
use crate::{core::Core, item::OwnedItem, ArgError, ForceUnicode, Item, ItemOs};
type AResult<T> = Result<T, ArgError>;
/// Use type to parse your command line arguments.
#[derive(Debug, Clone)]
pub struct ArgSplitter {
argv0: Option<OsString>,
core: Core,
last_flag: Option<String>,
stashed_args: Vec<OsString>,
}
impl ArgSplitter {
/// Create an [`ArgSplitter`] with the arguments from [`std::env::args_os`].
/// The first argument is assumed to be the program name and will be available
/// through [`ArgSplitter::argv0`], the rest are arguments and can be accessed
/// through [`ArgSplitter::item`], [`ArgSplitter::item_os`] and [`ArgSplitter::flag`].
#[allow(clippy::new_without_default)]
pub fn from_env() -> Self {
Self::from(env::args_os())
}
/// Create an [`ArgSplitter`] from the given argument list.
/// The first argument is assumed to be the program name and will be available
/// through [`ArgSplitter::argv0`], the rest are arguments and can be accessed
/// through [`ArgSplitter::item`], [`ArgSplitter::item_os`] and [`ArgSplitter::flag`].
pub fn from<S: AsRef<OsStr>>(argv: impl IntoIterator<Item = S>) -> Self {
let mut args = argv.into_iter().map(|s| s.as_ref().to_owned());
let argv0 = args.next();
let core = Core::new(args.collect());
ArgSplitter {
argv0,
core,
last_flag: None,
stashed_args: vec![],
}
}
fn flag_ref(&self) -> &str {
self.last_flag.as_ref().unwrap().as_str()
}
}
impl ArgSplitter {
/// Retrieve the very first item in the argument list, which is generally
/// the program name. Note that this value is set by the parent process and
/// can be absent or plain wrong.
pub fn argv0(&self) -> Option<&OsStr> {
self.argv0.as_deref()
}
/// Retrieve the next item on the command line as an [`ItemOs`]. Bundles of
/// single-letter arguments such as `-xvf` are split into separate items
/// `-x`, `-v` and `-f`. This method uses [`OsString`] for word arguments so
/// every file name can be represented. Use [`ArgSplitter::item`] if you
/// only care for arguments that are properly encoded.
pub fn item_os(&mut self) -> AResult<Option<ItemOs>> {
self.last_flag = None;
let owned_item = match self.core.take_item()? {
Some(i) => i,
None => return Ok(None),
};
let itemos = match owned_item {
OwnedItem::Flag(s) => {
self.last_flag = Some(s);
ItemOs::Flag(self.flag_ref())
}
OwnedItem::Word(w) => ItemOs::Word(w),
};
Ok(Some(itemos))
}
/// Retrieve the next item on the command line as an [`Item`].
/// Bundles of single-letter arguments such as `-xvf` are split into
/// separate items `-x`, `-v` and `-f`.
/// This method uses [`String`] for word arguments. Invalidly encoded
/// arguments will cause an [`ArgError::InvalidUnicode`].
/// Use [`ArgSplitter::item_os`] if you also want to accept badly encoded
/// arguments.
pub fn item(&mut self) -> AResult<Option<Item>> {
self.item_os().force_unicode()
}
/// Return `true` if and only if the parser is currently between arguments,
/// that is, not in the middle of a bundle (`-xvf`) or between a long
/// option and its parameter (`--file=data.csv`).
pub fn at_word(&self) -> bool {
self.core.at_word()
}
/// Return `true` if and only if the item most recently returned by
/// [`item_os`][`ArgSplitter::item_os`],
/// [`item`][`ArgSplitter::item`] or
/// [`flag`][`ArgSplitter::flag`]
/// was a flag and if a parameter is attached.
/// In the case of a long flag that means `--file=data.csv` but not
/// `--file data.csv`. In the case of a short flag it means
/// `-fdata.csv` but not `-f data.csv`.
pub fn has_param_attached(&self) -> bool {
self.core.param_ready()
}
/// If the item most recently returned by
/// [`item_os`][`ArgSplitter::item_os`],
/// [`item`][`ArgSplitter::item`] or
/// [`flag`][`ArgSplitter::flag`]
/// was a flag, return its parameter as an [`OsString`].
/// If the flag had a parameter attached (see [`has_param_attached`][`ArgSplitter::has_param_attached`]),
/// return that parameter. Otherwise, if the flag is followed by a word, return
/// that word. If no more arguments follow or if the next argument is another
/// flag, return [`ArgError::ParameterMissing`].
pub fn param_os(&mut self) -> AResult<OsString> {
assert!(
self.last_flag.is_some(),
"only call .parm_os() after .take_item() returned a flag"
);
if self.core.param_ready() {
Ok(self.core.param().unwrap())
} else if self.core.at_word() {
let it = self.core.take_item().unwrap().unwrap();
if let OwnedItem::Word(w) = it {
Ok(w)
} else {
panic!("at_word() inconsistent with take_item()");
}
} else {
Err(ArgError::ParameterMissing(self.flag_ref().to_owned()))
}
}
/// If the item most recently returned by
/// [`item_os`][`ArgSplitter::item_os`],
/// [`item`][`ArgSplitter::item`] or
/// [`flag`][`ArgSplitter::flag`]
/// was a flag, return its parameter as a [`String`].
/// If the flag had a parameter attached (see [`has_param_attached`][`ArgSplitter::has_param_attached`]),
/// return that parameter. Otherwise, if the flag is followed by a word, return
/// that word. If no more arguments follow or if the next argument is another
/// flag, return [`ArgError::ParameterMissing`].
pub fn param(&mut self) -> AResult<String> {
self.param_os().force_unicode()
}
}
impl ArgSplitter {
/// Similar to [`ArgSplitter::item_os`] but only returns flags.
/// Returns them as an `Option<&str>` rather than [`Item`] or [`ItemOs`]
/// for better match ergomics.
/// All non-flag arguments are stashed in a buffer from which they can
/// be retrieved using
/// [`stashed`][`ArgSplitter::stashed`],
/// [`stashed_os`][`ArgSplitter::stashed_os`],
/// [`stashed_args`][`ArgSplitter::stashed_args`] or
/// [`stashed_args_os`][`ArgSplitter::stashed_args_os`].
pub fn flag(&mut self) -> AResult<Option<&str>> {
loop {
let w = match self.item_os()? {
None => return Ok(None),
Some(ItemOs::Flag(_)) => break,
Some(ItemOs::Word(w)) => w,
};
self.stashed_args.push(w);
}
Ok(Some(self.flag_ref()))
}
fn take_stashed(&mut self) -> Option<OsString> {
if self.stashed_args.is_empty() {
None
} else {
Some(self.stashed_args.remove(0))
}
}
/// Return an argument set aside by [`ArgSplitter::flag`], as an
/// [`OsString`]. Yields an [`OsString`] or an error if no argument is
/// present. For optional arguments, see the iterator returned by
/// [`ArgSplitter::stashed_args_os`].
pub fn stashed_os(&mut self, desc: &str) -> AResult<OsString> {
match self.take_stashed() {
Some(v) => Ok(v),
None => Err(ArgError::ArgumentMissing(desc.to_owned())),
}
}
/// Return an argument set aside by [`ArgSplitter::flag`], as [`String`].
/// Yields a [`String`] or an error if no argument is present. For optional
/// arguments, see the iterator returned by [`ArgSplitter::stashed_args`].
pub fn stashed(&mut self, desc: &str) -> AResult<String> {
self.stashed_os(desc).force_unicode()
}
/// Iterate over the arguments set aside by [`ArgSplitter::flag`], as
/// [`OsString`]. Return an error if no sufficient number of stashed
/// arguments is available. Use `desc` as a description in the error
/// message.
///
/// A note about the return type. This function returns a Result of Iterator
/// over OsString. The Result is outside the Iterator because the only thing
/// that can go wrong is that there are too few arguments. This way we don't
/// have to deal with errors when processing individual items from the
/// iterator. This makes it different from [`ArgSplitter::stashed_args`].
///
/// # Example
/// ```
/// # fn main() -> Result<(),Box<dyn std::error::Error>> {
/// # use std::path::PathBuf;
/// # use argsplitter::ArgSplitter;
/// # let mut argsplitter = ArgSplitter::from(["test", "a", "-f"]);
/// # argsplitter.flag();
/// let filenames: Vec<PathBuf> = argsplitter
/// .stashed_args_os(1, "FILE")?
/// .map(PathBuf::from)
/// .collect();
/// # let _ = filenames; Ok(())
/// # }
/// ```
/// Note how the question mark operator comes directly after the call to
/// `stashed_args_os()`.
pub fn stashed_args_os(&mut self, expect_at_least: usize, desc: &str) -> AResult<StashedOs> {
if self.stashed_args.len() >= expect_at_least {
Ok(StashedOs(self))
} else {
Err(ArgError::ArgumentMissing(desc.to_owned()))
}
}
/// Iterate over the arguments set aside by [`ArgSplitter::flag`], as
/// [`String`]. Return errors if the encoding is wrong or if no sufficient
/// number of stashed arguments is available. Use `desc` as a description in
/// the error message.
///
/// A note about the return type. This function returns an Iterator over
/// Results of String. The Result is inside the Iterator because the errors
/// can either be about the number of available stashed arguments or about
/// the encoding of an individual argument. This makes it different from
/// [`ArgSplitter::stashed_args_os`].
///
/// # Example
/// ```
/// # fn main() -> Result<(),Box<dyn std::error::Error>> {
/// # use argsplitter::ArgSplitter;
/// # let mut argsplitter = ArgSplitter::from(["test", "a", "-f"]);
/// # argsplitter.flag();
/// let recipients: Vec<String> = argsplitter
/// .stashed_args(1, "RECIPIENT")
/// .collect::<Result<_,_>>()?;
/// # let _ = recipients; Ok(())
/// # }
/// ```
/// Note how the question mark operator only comes after the `collect` has
/// moved the Result from inside the iterator to outside.
pub fn stashed_args(&mut self, expect_at_least: usize, desc: &str) -> Stashed {
let err = if self.stashed_args.len() >= expect_at_least {
None
} else {
Some(ArgError::ArgumentMissing(desc.to_owned()))
};
Stashed {
splitter: self,
err,
}
}
/// Return `Ok(())` if all stashed arguments have been consumed,
/// `Err(ArgError::UnexpectedArgument)` otherwise.
pub fn no_more_stashed(&self) -> AResult<()> {
if self.stashed_args.is_empty() {
Ok(())
} else {
Err(ArgError::UnexpectedArgument(self.stashed_args[0].clone()))
}
}
}
/// Iterator returned by [`ArgSplitter::stashed_args_os()`].
pub struct StashedOs<'a>(&'a mut ArgSplitter);
impl Iterator for StashedOs<'_> {
type Item = OsString;
fn next(&mut self) -> Option<Self::Item> {
self.0.take_stashed()
}
}
/// Iterator returned by [`ArgSplitter::stashed_args()`].
pub struct Stashed<'a> {
splitter: &'a mut ArgSplitter,
err: Option<ArgError>,
}
impl Iterator for Stashed<'_> {
type Item = AResult<String>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(ref err) = self.err {
Some(Err(err.clone()))
} else {
self.splitter
.take_stashed()
.map(ForceUnicode::force_unicode)
}
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
#[test]
fn test_completely_empty() {
let empty: Vec<OsString> = vec![];
let mut sp = ArgSplitter::from(empty);
assert_eq!(sp.argv0(), None);
assert_eq!(sp.has_param_attached(), false);
assert_eq!(sp.item_os(), Ok(None));
assert_eq!(sp.has_param_attached(), false);
}
#[test]
fn test_no_args() {
let empty: Vec<OsString> = vec!["test".into()];
let mut sp = ArgSplitter::from(empty);
assert_eq!(sp.argv0(), Some(OsStr::new("test")));
assert_eq!(sp.has_param_attached(), false);
assert_eq!(sp.item_os(), Ok(None));
assert_eq!(sp.has_param_attached(), false);
}
#[test]
fn test_split_short() {
let mut sp = ArgSplitter::from(["test", "-vx", "-n", "ARGS"]);
assert_eq!(sp.has_param_attached(), false);
assert_eq!(sp.item(), Ok(Some(Item::Flag("-v"))));
assert_eq!(sp.has_param_attached(), true);
assert_eq!(sp.clone().param(), Ok("x".to_owned()));
assert_eq!(sp.item(), Ok(Some(Item::Flag("-x"))));
assert_eq!(sp.has_param_attached(), false);
assert_eq!(
sp.clone().param(),
Err(ArgError::ParameterMissing("-x".into()))
);
assert_eq!(sp.item(), Ok(Some(Item::Flag("-n"))));
assert_eq!(sp.has_param_attached(), false);
assert_eq!(sp.clone().param(), Ok("ARGS".into()));
assert_eq!(sp.item(), Ok(Some(Item::Word("ARGS".into()))));
assert_eq!(sp.has_param_attached(), false);
// must not call .parm after getting a Word.
}
#[test]
fn test_split_long() {
let mut sp = ArgSplitter::from(["test", "--foo", "--bar=BAR", "--baz", "ARGS"]);
assert_eq!(sp.has_param_attached(), false);
assert_eq!(sp.item(), Ok(Some(Item::Flag("--foo"))));
assert_eq!(sp.has_param_attached(), false);
assert_eq!(
sp.clone().param(),
Err(ArgError::ParameterMissing("--foo".into()))
);
assert_eq!(sp.item(), Ok(Some(Item::Flag("--bar"))));
assert_eq!(sp.has_param_attached(), true);
assert_eq!(sp.param(), Ok("BAR".into()));
assert_eq!(sp.has_param_attached(), false);
assert_eq!(
sp.clone().param(),
Err(ArgError::ParameterMissing("--bar".into()))
);
assert_eq!(sp.item(), Ok(Some(Item::Flag("--baz"))));
assert_eq!(sp.has_param_attached(), false);
assert_eq!(sp.clone().param(), Ok("ARGS".into()));
assert_eq!(sp.item(), Ok(Some(Item::Word("ARGS".into()))));
assert_eq!(sp.has_param_attached(), false);
// must not call .parm after getting a Word.
}
#[test]
fn test_flag() {
let mut sp = ArgSplitter::from(["testprog", "a", "-fFILE", "b", "--", "c"]);
assert_eq!(sp.flag(), Ok(Some("-f")));
assert_eq!(sp.param(), Ok("FILE".into()));
assert_eq!(sp.flag(), Ok(Some("--")));
// It should have collected a and b, but not c
let mut sp2 = sp.clone();
assert!(sp2.stashed_args_os(3, "STASHED").is_err());
assert!(sp2.stashed_args_os(2, "STASHED").is_ok());
// they have not been consumed:
assert!(sp2.stashed_args_os(2, "STASHED").is_ok());
// Note: Result outside next's Option
assert_eq!(
sp2.stashed_args_os(0, "STASHED").unwrap().next(),
Some("a".into())
);
assert_eq!(sp2.stashed_os("STASHED"), Ok("b".into()));
// No more..
assert_eq!(sp2.stashed_args_os(0, "STASHED").unwrap().next(), None);
assert_eq!(
sp2.stashed_os("STASHED"),
Err(ArgError::ArgumentMissing("STASHED".into()))
);
// Again, but without the _os
// Result is now inside next's Option
assert!(sp.stashed_args(3, "STASHED").next().unwrap().is_err());
assert_eq!(sp.stashed_args(2, "STASHED").next(), Some(Ok("a".into())));
assert_eq!(sp.stashed("STASHED"), Ok("b".into()));
// No more..
assert_eq!(sp.stashed_args(0, "STASHED").next(), None);
assert_eq!(
sp.stashed_args(1, "STASHED").next(),
Some(Err(ArgError::ArgumentMissing("STASHED".into())))
);
assert_eq!(
sp.stashed("STASHED"),
Err(ArgError::ArgumentMissing("STASHED".into()))
);
}
}