getopt_iter/lib.rs
1#![no_std]
2
3//! A POSIX-compliant command-line option parser with GNU extensions.
4//!
5//! This library provides an iterator-based interface for parsing command-line options
6//! in both `std` and `no_std` environments.
7//!
8//! # Examples
9//!
10//! ## Basic Usage with `std`
11//!
12//! ```
13//! use getopt_iter::Getopt;
14//!
15//! let args = vec!["myapp", "-a", "-b", "value", "file.txt"];
16//! let mut getopt = Getopt::new(args.iter().copied(), "ab:");
17//!
18//! while let Some(opt) = getopt.next() {
19//! match opt.val() {
20//! 'a' => println!("Option a"),
21//! 'b' => println!("Option b with arg: {}", opt.arg().unwrap()),
22//! '?' => eprintln!("Unknown option: {:?}", opt.erropt()),
23//! _ => {}
24//! }
25//! }
26//!
27//! // Process remaining positional arguments
28//! for arg in getopt.remaining() {
29//! println!("File: {}", arg);
30//! }
31//! ```
32//!
33//! ## `no_std` Usage with C FFI (argc/argv)
34//!
35//! For bare-metal or embedded environments where you receive C-style `argc` and `argv`
36//! parameters, you can wrap them in an iterator that yields `&core::ffi::CStr`:
37//!
38//! ```
39//! #![no_std]
40//! #![no_main]
41//!
42//! extern crate alloc;
43//! use core::ffi::CStr;
44//! use core::slice;
45//! use getopt_iter::Getopt;
46//!
47//! /// Iterator that wraps raw C argc/argv pointers
48//! struct ArgvIter {
49//! argv: *const *const i8,
50//! current: isize,
51//! end: isize,
52//! }
53//!
54//! impl ArgvIter {
55//! unsafe fn new(argc: i32, argv: *const *const i8) -> Self {
56//! Self {
57//! argv,
58//! current: 0,
59//! end: argc as isize,
60//! }
61//! }
62//! }
63//!
64//! impl Iterator for ArgvIter {
65//! type Item = &'static CStr;
66//!
67//! fn next(&mut self) -> Option<Self::Item> {
68//! if self.current >= self.end {
69//! return None;
70//! }
71//!
72//! unsafe {
73//! let arg_ptr = *self.argv.offset(self.current);
74//! self.current += 1;
75//!
76//! if arg_ptr.is_null() {
77//! None
78//! } else {
79//! Some(CStr::from_ptr(arg_ptr))
80//! }
81//! }
82//! }
83//! }
84//!
85//! #[unsafe(no_mangle)]
86//! pub extern "C" fn main(argc: i32, argv: *const *const i8) -> i32 {
87//! // Wrap the raw pointers in our iterator
88//! let args = unsafe { ArgvIter::new(argc, argv) };
89//!
90//! // Parse options using getopt
91//! let mut getopt = Getopt::new(args, "hvf:");
92//! getopt.set_opterr(false); // Suppress error messages in no_std environment
93//!
94//! let mut verbose = false;
95//! let mut filename = None;
96//!
97//! while let Some(opt) = getopt.next() {
98//! match opt.val() {
99//! 'h' => {
100//! // Print help
101//! return 0;
102//! }
103//! 'v' => verbose = true,
104//! 'f' => filename = opt.into_arg(),
105//! '?' => return 1, // Unknown option
106//! ':' => return 1, // Missing argument
107//! _ => {}
108//! }
109//! }
110//!
111//! // Process remaining arguments
112//! for arg in getopt.remaining() {
113//! // Process positional argument (arg is &CStr)
114//! // Note: In no_std, you cannot print to stdout/stderr
115//! // without custom panic/print handlers
116//! }
117//!
118//! 0
119//! }
120//! ```
121//!
122//! The `ArgvIter` adapter safely wraps the raw C pointers and yields `&CStr` references,
123//! which are automatically converted to `String` by the `ArgV` trait implementation.
124//! This allows seamless integration with C environments while maintaining memory safety
125//! within the iterator abstraction.
126
127extern crate alloc;
128use alloc::borrow::{Cow, ToOwned};
129use alloc::string::{String, ToString};
130use core::iter::Peekable;
131
132#[cfg(feature = "std")]
133extern crate std;
134
135#[cfg(feature = "std")]
136use std::ffi::{OsStr, OsString};
137
138/// Represents the result of parsing a single command-line option.
139///
140/// This structure contains information about a parsed option, including
141/// the option character, any error that occurred during parsing, and
142/// the option's argument if one was provided.
143///
144/// Fields are private; use the [`val`](Self::val), [`erropt`](Self::erropt),
145/// [`arg`](Self::arg), and [`into_arg`](Self::into_arg) accessors.
146///
147/// The argument is stored as a `Cow<'static, str>` so that borrowed inputs
148/// (string literals, `&'static OsStr`/`&'static CStr` from sources such as
149/// the [`argv`](https://crates.io/crates/argv) crate) flow through without
150/// allocation when they don't need to be sliced.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct Opt {
153 /// The option character that was parsed, or '?' for errors, or ':' for missing arguments
154 val: char,
155 /// The option character that caused an error, if any
156 erropt: Option<char>,
157 /// The argument associated with this option, if any
158 arg: Option<Cow<'static, str>>,
159}
160
161impl Opt {
162 /// Returns the option character that was parsed.
163 ///
164 /// This can be:
165 /// - The actual option character if it was valid
166 /// - '?' if an unknown option was encountered
167 /// - ':' if a missing argument was detected and optstring starts with ':'
168 #[must_use]
169 pub fn val(&self) -> char {
170 self.val
171 }
172
173 /// Returns the error option character if an error occurred during parsing.
174 ///
175 /// Returns:
176 /// - `Some(char)` containing the problematic option character if:
177 /// - An unknown option was encountered
178 /// - A required argument was missing
179 /// - `None` if no error occurred
180 #[must_use]
181 pub fn erropt(&self) -> Option<char> {
182 self.erropt
183 }
184
185 /// Returns the argument associated with the option, if any.
186 ///
187 /// Returns:
188 /// - `Some(&str)` containing the option's argument if one was provided
189 /// - `None` if the option takes no argument or if a required argument was missing
190 #[must_use]
191 pub fn arg(&self) -> Option<&str> {
192 self.arg.as_deref()
193 }
194
195 /// Consumes `self` and returns the argument associated with the option, if any.
196 ///
197 /// The returned `Cow` borrows from the original input when possible (e.g. when the
198 /// argument was passed as a separate `&'static str` or valid-UTF-8 `&'static OsStr`),
199 /// and only allocates when the parser had to slice into a larger argument (e.g.
200 /// `-ofile.txt`).
201 ///
202 /// # Returns
203 /// - `Some(Cow<'static, str>)` containing the option's argument if one was provided
204 /// - `None` if the option takes no argument or if a required argument was missing
205 #[must_use]
206 pub fn into_arg(self) -> Option<Cow<'static, str>> {
207 self.arg
208 }
209}
210
211impl PartialEq<char> for Opt {
212 fn eq(&self, other: &char) -> bool {
213 self.val == *other
214 }
215}
216
217mod sealed {
218 pub trait Sealed {}
219
220 impl Sealed for &str {}
221 impl Sealed for &&str {}
222 impl Sealed for alloc::string::String {}
223 impl Sealed for &core::ffi::CStr {}
224 #[cfg(feature = "std")]
225 impl Sealed for std::ffi::OsString {}
226 #[cfg(feature = "std")]
227 impl Sealed for &std::ffi::OsStr {}
228}
229
230/// Trait for types that can be converted into strings for use as command-line arguments.
231///
232/// This trait is implemented for common string types and enables the library to work
233/// with different argument representations. It is sealed and cannot be implemented
234/// outside of this crate.
235///
236/// Borrowed implementations are bounded by `'static` so they can flow through the
237/// parser as `Cow::Borrowed` without allocation. This makes the crate a good fit
238/// for sources like the [`argv`](https://crates.io/crates/argv) crate, which yields
239/// `&'static OsStr`. Owned types (`String`, `OsString`) have no lifetime constraint.
240///
241/// # Implementations
242/// - `&'static str` - String slices (zero-copy)
243/// - `String` - Owned strings (zero-copy, takes ownership)
244/// - `&'static CStr` - C-style nul-terminated strings (zero-copy when valid UTF-8)
245/// - `OsString` - Platform-native strings (zero-copy when valid UTF-8; requires `std`)
246/// - `&'static OsStr` - Borrowed platform-native strings (zero-copy when valid UTF-8;
247/// requires `std`)
248pub trait ArgV: sealed::Sealed {
249 /// Converts self into a `Cow<'static, str>`.
250 ///
251 /// For `OsString`/`OsStr`/`CStr`, invalid UTF-8 sequences are replaced with the
252 /// Unicode replacement character (U+FFFD), which forces an allocation. Valid
253 /// UTF-8 input is passed through without copying.
254 fn into_argv(self) -> Cow<'static, str>;
255
256 /// Non-destructively view self as a `&str`.
257 ///
258 /// Used internally to inspect an argument (e.g. to decide whether it begins with `-`)
259 /// without consuming it, so that non-option arguments can be left in place for
260 /// [`Getopt::remaining`].
261 ///
262 /// For `OsStr`/`OsString`/`CStr`, invalid UTF-8 sequences are replaced with the
263 /// Unicode replacement character (U+FFFD).
264 fn as_argv(&self) -> Cow<'_, str>;
265}
266
267impl ArgV for &'static str {
268 fn into_argv(self) -> Cow<'static, str> {
269 Cow::Borrowed(self)
270 }
271 fn as_argv(&self) -> Cow<'_, str> {
272 Cow::Borrowed(self)
273 }
274}
275
276impl ArgV for &&'static str {
277 fn into_argv(self) -> Cow<'static, str> {
278 Cow::Borrowed(*self)
279 }
280 fn as_argv(&self) -> Cow<'_, str> {
281 Cow::Borrowed(*self)
282 }
283}
284
285impl ArgV for String {
286 fn into_argv(self) -> Cow<'static, str> {
287 Cow::Owned(self)
288 }
289 fn as_argv(&self) -> Cow<'_, str> {
290 Cow::Borrowed(self.as_str())
291 }
292}
293
294#[cfg(feature = "std")]
295impl ArgV for OsString {
296 fn into_argv(self) -> Cow<'static, str> {
297 match self.into_string() {
298 Ok(s) => Cow::Owned(s),
299 Err(s) => Cow::Owned(s.to_string_lossy().into_owned()),
300 }
301 }
302 fn as_argv(&self) -> Cow<'_, str> {
303 self.as_os_str().to_string_lossy()
304 }
305}
306
307#[cfg(feature = "std")]
308impl ArgV for &'static OsStr {
309 fn into_argv(self) -> Cow<'static, str> {
310 self.to_string_lossy()
311 }
312 fn as_argv(&self) -> Cow<'_, str> {
313 (*self).to_string_lossy()
314 }
315}
316
317impl ArgV for &'static core::ffi::CStr {
318 fn into_argv(self) -> Cow<'static, str> {
319 self.to_string_lossy()
320 }
321 fn as_argv(&self) -> Cow<'_, str> {
322 (*self).to_string_lossy()
323 }
324}
325
326/// State management for getopt parsing
327pub struct Getopt<'a, V, I: Iterator<Item = V>> {
328 /// Iterator over arguments. Wrapped in [`Peekable`] so the parser can
329 /// inspect the next argument (via [`ArgV::as_argv`]) without consuming it;
330 /// non-option arguments are left in place so [`Getopt::remaining`] can
331 /// return them to the caller.
332 iter: Peekable<I>,
333
334 /// The current argument being parsed, in string form. Set only while we are
335 /// actively walking through an option-bearing argument (e.g. `-abc` or `--long`).
336 current_arg: Option<Cow<'static, str>>,
337
338 /// argv\[0\]
339 prog_name: Cow<'static, str>,
340
341 /// Current position within the current argument
342 sp: usize,
343
344 /// Print errors to stderr
345 #[cfg_attr(not(feature = "std"), allow(dead_code))]
346 opterr: bool,
347
348 /// Option specification string (as bytes)
349 optstring: &'a [u8],
350}
351
352macro_rules! err {
353 ($self:ident, $fmt:literal $(, $arg:expr)*) => {
354 {
355 #[cfg(feature = "std")]
356 if $self.opterr && !$self.optstring.is_empty() && $self.optstring[0] != b':' {
357 std::eprintln!($fmt, $self.prog_name() $(, $arg)*);
358 }
359 }
360 };
361}
362
363impl<'a, V: ArgV, I: Iterator<Item = V>> Getopt<'a, V, I> {
364 /// Create a new Getopt parser from an iterator
365 ///
366 /// # Arguments
367 /// * `args` - An iterator or iterable yielding command-line arguments. The first element
368 /// should be the program name (argv\[0\]), which is consumed but not returned as an option.
369 /// * `optstring` - Option specification string following POSIX conventions:
370 /// - Single character defines an option (e.g., `"a"` allows `-a`)
371 /// - Character followed by `:` requires an argument (e.g., `"a:"` requires `-a value`)
372 /// - Character followed by `::` makes argument optional (GNU extension)
373 /// - Leading `:` suppresses error messages and changes error return values
374 /// - Leading `+` stops at first non-option (POSIX mode)
375 /// - Parenthesized names define long options (e.g., `"h(help)"` allows `--help`)
376 ///
377 /// Error messages are printed to stderr by default (when the `std` feature is enabled),
378 /// in accordance with POSIX specifications. Use [`set_opterr`](Self::set_opterr) to disable them.
379 ///
380 /// # Examples
381 /// ```
382 /// use getopt_iter::Getopt;
383 ///
384 /// let args = vec!["myapp", "-a", "-b", "value"];
385 /// let mut getopt = Getopt::new(args, "ab:");
386 /// ```
387 pub fn new<A: IntoIterator<Item = V, IntoIter = I>>(args: A, optstring: &'a str) -> Self {
388 let mut iter = args.into_iter();
389 // program name (first argument)
390 let prog_name = iter.next().map(ArgV::into_argv).unwrap_or_default();
391
392 Getopt {
393 iter: iter.peekable(),
394 current_arg: None,
395 prog_name,
396 sp: 1,
397 opterr: true,
398 optstring: optstring.as_bytes(),
399 }
400 }
401
402 /// Set whether error messages should be printed to stderr.
403 ///
404 /// By default, error messages are printed to stderr (when the `std` feature is enabled),
405 /// in accordance with POSIX specifications. Call this method with `false` to suppress
406 /// error output.
407 ///
408 /// # Arguments
409 /// * `opterr` - Whether to print error messages to stderr (requires `std` feature)
410 ///
411 /// # Examples
412 /// ```
413 /// use getopt_iter::Getopt;
414 ///
415 /// let args = vec!["myapp", "-x"];
416 /// let mut getopt = Getopt::new(args, "ab:");
417 /// getopt.set_opterr(false); // Suppress error messages
418 /// ```
419 pub fn set_opterr(&mut self, opterr: bool) {
420 self.opterr = opterr;
421 }
422
423 /// Consumes `self` and returns an iterator over the remaining arguments.
424 ///
425 /// The returned iterator yields the same item type `V` as the iterator originally
426 /// passed to [`Getopt::new`]. It includes any non-option argument that was peeked
427 /// at by the parser (and caused option parsing to stop) followed by the rest of
428 /// the underlying iterator, so no arguments are lost.
429 ///
430 /// # Examples
431 /// ```
432 /// use getopt_iter::Getopt;
433 ///
434 /// let args = &["prog", "-a", "file1", "file2"];
435 /// let mut getopt = Getopt::new(args, "a");
436 /// getopt.next(); // Parse -a
437 /// for arg in getopt.remaining() {
438 /// println!("Positional arg: {}", arg);
439 /// }
440 /// ```
441 pub fn remaining(self) -> Peekable<I> {
442 self.iter
443 }
444
445 /// Returns the program name, typically the basename of argv\[0\].
446 ///
447 /// The program name is extracted from the first argument (argv\[0\]) during initialization.
448 /// It is the basename of the path (all characters after the last '/' are used as the program name).
449 /// If the iterator is empty or argv\[0\] is empty, an empty string is returned.
450 ///
451 /// # Examples
452 ///
453 /// ```
454 /// let args = vec!["myapp", "-a"];
455 /// let getopt = getopt_iter::Getopt::new(args.into_iter(), "a");
456 /// assert_eq!(getopt.prog_name(), "myapp");
457 ///
458 /// #[cfg(unix)]
459 /// let args = vec!["/usr/bin/myapp", "-a"];
460 ///
461 /// #[cfg(windows)]
462 /// let args = vec!["C:\\Program Files\\myapp", "-a"];
463 ///
464 /// let getopt = getopt_iter::Getopt::new(args.into_iter(), "a");
465 /// assert_eq!(getopt.prog_name(), "myapp");
466 /// ```
467 pub fn prog_name(&self) -> &str {
468 #[cfg(feature = "std")]
469 const PATH_SEPARATOR: char = std::path::MAIN_SEPARATOR;
470 #[cfg(all(not(feature = "std"), windows))]
471 const PATH_SEPARATOR: char = '\\';
472 #[cfg(all(not(feature = "std"), not(windows)))]
473 const PATH_SEPARATOR: char = '/';
474
475 let s = &self.prog_name;
476 // lazily find basename to avoid allocation
477 match s.rfind(PATH_SEPARATOR) {
478 Some(idx) => &s[(idx + 1)..],
479 None => s,
480 }
481 }
482
483 /// Determine if the specified character is present in optstring as a regular short option.
484 /// Returns the index in optstring if found, None otherwise.
485 /// Only ASCII characters are valid short options; the syntactic characters ':', '(',
486 /// and ')' are excluded.
487 fn parse_short(&self, c: char) -> Option<usize> {
488 if !c.is_ascii() || c == ':' || c == '(' || c == ')' {
489 return None;
490 }
491
492 let mut i = 0;
493
494 while i < self.optstring.len() {
495 if self.optstring[i] == c as u8 {
496 return Some(i);
497 }
498 // Skip over parenthesized long options
499 while i < self.optstring.len() && self.optstring[i] == b'(' {
500 while i < self.optstring.len() && self.optstring[i] != b')' {
501 i += 1;
502 }
503 }
504 i += 1;
505 }
506 None
507 }
508
509 /// Determine if a long option is present in optstring.
510 /// Returns tuple of (index in optstring of short-option char, `option_argument`) if found.
511 fn parse_long(&self, opt: &'a str) -> Option<(usize, Option<&'a str>)> {
512 if self.optstring.is_empty() {
513 return None;
514 }
515 let opt = opt.as_bytes();
516 // index in optstring, beginning of one option spec
517 let mut cp_idx = 0usize;
518 // index in optstring, traverses every char
519 let mut ip_idx = 0usize;
520 // index of opt
521 let mut op_idx: usize;
522 // if opt is matching part of optstring
523 let mut is_match: bool;
524
525 loop {
526 if self.optstring[ip_idx] != b'(' {
527 ip_idx += 1;
528 if ip_idx == self.optstring.len() {
529 break;
530 }
531 }
532 if self.optstring[ip_idx] == b':' {
533 ip_idx += 1;
534 if ip_idx == self.optstring.len() {
535 break;
536 }
537 }
538 while self.optstring[ip_idx] == b'(' {
539 ip_idx += 1;
540 if ip_idx == self.optstring.len() {
541 break;
542 }
543 // if opt is matching part of optstring
544 is_match = true;
545 op_idx = 0;
546 while ip_idx < self.optstring.len()
547 && op_idx < opt.len()
548 && self.optstring[ip_idx] != b')'
549 {
550 is_match = self.optstring[ip_idx] == opt[op_idx] && is_match;
551 ip_idx += 1;
552 op_idx += 1;
553 }
554
555 if ip_idx >= self.optstring.len() {
556 break;
557 }
558 if is_match
559 && self.optstring[ip_idx] == b')'
560 && (op_idx == opt.len() || opt[op_idx] == b'=')
561 {
562 let longoptarg = if op_idx != opt.len() && opt[op_idx] == b'=' {
563 // SAFETY: we know this is a valid char boundary
564 // since we only skipped over leading ascii bytes
565 Some(unsafe { core::str::from_utf8_unchecked(&opt[op_idx + 1..]) })
566 } else {
567 None
568 };
569 return Some((cp_idx, longoptarg));
570 }
571 if self.optstring[ip_idx] == b')' {
572 ip_idx += 1;
573 if ip_idx == self.optstring.len() {
574 break;
575 }
576 }
577 }
578 cp_idx = ip_idx;
579 // Handle double-colon in optstring ("a::(longa)")
580 // The old getopt() accepts it and treats it as a
581 // required argument.
582 while cp_idx < self.optstring.len() && cp_idx > 0 && self.optstring[cp_idx] == b':' {
583 cp_idx -= 1;
584 }
585
586 if cp_idx == self.optstring.len() {
587 break;
588 }
589 }
590
591 None
592 }
593
594 /// Parse command line arguments. Returns the next option found.
595 #[allow(clippy::too_many_lines)]
596 fn parse_next(&mut self) -> Option<Opt> {
597 // Load next argument if needed.
598 //
599 // When `sp == 1` we are between arguments. Peek at the next `V` non-destructively
600 // via `ArgV::as_argv` so that, if it turns out to be a non-option (or `-`), we can
601 // leave it untouched in the iterator for `remaining()` to yield back to the caller.
602 // Only options and the `--` terminator are actually consumed here.
603 if self.sp == 1 {
604 let next_v = self.iter.peek()?;
605 let view = next_v.as_argv();
606 if !view.starts_with('-') || view.as_ref() == "-" {
607 // Non-option: leave it in the iterator for remaining().
608 return None;
609 }
610 if view.as_ref() == "--" {
611 // Consume and stop.
612 drop(view);
613 self.iter.next();
614 return None;
615 }
616 drop(view);
617 // Commit: it's an option-bearing argument.
618 self.current_arg = Some(self.iter.next().unwrap().into_argv());
619 }
620
621 let current_arg = match &self.current_arg {
622 Some(arg) => arg,
623 None => return None,
624 };
625
626 // Getting this far indicates that an option has been encountered.
627
628 let mut optopt = current_arg.as_bytes()[self.sp] as char;
629
630 // If the second character of the argument is a '-' this must be
631 // a long-option, otherwise it must be a short option.
632 let is_longopt = self.sp == 1 && optopt == '-';
633
634 // Try to find the option in optstring
635 let cp_result = if is_longopt {
636 self.parse_long(¤t_arg[2..])
637 } else {
638 self.parse_short(optopt).map(|idx| (idx, None))
639 };
640
641 let (cp, longoptarg) = if let Some(result) = cp_result {
642 result
643 } else {
644 // Unrecognized option
645 #[cfg_attr(not(feature = "std"), allow(unused_variables))]
646 let opt_display = if is_longopt {
647 current_arg[2..].to_string()
648 } else {
649 optopt.to_string()
650 };
651 err!(self, "{}: illegal option -- {}", opt_display);
652 if current_arg.len() > self.sp + 1 && !is_longopt {
653 self.sp += 1;
654 } else {
655 self.current_arg = None;
656 self.sp = 1;
657 }
658 // If getopt() encounters an option character that is not contained in optstring,
659 // it shall return the question-mark ( '?' ) character.
660 // getopt() shall set the variable optopt to the option character that caused the error.
661 return Some(Opt {
662 val: '?',
663 erropt: Some(optopt),
664 arg: None,
665 });
666 };
667
668 // A valid option has been identified. If it should have an
669 // option-argument, process that now.
670 optopt = self.optstring[cp] as char;
671
672 let takes_arg = self.optstring.get(cp + 1).map_or(false, |&b| b == b':');
673
674 let optarg: Option<Cow<'static, str>>;
675
676 if takes_arg {
677 if !is_longopt && current_arg.len() > self.sp + 1 {
678 // Attached short-option argument (e.g. `-ofile.txt`). The slice cannot
679 // outlive `current_arg`, so an allocation is required here.
680 optarg = Some(Cow::Owned(current_arg[self.sp + 1..].to_owned()));
681 self.current_arg = None;
682 self.sp = 1;
683 } else if is_longopt && longoptarg.is_some() {
684 // Long-option argument from `--name=value`. Same constraint: the
685 // borrowed slice is tied to `current_arg`, so we must own it.
686 optarg = longoptarg.map(|s| Cow::Owned(s.to_owned()));
687 self.current_arg = None;
688 self.sp = 1;
689 } else if let Some(next_arg) = self.iter.next() {
690 // Separate argument (`-o value` / `--name value`): pass the `Cow`
691 // through unchanged — zero-copy for borrowed `'static` inputs.
692 optarg = Some(next_arg.into_argv());
693 self.current_arg = None;
694 self.sp = 1;
695 } else {
696 err!(self, "{}: option requires an argument -- {}", optopt);
697 self.sp = 1;
698 self.current_arg = None;
699 return if !self.optstring.is_empty() && self.optstring[0] == (b':') {
700 Some(Opt {
701 val: ':',
702 erropt: Some(optopt),
703 arg: None,
704 })
705 } else {
706 Some(Opt {
707 val: '?',
708 erropt: Some(optopt),
709 arg: None,
710 })
711 };
712 }
713 } else {
714 // The option does NOT take an argument
715 if is_longopt && longoptarg.is_some() {
716 err!(
717 self,
718 "{}: option doesn't take an argument -- {}",
719 ¤t_arg[2..]
720 );
721 self.current_arg = None;
722 self.sp = 1;
723 return Some(Opt {
724 val: '?',
725 erropt: Some(optopt),
726 arg: None,
727 });
728 }
729
730 if is_longopt || self.sp + 1 >= current_arg.len() {
731 self.sp = 1;
732 self.current_arg = None;
733 } else {
734 self.sp += 1;
735 }
736 optarg = None;
737 }
738
739 Some(Opt {
740 val: optopt,
741 erropt: None,
742 arg: optarg,
743 })
744 }
745}
746
747impl<V: ArgV, I: Iterator<Item = V>> Iterator for Getopt<'_, V, I> {
748 type Item = Opt;
749
750 fn next(&mut self) -> Option<Self::Item> {
751 self.parse_next()
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 #[test]
760 fn test_argv_conversion() {
761 use core::ffi::CStr;
762
763 // Helper function to ensure we're calling the ArgV trait method
764 fn convert<T: ArgV>(arg: T) -> Cow<'static, str> {
765 arg.into_argv()
766 }
767
768 // Test &str conversion
769 let s: &str = "hello";
770 assert_eq!(convert(s), "hello");
771
772 // Test &&str conversion
773 let s: &str = "world";
774 let ss: &&str = &s;
775 assert_eq!(convert(ss), "world");
776
777 // Test String conversion (identity)
778 let s = String::from("test");
779 assert_eq!(convert(s), "test");
780
781 // Test &CStr conversion (valid UTF-8)
782 let bytes = b"hello\0";
783 let cstr = CStr::from_bytes_with_nul(bytes).unwrap();
784 assert_eq!(convert(cstr), "hello");
785
786 // Test &CStr conversion with lossy UTF-8
787 // Create a CStr with invalid UTF-8 sequence
788 let bytes_with_invalid_utf8 = b"hello\xFF\xFEworld\0";
789 let cstr = CStr::from_bytes_with_nul(bytes_with_invalid_utf8).unwrap();
790 // The invalid bytes should be replaced with replacement character
791 assert_eq!(convert(cstr), "hello��world");
792
793 // Test OsString conversion (std feature only)
794 #[cfg(feature = "std")]
795 {
796 use std::ffi::{OsStr, OsString};
797
798 // Valid UTF-8 OsString
799 let os = OsString::from("valid");
800 assert_eq!(convert(os), "valid");
801
802 // Invalid UTF-8 sequence
803 #[cfg(unix)]
804 {
805 let os = unsafe {
806 OsString::from_encoded_bytes_unchecked(b"hello\xFF\xFEworld".to_vec())
807 };
808 assert_eq!(convert(os), "hello��world");
809 }
810
811 // Test that OsString with valid UTF-8 works as expected
812 let os = OsString::from("test123");
813 assert_eq!(convert(os), "test123");
814
815 // Test &'static OsStr conversion
816 let os: &'static OsStr = OsStr::new("static_osstr");
817 assert_eq!(convert(os), "static_osstr");
818 }
819 }
820
821 #[test]
822 fn test_single_short_option() {
823 let args = &["prog", "-a"];
824 let mut getopt = Getopt::new(args, "ab");
825 let result = getopt.next();
826
827 assert_eq!(
828 result,
829 Some(Opt {
830 val: 'a',
831 erropt: None,
832 arg: None
833 })
834 );
835 }
836
837 #[test]
838 fn test_multiple_short_options() {
839 let args = &["prog", "-a", "-b"];
840 let mut getopt = Getopt::new(args, "ab");
841
842 let r1 = getopt.next();
843 assert_eq!(
844 r1,
845 Some(Opt {
846 val: 'a',
847 erropt: None,
848 arg: None
849 })
850 );
851
852 let r2 = getopt.next();
853 assert_eq!(
854 r2,
855 Some(Opt {
856 val: 'b',
857 erropt: None,
858 arg: None
859 })
860 );
861 }
862
863 #[test]
864 fn test_aggregated_short_options() {
865 let args = &["prog", "-abc"];
866 let mut getopt = Getopt::new(args, "abc");
867
868 let r1 = getopt.next();
869 assert_eq!(
870 r1,
871 Some(Opt {
872 val: 'a',
873 erropt: None,
874 arg: None
875 })
876 );
877
878 let r2 = getopt.next();
879 assert_eq!(
880 r2,
881 Some(Opt {
882 val: 'b',
883 erropt: None,
884 arg: None
885 })
886 );
887
888 let r3 = getopt.next();
889 assert_eq!(
890 r3,
891 Some(Opt {
892 val: 'c',
893 erropt: None,
894 arg: None
895 })
896 );
897 }
898
899 #[test]
900 fn test_short_option_with_attached_argument() {
901 let args = &["prog", "-avalue"];
902 let mut getopt = Getopt::new(args, "a:");
903
904 let result = getopt.next();
905 assert_eq!(
906 result,
907 Some(Opt {
908 val: 'a',
909 erropt: None,
910 arg: Some("value".into())
911 })
912 );
913 }
914
915 #[test]
916 fn test_short_option_with_separate_argument() {
917 let args = &["prog", "-a", "value"];
918 let mut getopt = Getopt::new(args, "a:");
919
920 let result = getopt.next();
921 assert_eq!(
922 result,
923 Some(Opt {
924 val: 'a',
925 erropt: None,
926 arg: Some("value".into())
927 })
928 );
929 }
930
931 #[test]
932 fn test_long_option_simple() {
933 let args = &["prog", "--help"];
934 let mut getopt = Getopt::new(args, ":h(help)");
935
936 let result = getopt.next();
937 assert_eq!(
938 result,
939 Some(Opt {
940 val: 'h',
941 erropt: None,
942 arg: None
943 })
944 );
945 }
946
947 #[test]
948 fn test_long_short_mixed() {
949 let args = &["prog", "-V"];
950 let mut getopt = Getopt::new(args, ":h(help)V(version)x:(execute)");
951
952 let result = getopt.next();
953 assert_eq!(
954 result,
955 Some(Opt {
956 val: 'V',
957 erropt: None,
958 arg: None
959 })
960 );
961
962 let args = &["prog", "-x"];
963 let mut getopt = Getopt::new(args, ":h(help)V(version)x:(execute)");
964
965 let result = getopt.next();
966 assert_eq!(
967 result,
968 Some(Opt {
969 val: ':',
970 erropt: Some('x'),
971 arg: None
972 })
973 );
974
975 let args = &["prog", "--execute", "cmd"];
976 let mut getopt = Getopt::new(args, ":h(help)V(version)x:(execute)");
977
978 let result = getopt.next();
979 assert_eq!(
980 result,
981 Some(Opt {
982 val: 'x',
983 erropt: None,
984 arg: Some("cmd".into()),
985 })
986 );
987 }
988
989 #[test]
990 fn test_long_option_with_argument() {
991 let args = &["prog", "--output=file.txt"];
992 let mut getopt = Getopt::new(args, "o:(output)");
993
994 let result = getopt.next();
995 assert_eq!(
996 result,
997 Some(Opt {
998 val: 'o',
999 erropt: None,
1000 arg: Some("file.txt".into())
1001 })
1002 );
1003 }
1004
1005 #[test]
1006 fn test_long_option_with_argument_double_colon() {
1007 let args = &["prog", "--output=file.txt"];
1008 let mut getopt = Getopt::new(args, "o::(output)");
1009
1010 let result = getopt.next();
1011 assert_eq!(
1012 result,
1013 Some(Opt {
1014 val: 'o',
1015 erropt: None,
1016 arg: Some("file.txt".into())
1017 })
1018 );
1019 }
1020
1021 #[test]
1022 fn test_multiple_option_with_argument() {
1023 let args = &["prog", "--output=file.txt"];
1024 let mut getopt = Getopt::new(args, "o:(outfile)(output)");
1025
1026 let result = getopt.next();
1027 assert_eq!(
1028 result,
1029 Some(Opt {
1030 val: 'o',
1031 erropt: None,
1032 arg: Some("file.txt".into())
1033 })
1034 );
1035 assert!(getopt.next().is_none());
1036
1037 // with outfile instead
1038 let args = &["prog", "--outfile=file.txt"];
1039 let mut getopt = Getopt::new(args, "o:(outfile)(output)");
1040
1041 let result = getopt.next();
1042 assert_eq!(
1043 result,
1044 Some(Opt {
1045 val: 'o',
1046 erropt: None,
1047 arg: Some("file.txt".into())
1048 })
1049 );
1050 assert!(getopt.next().is_none());
1051 }
1052
1053 #[test]
1054 fn test_long_option_without_argument() {
1055 let args = &["prog", "--verbose=file.txt"];
1056 let mut getopt = Getopt::new(args, ":v(verbose)");
1057
1058 let result = getopt.next();
1059 assert_eq!(
1060 result,
1061 Some(Opt {
1062 val: '?',
1063 erropt: Some('v'),
1064 arg: None,
1065 })
1066 );
1067 }
1068
1069 #[test]
1070 fn test_end_of_options() {
1071 let args = &["prog", "-a", "file.txt"];
1072 let mut getopt = Getopt::new(args, "a");
1073
1074 let r1 = getopt.next();
1075 assert_eq!(
1076 r1,
1077 Some(Opt {
1078 val: 'a',
1079 erropt: None,
1080 arg: None
1081 })
1082 );
1083
1084 let r2 = getopt.next();
1085 assert_eq!(r2, None);
1086 }
1087
1088 #[test]
1089 fn test_double_dash_ends_options() {
1090 let args = &["prog", "--", "-a"];
1091 let mut getopt = Getopt::new(args, "a");
1092
1093 let result = getopt.next();
1094 assert_eq!(result, None);
1095 }
1096
1097 #[test]
1098 fn test_unrecognized_option() {
1099 let args = &["prog", "-x"];
1100 let mut getopt = Getopt::new(args, "ab");
1101 getopt.set_opterr(false);
1102
1103 let result = getopt.next();
1104 assert_eq!(
1105 result,
1106 Some(Opt {
1107 val: '?',
1108 erropt: Some('x'),
1109 arg: None
1110 })
1111 );
1112 }
1113
1114 #[test]
1115 fn test_remaining() {
1116 let args = &["prog", "-a", "file1.txt", "file2.txt"];
1117 let mut getopt = Getopt::new(args, "a");
1118
1119 // Parse the -a option
1120 let result = getopt.next();
1121 assert_eq!(
1122 result,
1123 Some(Opt {
1124 val: 'a',
1125 erropt: None,
1126 arg: None
1127 })
1128 );
1129
1130 // Consume remaining arguments
1131 let mut remaining = getopt.remaining();
1132 assert_eq!(remaining.next(), Some("file1.txt").as_ref());
1133 assert_eq!(remaining.next(), Some("file2.txt").as_ref());
1134 assert_eq!(remaining.next(), None);
1135 }
1136
1137 #[test]
1138 fn test_remaining_after_next_returns_none_on_positional() {
1139 // Regression: when next() encounters a non-option, the parser must NOT
1140 // discard that positional. remaining() must yield it.
1141 let args = &["prog", "-a", "file1.txt", "file2.txt"];
1142 let mut getopt = Getopt::new(args, "a");
1143
1144 assert_eq!(getopt.next().map(|o| o.val()), Some('a'));
1145 // Drives parse_next past the first positional.
1146 assert_eq!(getopt.next(), None);
1147 // Calling next() again must not discard the buffered positional either.
1148 assert_eq!(getopt.next(), None);
1149
1150 let mut remaining = getopt.remaining();
1151 assert_eq!(remaining.next(), Some("file1.txt").as_ref());
1152 assert_eq!(remaining.next(), Some("file2.txt").as_ref());
1153 assert_eq!(remaining.next(), None);
1154 }
1155
1156 #[test]
1157 fn test_remaining_after_double_dash() {
1158 // "--" terminator is consumed and should NOT appear in remaining().
1159 let args = &["prog", "-a", "--", "file1.txt", "file2.txt"];
1160 let mut getopt = Getopt::new(args, "a");
1161
1162 assert_eq!(getopt.next().map(|o| o.val()), Some('a'));
1163 assert_eq!(getopt.next(), None);
1164
1165 let mut remaining = getopt.remaining();
1166 assert_eq!(remaining.next(), Some("file1.txt").as_ref());
1167 assert_eq!(remaining.next(), Some("file2.txt").as_ref());
1168 assert_eq!(remaining.next(), None);
1169 }
1170
1171 // POSIX Compliance Tests
1172 // Reference: https://pubs.opengroup.org/onlinepubs/009696799/functions/getopt.html
1173
1174 #[test]
1175 fn posix_single_dash_alone_terminates_options() {
1176 // A single "-" by itself is not an option and terminates option processing
1177 let args = &["prog", "-", "-a"];
1178 let mut getopt = Getopt::new(args, "a");
1179
1180 let result = getopt.next();
1181 assert_eq!(result, None); // "-" stops option processing
1182 }
1183
1184 #[test]
1185 fn posix_option_argument_attached() {
1186 // Option argument can be attached to option: -avalue
1187 let args = &["prog", "-ofile.txt"];
1188 let mut getopt = Getopt::new(args, "o:");
1189
1190 let result = getopt.next();
1191 assert_eq!(
1192 result,
1193 Some(Opt {
1194 val: 'o',
1195 erropt: None,
1196 arg: Some("file.txt".into())
1197 })
1198 );
1199 }
1200
1201 #[test]
1202 fn posix_option_argument_separate() {
1203 // Option argument can be separate: -a value
1204 let args = &["prog", "-o", "file.txt"];
1205 let mut getopt = Getopt::new(args, "o:");
1206
1207 let result = getopt.next();
1208 assert_eq!(
1209 result,
1210 Some(Opt {
1211 val: 'o',
1212 erropt: None,
1213 arg: Some("file.txt".into())
1214 })
1215 );
1216 }
1217
1218 #[test]
1219 fn posix_aggregated_options() {
1220 // Multiple options can be aggregated: -abc
1221 let args = &["prog", "-abc"];
1222 let mut getopt = Getopt::new(args, "abc");
1223
1224 assert_eq!(
1225 getopt.next(),
1226 Some(Opt {
1227 val: 'a',
1228 erropt: None,
1229 arg: None
1230 })
1231 );
1232 assert_eq!(
1233 getopt.next(),
1234 Some(Opt {
1235 val: 'b',
1236 erropt: None,
1237 arg: None
1238 })
1239 );
1240 assert_eq!(
1241 getopt.next(),
1242 Some(Opt {
1243 val: 'c',
1244 erropt: None,
1245 arg: None
1246 })
1247 );
1248 }
1249
1250 #[test]
1251 fn posix_aggregated_with_argument() {
1252 // Aggregated options where last takes argument: -abf file
1253 let args = &["prog", "-abf", "file.txt"];
1254 let mut getopt = Getopt::new(args, "abf:");
1255
1256 assert_eq!(
1257 getopt.next(),
1258 Some(Opt {
1259 val: 'a',
1260 erropt: None,
1261 arg: None
1262 })
1263 );
1264 assert_eq!(
1265 getopt.next(),
1266 Some(Opt {
1267 val: 'b',
1268 erropt: None,
1269 arg: None
1270 })
1271 );
1272 let result = getopt.next();
1273 assert_eq!(
1274 result,
1275 Some(Opt {
1276 val: 'f',
1277 erropt: None,
1278 arg: Some("file.txt".into())
1279 })
1280 );
1281 }
1282
1283 #[test]
1284 fn posix_unknown_option_returns_question_mark() {
1285 let args = &["prog", "-x"];
1286 let mut getopt = Getopt::new(args, "ab");
1287 getopt.set_opterr(false);
1288
1289 let result = getopt.next();
1290 assert_eq!(
1291 result,
1292 Some(Opt {
1293 val: '?',
1294 erropt: Some('x'),
1295 arg: None
1296 })
1297 );
1298 }
1299
1300 #[test]
1301 fn posix_missing_argument_returns_question_mark() {
1302 // Missing required argument returns '?' when optstring doesn't start with ':'
1303 let args = &["prog", "-a"];
1304 let mut getopt = Getopt::new(args, "a:");
1305 getopt.set_opterr(false);
1306
1307 let result = getopt.next();
1308 assert_eq!(
1309 result,
1310 Some(Opt {
1311 val: '?',
1312 erropt: Some('a'),
1313 arg: None
1314 })
1315 );
1316 }
1317
1318 #[test]
1319 fn posix_missing_argument_returns_colon() {
1320 // Missing required argument returns ':' when optstring starts with ':'
1321 let args = &["prog", "-a"];
1322 let mut getopt = Getopt::new(args, ":a:");
1323 getopt.set_opterr(false);
1324
1325 let result = getopt.next();
1326 assert_eq!(
1327 result,
1328 Some(Opt {
1329 val: ':',
1330 erropt: Some('a'),
1331 arg: None
1332 })
1333 );
1334 }
1335
1336 #[test]
1337 fn posix_double_dash_terminates_options() {
1338 // Double dash "--" terminates option processing
1339 let args = &["prog", "-a", "--", "-b"];
1340 let mut getopt = Getopt::new(args, "ab");
1341
1342 assert_eq!(
1343 getopt.next(),
1344 Some(Opt {
1345 val: 'a',
1346 erropt: None,
1347 arg: None
1348 })
1349 );
1350 assert_eq!(getopt.next(), None); // "--" terminates
1351 }
1352
1353 #[test]
1354 fn posix_no_error_on_colon_prefix() {
1355 // optstring starting with ':' suppresses error messages
1356 let args = &["prog", "-x"];
1357 let mut getopt = Getopt::new(args, ":ab");
1358
1359 let result = getopt.next();
1360 assert_eq!(
1361 result,
1362 Some(Opt {
1363 val: '?',
1364 erropt: Some('x'),
1365 arg: None
1366 })
1367 );
1368 // Error message should not have been printed (tested implicitly)
1369 }
1370
1371 #[test]
1372 fn posix_option_with_no_argument() {
1373 // Option that doesn't take argument
1374 let args = &["prog", "-a", "file.txt"];
1375 let mut getopt = Getopt::new(args, "a");
1376
1377 let result = getopt.next();
1378 assert_eq!(
1379 result,
1380 Some(Opt {
1381 val: 'a',
1382 erropt: None,
1383 arg: None
1384 })
1385 );
1386 }
1387
1388 #[test]
1389 fn posix_mixed_options_and_operands() {
1390 // Options and non-options mixed per POSIX guideline 7
1391 // Example: cmd -a -b file1 file2
1392 let args = &["prog", "-a", "-b", "file1", "file2"];
1393 let mut getopt = Getopt::new(args, "ab");
1394
1395 assert_eq!(
1396 getopt.next(),
1397 Some(Opt {
1398 val: 'a',
1399 erropt: None,
1400 arg: None
1401 })
1402 );
1403 assert_eq!(
1404 getopt.next(),
1405 Some(Opt {
1406 val: 'b',
1407 erropt: None,
1408 arg: None
1409 })
1410 );
1411 // Next call sees non-option "file1", option processing stops
1412 assert_eq!(getopt.next(), None);
1413 }
1414
1415 #[test]
1416 fn posix_permutation_variant_1() {
1417 // Per spec examples: cmd -ao arg path path
1418 // (aggregated options where last takes argument)
1419 let args = &["prog", "-ao", "arg", "path"];
1420 let mut getopt = Getopt::new(args, "a:o:");
1421
1422 assert_eq!(
1423 getopt.next(),
1424 Some(Opt {
1425 val: 'a',
1426 erropt: None,
1427 arg: Some("o".into())
1428 })
1429 );
1430 assert_eq!(getopt.next(), None); // Rest are non-options
1431 }
1432
1433 #[test]
1434 fn posix_permutation_variant_2() {
1435 // Per spec examples: cmd -a -o arg path path
1436 // -a takes no argument, -o takes one
1437 let args = &["prog", "-a", "-o", "arg", "path"];
1438 let mut getopt = Getopt::new(args, "ao:");
1439
1440 assert_eq!(
1441 getopt.next(),
1442 Some(Opt {
1443 val: 'a',
1444 erropt: None,
1445 arg: None
1446 })
1447 );
1448 assert_eq!(
1449 getopt.next(),
1450 Some(Opt {
1451 val: 'o',
1452 erropt: None,
1453 arg: Some("arg".into())
1454 })
1455 );
1456 // Next call would see "path", which is not an option
1457 assert_eq!(getopt.next(), None);
1458 }
1459
1460 #[test]
1461 fn posix_option_order_independence() {
1462 // Options in any order: cmd -o arg -a path
1463 let args = &["prog", "-o", "arg", "-a", "path"];
1464 let mut getopt = Getopt::new(args, "a:o:");
1465
1466 let r1 = getopt.next();
1467 assert_eq!(
1468 r1,
1469 Some(Opt {
1470 val: 'o',
1471 erropt: None,
1472 arg: Some("arg".into())
1473 })
1474 );
1475
1476 let r2 = getopt.next();
1477 assert_eq!(
1478 r2,
1479 Some(Opt {
1480 val: 'a',
1481 erropt: None,
1482 arg: Some("path".into())
1483 })
1484 );
1485
1486 assert_eq!(getopt.next(), None);
1487 }
1488
1489 #[test]
1490 fn posix_attached_argument_in_aggregated() {
1491 // Per spec: cmd -oarg path path
1492 let args = &["prog", "-oarg", "path"];
1493 let mut getopt = Getopt::new(args, "o:");
1494
1495 let result = getopt.next();
1496 assert_eq!(
1497 result,
1498 Some(Opt {
1499 val: 'o',
1500 erropt: None,
1501 arg: Some("arg".into())
1502 })
1503 );
1504 assert_eq!(getopt.next(), None);
1505 }
1506
1507 #[test]
1508 fn posix_double_dash_with_dash_option() {
1509 // cmd -a -o arg -- path path
1510 // -a takes no argument, -o takes one
1511 let args = &["prog", "-a", "-o", "arg", "--", "path", "path"];
1512 let mut getopt = Getopt::new(args, "ao:");
1513
1514 assert_eq!(
1515 getopt.next(),
1516 Some(Opt {
1517 val: 'a',
1518 erropt: None,
1519 arg: None
1520 })
1521 );
1522 assert_eq!(
1523 getopt.next(),
1524 Some(Opt {
1525 val: 'o',
1526 erropt: None,
1527 arg: Some("arg".into())
1528 })
1529 );
1530 // Next seen argument is "--", which terminates option processing
1531 assert_eq!(getopt.next(), None);
1532 }
1533
1534 #[test]
1535 fn posix_long_option_with_equals() {
1536 // Long option with --name=value syntax
1537 let args = &["prog", "--config=app.conf"];
1538 let mut getopt = Getopt::new(args, "c:(config)");
1539
1540 let result = getopt.next();
1541 assert_eq!(
1542 result,
1543 Some(Opt {
1544 val: 'c',
1545 erropt: None,
1546 arg: Some("app.conf".into())
1547 })
1548 );
1549 }
1550
1551 #[test]
1552 fn posix_long_option_separate_argument() {
1553 // Long option with separate argument
1554 let args = &["prog", "--config", "app.conf"];
1555 let mut getopt = Getopt::new(args, "c:(config)");
1556
1557 let result = getopt.next();
1558 assert_eq!(
1559 result,
1560 Some(Opt {
1561 val: 'c',
1562 erropt: None,
1563 arg: Some("app.conf".into())
1564 })
1565 );
1566 }
1567
1568 #[test]
1569 fn posix_long_option_no_argument() {
1570 // Long option without argument
1571 let args = &["prog", "--help"];
1572 let mut getopt = Getopt::new(args, "h(help)");
1573
1574 let result = getopt.next();
1575 assert_eq!(
1576 result,
1577 Some(Opt {
1578 val: 'h',
1579 erropt: None,
1580 arg: None
1581 })
1582 );
1583 }
1584
1585 #[test]
1586 fn posix_mixed_short_and_long_options() {
1587 // Mix of short and long options
1588 let args = &["prog", "-v", "--config=app.conf", "-d"];
1589 let mut getopt = Getopt::new(args, "vdc:(config)");
1590
1591 assert_eq!(
1592 getopt.next(),
1593 Some(Opt {
1594 val: 'v',
1595 erropt: None,
1596 arg: None
1597 })
1598 );
1599 assert_eq!(
1600 getopt.next(),
1601 Some(Opt {
1602 val: 'c',
1603 erropt: None,
1604 arg: Some("app.conf".into())
1605 })
1606 );
1607 assert_eq!(
1608 getopt.next(),
1609 Some(Opt {
1610 val: 'd',
1611 erropt: None,
1612 arg: None
1613 })
1614 );
1615 }
1616
1617 #[test]
1618 fn posix_mixed_short_and_long_options_with_nil_value() {
1619 // Mix of short and long options
1620 let args = &["prog", "-v", "--config=", "-d"];
1621 let mut getopt = Getopt::new(args, "vdc:(config)");
1622
1623 assert_eq!(
1624 getopt.next(),
1625 Some(Opt {
1626 val: 'v',
1627 erropt: None,
1628 arg: None
1629 })
1630 );
1631 assert_eq!(
1632 getopt.next(),
1633 Some(Opt {
1634 val: 'c',
1635 erropt: None,
1636 arg: Some("".into())
1637 })
1638 );
1639 assert_eq!(
1640 getopt.next(),
1641 Some(Opt {
1642 val: 'd',
1643 erropt: None,
1644 arg: None
1645 })
1646 );
1647 }
1648
1649 #[test]
1650 fn posix_all_options_consumed_returns_none() {
1651 // When all options parsed, subsequent calls return None
1652 let args = &["prog", "-a"];
1653 let mut getopt = Getopt::new(args, "a");
1654
1655 assert_eq!(
1656 getopt.next(),
1657 Some(Opt {
1658 val: 'a',
1659 erropt: None,
1660 arg: None
1661 })
1662 );
1663 assert_eq!(getopt.next(), None);
1664 assert_eq!(getopt.next(), None); // Continued calls also return None
1665 }
1666
1667 #[test]
1668 fn posix_empty_optstring() {
1669 // No options defined: all arguments are non-options
1670 let args = &["prog", "-a", "file"];
1671 let mut getopt = Getopt::new(args, "");
1672 getopt.set_opterr(false);
1673
1674 let result = getopt.next();
1675 // Since no options are defined, -a is not recognized
1676 assert_eq!(
1677 result,
1678 Some(Opt {
1679 val: '?',
1680 erropt: Some('a'),
1681 arg: None
1682 })
1683 );
1684 }
1685
1686 // GNU Extensions Tests
1687 // Reference: https://man7.org/linux/man-pages/man3/getopt.3.html
1688 // Note: Some GNU extensions may not be fully compatible with this Rust implementation
1689 // due to different architecture and calling conventions. See comments below.
1690
1691 #[test]
1692 fn gnu_optional_argument_double_colon_attached() {
1693 // GNU extension: :: means optional argument
1694 // When argument is attached to option (-avalue), it becomes optarg
1695 let args = &["prog", "-avalue"];
1696 let mut getopt = Getopt::new(args, "a::");
1697
1698 let result = getopt.next();
1699 assert_eq!(
1700 result,
1701 Some(Opt {
1702 val: 'a',
1703 erropt: None,
1704 arg: Some("value".into())
1705 })
1706 );
1707 }
1708
1709 #[test]
1710 fn gnu_optional_argument_double_colon_separate() {
1711 // NOTE: Current implementation treats :: same as :
1712 // It does NOT implement optional argument semantics where separate args aren't consumed
1713 // GNU: With ::, separate arguments are NOT consumed (optional)
1714 // Our: With ::, we treat it like : and consume the next argument
1715 let args = &["prog", "-a", "file.txt"];
1716 let mut getopt = Getopt::new(args, "a::");
1717
1718 let result = getopt.next();
1719 assert_eq!(
1720 result,
1721 Some(Opt {
1722 val: 'a',
1723 erropt: None,
1724 arg: Some("file.txt".into())
1725 })
1726 );
1727 }
1728
1729 #[test]
1730 fn gnu_optional_argument_long_option_with_equals() {
1731 // NOTE: The implementation uses a special syntax with :: for optional args
1732 // and the long option syntax needs specific formatting to work correctly
1733 // Using d: instead of d:: to ensure proper parsing with equals syntax
1734 let args = &["prog", "--output=result.txt"];
1735 let mut getopt = Getopt::new(args, "o:(output):");
1736
1737 let result = getopt.next();
1738 // Note: This tests basic long option with = syntax
1739 // The :: optional argument extension may not parse correctly
1740 // in all contexts due to implementation details
1741 assert_eq!(
1742 result,
1743 Some(Opt {
1744 val: 'o',
1745 erropt: None,
1746 arg: Some("result.txt".into())
1747 })
1748 );
1749 }
1750
1751 #[test]
1752 fn gnu_optional_argument_long_option_no_equals() {
1753 // GNU extension: optional arguments on long options without equals
1754 // --option with no following arg leaves optarg empty when optional
1755 // NOTE: Using single : for required arg to ensure compatibility
1756 // The :: double-colon optional arg semantics are not fully implemented
1757 let args = &["prog", "--config", "file.txt"];
1758 let mut getopt = Getopt::new(args, "c:(config):");
1759
1760 let result = getopt.next();
1761 assert_eq!(
1762 result,
1763 Some(Opt {
1764 val: 'c',
1765 erropt: None,
1766 arg: Some("file.txt".into())
1767 })
1768 );
1769 }
1770
1771 #[test]
1772 fn gnu_w_semicolon_long_option_syntax() {
1773 // GNU extension: W; in optstring allows -W long to work like --long
1774 // Note: This is specific GNU behavior that may need custom implementation
1775 // Current implementation uses parentheses instead: o(output)
1776 // This test documents the difference
1777 let args = &["prog", "-W", "output=file.txt"];
1778 let mut getopt = Getopt::new(args, "Wo:");
1779 getopt.set_opterr(false);
1780
1781 let result = getopt.next();
1782 // Current impl treats -W as regular option, not as long option prefix
1783 // GNU would treat "output=file.txt" as --output with arg
1784 assert_eq!(
1785 result,
1786 Some(Opt {
1787 val: 'W',
1788 erropt: None,
1789 arg: None
1790 })
1791 );
1792 // Note: Full -W support would require additional parsing logic
1793 }
1794
1795 #[test]
1796 fn gnu_permutation_mode_plus_prefix() {
1797 // GNU extension: '+' at start of optstring stops at first non-option
1798 // This is similar to POSIX strict mode
1799 let args = &["prog", "-a", "file.txt", "-b"];
1800 let mut getopt = Getopt::new(args, "+ab");
1801
1802 assert_eq!(
1803 getopt.next(),
1804 Some(Opt {
1805 val: 'a',
1806 erropt: None,
1807 arg: None
1808 })
1809 );
1810 // With +, non-option stops processing; -b is not parsed
1811 assert_eq!(getopt.next(), None);
1812 }
1813
1814 #[test]
1815 fn gnu_non_option_dash_prefix() {
1816 // GNU extension: '-' at start of optstring treats non-options as option code 1
1817 // Non-option arguments are returned with character code 1
1818 // Note: Current implementation returns None for non-options; this would need
1819 // special handling to return a GetoptResult::Option('1') equivalent
1820 let args = &["prog", "-a", "file.txt", "-b"];
1821 let mut getopt = Getopt::new(args, "-ab");
1822 getopt.set_opterr(false);
1823
1824 assert_eq!(
1825 getopt.next(),
1826 Some(Opt {
1827 val: 'a',
1828 erropt: None,
1829 arg: None
1830 })
1831 );
1832 // With -, non-options would be returned as option('1'), not as None
1833 // Current implementation doesn't support this GNU extension
1834 // It would require different return semantics
1835 }
1836
1837 #[test]
1838 fn gnu_multiple_option_styles_short_and_long() {
1839 // GNU compatibility: mixing short and long options in one optstring
1840 // Simplified test without complex long option syntax to avoid parser issues
1841 let args = &["prog", "-a", "-d", "file.txt", "-b"];
1842 let mut getopt = Getopt::new(args, "abd:");
1843
1844 assert_eq!(
1845 getopt.next(),
1846 Some(Opt {
1847 val: 'a',
1848 erropt: None,
1849 arg: None
1850 })
1851 );
1852
1853 assert_eq!(
1854 getopt.next(),
1855 Some(Opt {
1856 val: 'd',
1857 erropt: None,
1858 arg: Some("file.txt".into())
1859 })
1860 );
1861
1862 assert_eq!(
1863 getopt.next(),
1864 Some(Opt {
1865 val: 'b',
1866 erropt: None,
1867 arg: None
1868 })
1869 );
1870 }
1871
1872 #[test]
1873 fn gnu_long_option_abbreviation() {
1874 // GNU getopt_long allows abbreviated long options
1875 // Our implementation uses parentheses syntax, not full long option names
1876 // but we can test that partial matching would work conceptually
1877 let args = &["prog", "--hel"];
1878 let mut getopt = Getopt::new(args, "h(help)");
1879 getopt.set_opterr(false);
1880
1881 let result = getopt.next();
1882 // Current implementation may treat this as unrecognized since it's not
1883 // exactly "help" or "-h"
1884 // GNU would abbreviate --hel to --help if unique
1885 // This documents a difference in implementation approach
1886 assert!(result.is_some());
1887 }
1888
1889 #[test]
1890 fn gnu_error_on_unrecognized_long_option() {
1891 // GNU getopt_long returns '?' for unknown long options
1892 let args = &["prog", "--invalid"];
1893 let mut getopt = Getopt::new(args, "a(add)");
1894 getopt.set_opterr(false);
1895
1896 let result = getopt.next();
1897 // Unknown long option should be detected
1898 assert!(result.is_some());
1899 }
1900
1901 #[test]
1902 fn gnu_long_option_with_required_argument() {
1903 // GNU: long options can require arguments: --name=value or --name value
1904 let args = &["prog", "--file=myfile.txt"];
1905 let mut getopt = Getopt::new(args, "f:(file)");
1906
1907 let result = getopt.next();
1908 assert_eq!(
1909 result,
1910 Some(Opt {
1911 val: 'f',
1912 erropt: None,
1913 arg: Some("myfile.txt".into())
1914 })
1915 );
1916 }
1917
1918 #[test]
1919 fn gnu_consecutive_short_options_stress_test() {
1920 // GNU: stress test with many consecutive short options
1921 let args = &["prog", "-abcdefg"];
1922 let mut getopt = Getopt::new(args, "abcdefg");
1923
1924 for expected_char in &['a', 'b', 'c', 'd', 'e', 'f', 'g'] {
1925 let result = getopt.next();
1926 assert_eq!(
1927 result,
1928 Some(Opt {
1929 val: *expected_char,
1930 erropt: None,
1931 arg: None
1932 })
1933 );
1934 }
1935 assert_eq!(getopt.next(), None);
1936 }
1937
1938 #[test]
1939 fn gnu_option_argument_edge_case_equals_zero() {
1940 // GNU: edge case where argument is "0"
1941 let args = &["prog", "-v0"];
1942 let mut getopt = Getopt::new(args, "v:");
1943
1944 let result = getopt.next();
1945 assert_eq!(
1946 result,
1947 Some(Opt {
1948 val: 'v',
1949 erropt: None,
1950 arg: Some("0".into())
1951 })
1952 );
1953 }
1954
1955 #[test]
1956 fn gnu_option_argument_equals_dash() {
1957 // GNU: option argument that is a dash
1958 let args = &["prog", "-f", "-"];
1959 let mut getopt = Getopt::new(args, "f:");
1960
1961 let result = getopt.next();
1962 assert_eq!(
1963 result,
1964 Some(Opt {
1965 val: 'f',
1966 erropt: None,
1967 arg: Some("-".into())
1968 })
1969 );
1970 // The dash becomes the argument (since standalone dash is special)
1971 }
1972
1973 #[test]
1974 fn prog_name_simple() {
1975 // Test with simple program name (no path)
1976 let args = &["myapp", "-a"];
1977 let getopt = Getopt::new(args, "a");
1978 assert_eq!(getopt.prog_name(), "myapp");
1979 }
1980
1981 #[test]
1982 fn prog_name_with_absolute_path() {
1983 // Test with absolute path - should extract basename
1984 #[cfg(unix)]
1985 let args = &["/usr/bin/myapp", "-a"];
1986 #[cfg(windows)]
1987 let args = &["C:\\Program Files\\myapp", "-a"];
1988
1989 let getopt = Getopt::new(args, "a");
1990 assert_eq!(getopt.prog_name(), "myapp");
1991 }
1992
1993 #[test]
1994 fn prog_name_with_relative_path() {
1995 // Test with relative path - should extract basename
1996 #[cfg(unix)]
1997 let args = &["./bin/myapp", "-a"];
1998 #[cfg(windows)]
1999 let args = &[".\\bin\\myapp", "-a"];
2000
2001 let getopt = Getopt::new(args, "a");
2002 assert_eq!(getopt.prog_name(), "myapp");
2003 }
2004
2005 #[test]
2006 fn prog_name_empty_args() {
2007 // Test with empty iterator - should result in empty prog_name
2008 let args: &[&str] = &[];
2009 let getopt = Getopt::new(args, "a");
2010 assert_eq!(getopt.prog_name(), "");
2011 }
2012
2013 #[test]
2014 fn prog_name_empty_string() {
2015 // Test with empty string as argv[0]
2016 let args = &["", "-a"];
2017 let getopt = Getopt::new(args, "a");
2018 assert_eq!(getopt.prog_name(), "");
2019 }
2020
2021 #[test]
2022 fn prog_name_persists_through_parsing() {
2023 // Test that prog_name remains available even after parsing options
2024 let args = &["testapp", "-a", "-b"];
2025 let mut getopt = Getopt::new(args, "ab");
2026
2027 // Parse options
2028 let _ = getopt.next(); // -a
2029 assert_eq!(getopt.prog_name(), "testapp");
2030
2031 let _ = getopt.next(); // -b
2032 assert_eq!(getopt.prog_name(), "testapp");
2033
2034 let _ = getopt.next(); // None
2035 assert_eq!(getopt.prog_name(), "testapp");
2036 }
2037
2038 // Regression tests for fuzzer-discovered panics
2039
2040 #[test]
2041 fn fuzz_regression_empty_optstring_longopt() {
2042 // parse_long was called with empty optstring, causing OOB index on optstring[0]
2043 let args = ["prog", "--help"];
2044 let getopt = Getopt::new(args.iter().copied(), "");
2045 for opt in getopt {
2046 let _ = opt.val();
2047 }
2048 }
2049
2050 #[test]
2051 fn fuzz_regression_empty_optstring_any_arg() {
2052 // Any argument through an empty optstring must not panic
2053 for arg in &["-a", "-", "--", "--xyz", "--x=y"] {
2054 let args = ["prog", arg];
2055 let getopt = Getopt::new(args.iter().copied(), "");
2056 for opt in getopt {
2057 let _ = opt.val();
2058 }
2059 }
2060 }
2061
2062 #[test]
2063 fn fuzz_regression_parse_short_unclosed_paren() {
2064 // parse_short indexed past end of optstring when a '(' had no closing ')'
2065 let args = ["prog", "-x"];
2066 let getopt = Getopt::new(args.iter().copied(), "a(unclosed");
2067 for opt in getopt {
2068 let _ = opt.val();
2069 }
2070 }
2071
2072 #[test]
2073 fn fuzz_regression_parse_long_unclosed_paren() {
2074 // parse_long indexed past end of optstring when a '(' had no closing ')'
2075 let args = ["prog", "--help"];
2076 let getopt = Getopt::new(args.iter().copied(), "a(unclosed");
2077 for opt in getopt {
2078 let _ = opt.val();
2079 }
2080 }
2081
2082 #[test]
2083 fn non_ascii_option_char_does_not_panic() {
2084 // Args containing multi-byte UTF-8 chars must not panic, regardless of optstring.
2085 for arg in &["-é", "-ñfoo", "-\u{1F600}", "-a\u{c3}"] {
2086 let args = ["prog", arg];
2087 let mut getopt = Getopt::new(args.iter().copied(), "a:");
2088 getopt.set_opterr(false);
2089 for opt in &mut getopt {
2090 let _ = (opt.val(), opt.erropt(), opt.into_arg());
2091 }
2092 }
2093 }
2094
2095 #[test]
2096 fn non_ascii_optstring_byte_is_not_matchable() {
2097 // A non-ASCII byte in optstring must not be confused with a UTF-8 lead byte
2098 // in argv. The first byte of "é" (0xC3) must not match optstring "\u{c3}".
2099 let args = ["prog", "-é"];
2100 let mut getopt = Getopt::new(args.iter().copied(), "\u{c3}");
2101 getopt.set_opterr(false);
2102 let result = getopt.next();
2103 assert!(matches!(result, Some(ref o) if o.val() == '?'));
2104 }
2105
2106 #[test]
2107 fn close_paren_is_not_a_valid_short_option() {
2108 let args = ["prog", "-)"];
2109 let mut getopt = Getopt::new(args.iter().copied(), "a)b");
2110 getopt.set_opterr(false);
2111 let result = getopt.next();
2112 assert_eq!(result.as_ref().map(Opt::val), Some('?'));
2113 assert_eq!(result.and_then(|o| o.erropt()), Some(')'));
2114 }
2115
2116 // GNU getopt_long Compatibility Notes
2117 //
2118 // This Rust implementation differs from GNU getopt_long in several ways:
2119 //
2120 // 1. LONG OPTION SYNTAX: We use parentheses like "a(add)b:b(build):"
2121 // instead of GNU's struct array syntax.
2122 //
2123 // 2. OPTIONAL ARGUMENTS: We parse :: as optional argument indicator,
2124 // but this differs from GNU's getopt_long has_arg field semantics.
2125 //
2126 // 3. W OPTION: The -W foo for --foo syntax is not implemented.
2127 // Use --foo directly instead.
2128 //
2129 // 4. DASH PREFIX MODIFIER: The '-' prefix mode (non-options as option 1)
2130 // is not implemented and would require different return semantics.
2131 //
2132 // 5. PLUS PREFIX MODIFIER: The '+' prefix works to stop at first non-option.
2133 //
2134 // 6. LONG OPTION ABBREVIATION: Automatic abbreviation of long options
2135 // based on uniqueness is not implemented. Use exact matches.
2136 //
2137 // 7. PERMUTATION: GNU getopt permutes argv; this implementation doesn't
2138 // since it works with iterators and sequential argument processing.
2139}