Skip to main content

bsd_getopt/
lib.rs

1//! # bsd-getopt
2//!
3//! A small, dependency-free implementation of a BSD-style `getopt` parser in Rust.
4//!
5//! This crate provides a minimal and predictable way to parse short command-line
6//! options (e.g. `-a`, `-b value`, `-abc`) similar to traditional Unix `getopt`.
7//!
8//! ## Features
9//!
10//! - Supports grouped options (`-abc`)
11//! - Supports options with required arguments (`-o value` or `-ovalue`)
12//! - Handles `--` to terminate option parsing
13//! - Exposes `optind`, `optarg`, and `optopt` like classic `getopt`
14//! - No unsafe code, no dependencies
15//!
16//! ## Example
17//!
18//! ```rust
19//! use bsd_getopt::Getopt;
20//!
21//! let args = vec![
22//!     "prog".to_string(),
23//!     "-a".to_string(),
24//!     "-b".to_string(),
25//!     "value".to_string(),
26//!     "-cfoo".to_string(),
27//! ];
28//!
29//! let mut parser = Getopt::new("ab:c:", args);
30//!
31//! while let Some(opt) = parser.next() {
32//!     match opt {
33//!         'a' => println!("option a"),
34//!         'b' => println!("option b with arg {:?}", parser.optarg),
35//!         'c' => println!("option c with arg {:?}", parser.optarg),
36//!         '?' => println!("unknown option: {}", parser.optopt),
37//!         ':' => println!("missing argument for: {}", parser.optopt),
38//!         _ => {}
39//!     }
40//! }
41//! ```
42//!
43//! ## `optstring` format
44//!
45//! - `a` → option without argument
46//! - `b:` → option requires argument
47//! - `:abc` → suppress error messages, return `:` on missing argument
48//!
49//! ## Notes
50//!
51//! - Parsing stops at the first non-option argument or `--`
52//! - This crate only supports short options (no long options like `--help`)
53
54/// A BSD-style command-line option parser.
55///
56/// `Getopt` iterates over command-line arguments and returns options one by one,
57/// mimicking the behavior of traditional Unix `getopt`.
58///
59/// The parser maintains internal state such as:
60///
61/// - `optind`: index of the next argument to process
62/// - `optarg`: argument associated with the current option (if any)
63/// - `optopt`: the last option character processed
64/// - `opterr`: whether to print error messages
65///
66/// # Example
67///
68/// ```rust
69/// use bsd_getopt::Getopt;
70///
71/// let args = vec![
72///     "prog".to_string(),
73///     "-a".to_string(),
74///     "-bvalue".to_string(),
75/// ];
76///
77/// let mut g = Getopt::new("ab:", args);
78///
79/// assert_eq!(g.next(), Some('a'));
80/// assert_eq!(g.next(), Some('b'));
81/// assert_eq!(g.optarg, Some("value".to_string()));
82/// assert_eq!(g.next(), None);
83/// ```
84#[derive(Debug, Clone)]
85pub struct Getopt {
86    args: Vec<String>,
87    optstring: String,
88    pub optind: usize,
89    pub optopt: char,
90    pub optarg: Option<String>,
91    pub opterr: bool,
92    nextchar_idx: usize, 
93}
94
95impl Getopt {
96    /// Creates a new `Getopt` parser.
97    ///
98    /// # Parameters
99    ///
100    /// - `optstring`: A string describing valid options.
101    /// - `args`: Command-line arguments (typically from `std::env::args()`).
102    ///
103    /// # Behavior
104    ///
105    /// - Parsing starts from index 1 (skipping program name)
106    /// - Internal state is initialized to default values
107    ///
108    /// # Example
109    ///
110    /// ```rust
111    /// use bsd_getopt::Getopt;
112    ///
113    /// let args = vec!["prog".to_string(), "-a".to_string()];
114    /// let parser = Getopt::new("a", args);
115    /// ```
116    pub fn new(optstring: &str, args: Vec<String>) -> Self {
117        Self {
118            args,
119            optstring: optstring.to_string(),
120            optind: 1,
121            optopt: '?',
122            optarg: None,
123            opterr: true,
124            nextchar_idx: 0,
125        }
126    }
127
128    /// Returns the next option character, or `None` if parsing is complete.
129    ///
130    /// This function mimics the behavior of the standard `getopt`:
131    ///
132    /// - Returns `Some(char)` for a valid option
133    /// - Returns `Some('?')` for an unknown option
134    /// - Returns `Some(':')` if an argument is missing and `optstring` starts with `:`
135    /// - Returns `None` when no more options are available
136    ///
137    /// # Side Effects
138    ///
139    /// - Updates `optind`, `optarg`, and `optopt`
140    /// - May print errors to stderr if `opterr` is `true`
141    ///
142    /// # Rules
143    ///
144    /// - Options can be grouped: `-abc`
145    /// - Options with arguments:
146    ///   - `-o value`
147    ///   - `-ovalue`
148    /// - `--` stops option parsing
149    ///
150    /// # Example
151    ///
152    /// ```rust
153    /// use bsd_getopt::Getopt;
154    ///
155    /// let args = vec![
156    ///     "prog".to_string(),
157    ///     "-a".to_string(),
158    ///     "-b".to_string(),
159    ///     "foo".to_string(),
160    /// ];
161    ///
162    /// let mut g = Getopt::new("ab:", args);
163    ///
164    /// assert_eq!(g.next(), Some('a'));
165    /// assert_eq!(g.next(), Some('b'));
166    /// assert_eq!(g.optarg, Some("foo".to_string()));
167    /// assert_eq!(g.next(), None);
168    /// ```
169    pub fn next(&mut self) -> Option<char> {
170        self.optarg = None;
171
172        if self.optind >= self.args.len() {
173            return None;
174        }
175
176        let arg = &self.args[self.optind];
177
178        if self.nextchar_idx == 0 {
179            if !arg.starts_with('-') || arg == "-" {
180                return None;
181            }
182            if arg == "--" {
183                self.optind += 1;
184                return None;
185            }
186            self.nextchar_idx = 1; 
187        }
188
189        let c = arg.chars().nth(self.nextchar_idx).unwrap();
190        self.optopt = c;
191        self.nextchar_idx += 1;
192
193        match self.optstring.find(c) {
194            None => {
195                if self.nextchar_idx >= arg.len() {
196                    self.optind += 1;
197                    self.nextchar_idx = 0;
198                }
199                if self.opterr && !self.optstring.starts_with(':') {
200                    eprintln!("{}: illegal option -- {}", self.args[0], c);
201                }
202                Some('?')
203            }
204            Some(idx) => {
205                if self.optstring.as_bytes().get(idx + 1) == Some(&b':') {
206                    if self.nextchar_idx < arg.len() {
207                        self.optarg = Some(arg[self.nextchar_idx..].to_string());
208                        self.optind += 1;
209                        self.nextchar_idx = 0;
210                    } else {
211                        self.optind += 1;
212                        if self.optind < self.args.len() {
213                            self.optarg = Some(self.args[self.optind].clone());
214                            self.optind += 1;
215                        } else {
216                            // 缺少参数
217                            self.nextchar_idx = 0;
218                            if self.optstring.starts_with(':') {
219                                return Some(':');
220                            } else {
221                                if self.opterr {
222                                    eprintln!("{}: option requires an argument -- {}", self.args[0], c);
223                                }
224                                return Some('?');
225                            }
226                        }
227                    }
228                    self.nextchar_idx = 0;
229                } else {
230                    if self.nextchar_idx >= arg.len() {
231                        self.optind += 1;
232                        self.nextchar_idx = 0;
233                    }
234                }
235                Some(c)
236            }
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests;