Skip to main content

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