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(¤t_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 ¤t_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}