1 //! Minimal, flexible command-line parser
3 //! As opposed to a declarative parser, this processes arguments as a stream of tokens. As lexing
4 //! a command-line is not context-free, we rely on the caller to decide how to interpret the
10 //! use std::path::PathBuf;
11 //! use std::ffi::OsStr;
13 //! type BoxedError = Box<dyn std::error::Error + Send + Sync>;
17 //! paths: Vec<PathBuf>,
30 //! fn parse(s: Option<&OsStr>) -> Result<Self, BoxedError> {
31 //! let s = s.map(|s| s.to_str().ok_or(s));
33 //! Some(Ok("always")) | Some(Ok("")) | None => {
36 //! Some(Ok("auto")) => {
39 //! Some(Ok("never")) => {
42 //! Some(invalid) => {
43 //! Err(format!("Invalid value for `--color`, {invalid:?}").into())
50 //! raw: impl IntoIterator<Item=impl Into<std::ffi::OsString>>
51 //! ) -> Result<Args, BoxedError> {
52 //! let mut args = Args {
53 //! paths: Vec::new(),
54 //! color: Color::Auto,
58 //! let raw = clap_lex::RawArgs::new(raw);
59 //! let mut cursor = raw.cursor();
60 //! raw.next(&mut cursor); // Skip the bin
61 //! while let Some(arg) = raw.next(&mut cursor) {
62 //! if arg.is_escape() {
63 //! args.paths.extend(raw.remaining(&mut cursor).map(PathBuf::from));
64 //! } else if arg.is_stdio() {
65 //! args.paths.push(PathBuf::from("-"));
66 //! } else if let Some((long, value)) = arg.to_long() {
68 //! Ok("verbose") => {
69 //! if let Some(value) = value {
70 //! return Err(format!("`--verbose` does not take a value, got `{value:?}`").into());
72 //! args.verbosity += 1;
75 //! args.color = Color::parse(value)?;
79 //! format!("Unexpected flag: --{}", arg.display()).into()
83 //! } else if let Some(mut shorts) = arg.to_short() {
84 //! while let Some(short) = shorts.next_flag() {
87 //! args.verbosity += 1;
90 //! let value = shorts.next_value_os();
91 //! args.color = Color::parse(value)?;
94 //! return Err(format!("Unexpected flag: -{c}").into());
97 //! return Err(format!("Unexpected flag: -{}", e.to_string_lossy()).into());
102 //! args.paths.push(PathBuf::from(arg.to_value_os().to_owned()));
109 //! let args = parse_args(["bin", "--hello", "world"]);
110 //! println!("{args:?}");
116 use std::ffi::OsString;
118 pub use std::io::SeekFrom;
120 pub use ext::OsStrExt;
122 /// Command-line arguments
123 #[derive(Default, Clone, Debug, PartialEq, Eq)]
125 items: Vec<OsString>,
129 //// Create an argument list to parse
131 /// **NOTE:** The argument returned will be the current binary.
136 /// # use std::path::PathBuf;
137 /// let raw = clap_lex::RawArgs::from_args();
138 /// let mut cursor = raw.cursor();
139 /// let _bin = raw.next_os(&mut cursor);
141 /// let mut paths = raw.remaining(&mut cursor).map(PathBuf::from).collect::<Vec<_>>();
142 /// println!("{paths:?}");
144 pub fn from_args() -> Self {
145 Self::new(std::env::args_os())
148 //// Create an argument list to parse
153 /// # use std::path::PathBuf;
154 /// let raw = clap_lex::RawArgs::new(["bin", "foo.txt"]);
155 /// let mut cursor = raw.cursor();
156 /// let _bin = raw.next_os(&mut cursor);
158 /// let mut paths = raw.remaining(&mut cursor).map(PathBuf::from).collect::<Vec<_>>();
159 /// println!("{paths:?}");
161 pub fn new(iter: impl IntoIterator<Item = impl Into<std::ffi::OsString>>) -> Self {
162 let iter = iter.into_iter();
166 /// Create a cursor for walking the arguments
171 /// # use std::path::PathBuf;
172 /// let raw = clap_lex::RawArgs::new(["bin", "foo.txt"]);
173 /// let mut cursor = raw.cursor();
174 /// let _bin = raw.next_os(&mut cursor);
176 /// let mut paths = raw.remaining(&mut cursor).map(PathBuf::from).collect::<Vec<_>>();
177 /// println!("{paths:?}");
179 pub fn cursor(&self) -> ArgCursor {
183 /// Advance the cursor, returning the next [`ParsedArg`]
184 pub fn next(&self, cursor: &mut ArgCursor) -> Option<ParsedArg<'_>> {
185 self.next_os(cursor).map(ParsedArg::new)
188 /// Advance the cursor, returning a raw argument value.
189 pub fn next_os(&self, cursor: &mut ArgCursor) -> Option<&OsStr> {
190 let next = self.items.get(cursor.cursor).map(|s| s.as_os_str());
191 cursor.cursor = cursor.cursor.saturating_add(1);
195 /// Return the next [`ParsedArg`]
196 pub fn peek(&self, cursor: &ArgCursor) -> Option<ParsedArg<'_>> {
197 self.peek_os(cursor).map(ParsedArg::new)
200 /// Return a raw argument value.
201 pub fn peek_os(&self, cursor: &ArgCursor) -> Option<&OsStr> {
202 self.items.get(cursor.cursor).map(|s| s.as_os_str())
205 /// Return all remaining raw arguments, advancing the cursor to the end
210 /// # use std::path::PathBuf;
211 /// let raw = clap_lex::RawArgs::new(["bin", "foo.txt"]);
212 /// let mut cursor = raw.cursor();
213 /// let _bin = raw.next_os(&mut cursor);
215 /// let mut paths = raw.remaining(&mut cursor).map(PathBuf::from).collect::<Vec<_>>();
216 /// println!("{paths:?}");
218 pub fn remaining(&self, cursor: &mut ArgCursor) -> impl Iterator<Item = &OsStr> {
219 let remaining = self.items[cursor.cursor..].iter().map(|s| s.as_os_str());
220 cursor.cursor = self.items.len();
224 /// Adjust the cursor's position
225 pub fn seek(&self, cursor: &mut ArgCursor, pos: SeekFrom) {
226 let pos = match pos {
227 SeekFrom::Start(pos) => pos,
228 SeekFrom::End(pos) => (self.items.len() as i64).saturating_add(pos).max(0) as u64,
229 SeekFrom::Current(pos) => (cursor.cursor as i64).saturating_add(pos).max(0) as u64,
231 let pos = (pos as usize).min(self.items.len());
235 /// Inject arguments before the [`RawArgs::next`]
239 insert_items: impl IntoIterator<Item = impl Into<OsString>>,
242 cursor.cursor..cursor.cursor,
243 insert_items.into_iter().map(Into::into),
247 /// Any remaining args?
248 pub fn is_end(&self, cursor: &ArgCursor) -> bool {
249 self.peek_os(cursor).is_none()
253 impl<I, T> From<I> for RawArgs
255 I: Iterator<Item = T>,
258 fn from(val: I) -> Self {
260 items: val.map(|x| x.into()).collect(),
265 /// Position within [`RawArgs`]
266 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
267 pub struct ArgCursor {
277 /// Command-line Argument
278 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
279 pub struct ParsedArg<'s> {
283 impl<'s> ParsedArg<'s> {
284 fn new(inner: &'s OsStr) -> Self {
288 /// Argument is length of 0
289 pub fn is_empty(&self) -> bool {
290 self.inner.is_empty()
293 /// Does the argument look like a stdio argument (`-`)
294 pub fn is_stdio(&self) -> bool {
298 /// Does the argument look like an argument escape (`--`)
299 pub fn is_escape(&self) -> bool {
303 /// Does the argument look like a number
304 pub fn is_number(&self) -> bool {
306 .map(|s| s.parse::<f64>().is_ok())
310 /// Treat as a long-flag
311 pub fn to_long(&self) -> Option<(Result<&str, &OsStr>, Option<&OsStr>)> {
312 let raw = self.inner;
313 let remainder = raw.strip_prefix("--")?;
314 if remainder.is_empty() {
315 debug_assert!(self.is_escape());
319 let (flag, value) = if let Some((p0, p1)) = remainder.split_once("=") {
324 let flag = flag.to_str().ok_or(flag);
328 /// Can treat as a long-flag
329 pub fn is_long(&self) -> bool {
330 self.inner.starts_with("--") && !self.is_escape()
333 /// Treat as a short-flag
334 pub fn to_short(&self) -> Option<ShortFlags<'_>> {
335 if let Some(remainder_os) = self.inner.strip_prefix("-") {
336 if remainder_os.starts_with("-") {
338 } else if remainder_os.is_empty() {
339 debug_assert!(self.is_stdio());
342 Some(ShortFlags::new(remainder_os))
349 /// Can treat as a short-flag
350 pub fn is_short(&self) -> bool {
351 self.inner.starts_with("-") && !self.is_stdio() && !self.inner.starts_with("--")
356 /// **NOTE:** May return a flag or an escape.
357 pub fn to_value_os(&self) -> &OsStr {
363 /// **NOTE:** May return a flag or an escape.
364 pub fn to_value(&self) -> Result<&str, &OsStr> {
365 self.inner.to_str().ok_or(self.inner)
368 /// Safely print an argument that may contain non-UTF8 content
370 /// This may perform lossy conversion, depending on the platform. If you would like an implementation which escapes the path please use Debug instead.
371 pub fn display(&self) -> impl std::fmt::Display + '_ {
372 self.inner.to_string_lossy()
376 /// Walk through short flags within a [`ParsedArg`]
377 #[derive(Clone, Debug)]
378 pub struct ShortFlags<'s> {
380 utf8_prefix: std::str::CharIndices<'s>,
381 invalid_suffix: Option<&'s OsStr>,
384 impl<'s> ShortFlags<'s> {
385 fn new(inner: &'s OsStr) -> Self {
386 let (utf8_prefix, invalid_suffix) = split_nonutf8_once(inner);
387 let utf8_prefix = utf8_prefix.char_indices();
395 /// Move the iterator forward by `n` short flags
396 pub fn advance_by(&mut self, n: usize) -> Result<(), usize> {
398 self.next().ok_or(i)?.map_err(|_| i)?;
403 /// No short flags left
404 pub fn is_empty(&self) -> bool {
405 self.invalid_suffix.is_none() && self.utf8_prefix.as_str().is_empty()
408 /// Does the short flag look like a number
410 /// Ideally call this before doing any iterator
411 pub fn is_number(&self) -> bool {
412 self.invalid_suffix.is_none() && self.utf8_prefix.as_str().parse::<f64>().is_ok()
415 /// Advance the iterator, returning the next short flag on success
417 /// On error, returns the invalid-UTF8 value
418 pub fn next_flag(&mut self) -> Option<Result<char, &'s OsStr>> {
419 if let Some((_, flag)) = self.utf8_prefix.next() {
420 return Some(Ok(flag));
423 if let Some(suffix) = self.invalid_suffix {
424 self.invalid_suffix = None;
425 return Some(Err(suffix));
431 /// Advance the iterator, returning everything left as a value
432 pub fn next_value_os(&mut self) -> Option<&'s OsStr> {
433 if let Some((index, _)) = self.utf8_prefix.next() {
434 self.utf8_prefix = "".char_indices();
435 self.invalid_suffix = None;
436 // SAFETY: `char_indices` ensures `index` is at a valid UTF-8 boundary
437 let remainder = unsafe { ext::split_at(self.inner, index).1 };
438 return Some(remainder);
441 if let Some(suffix) = self.invalid_suffix {
442 self.invalid_suffix = None;
450 impl<'s> Iterator for ShortFlags<'s> {
451 type Item = Result<char, &'s OsStr>;
453 fn next(&mut self) -> Option<Self::Item> {
458 fn split_nonutf8_once(b: &OsStr) -> (&str, Option<&OsStr>) {
462 // SAFETY: `char_indices` ensures `index` is at a valid UTF-8 boundary
463 let (valid, after_valid) = unsafe { ext::split_at(b, err.valid_up_to()) };
464 let valid = valid.try_str().unwrap();
465 (valid, Some(after_valid))