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/// ```
84pub struct Getopt {
85    args: Vec<String>,
86    optstring: String,
87    pub optind: usize,
88    pub optopt: char,
89    pub optarg: Option<String>,
90    pub opterr: bool,
91    nextchar_idx: usize, 
92}
93
94impl Getopt {
95    /// Creates a new `Getopt` parser.
96    ///
97    /// # Parameters
98    ///
99    /// - `optstring`: A string describing valid options.
100    /// - `args`: Command-line arguments (typically from `std::env::args()`).
101    ///
102    /// # Behavior
103    ///
104    /// - Parsing starts from index 1 (skipping program name)
105    /// - Internal state is initialized to default values
106    ///
107    /// # Example
108    ///
109    /// ```rust
110    /// use bsd_getopt::Getopt;
111    ///
112    /// let args = vec!["prog".to_string(), "-a".to_string()];
113    /// let parser = Getopt::new("a", args);
114    /// ```
115    pub fn new(optstring: &str, args: Vec<String>) -> Self {
116        Self {
117            args,
118            optstring: optstring.to_string(),
119            optind: 1,
120            optopt: '?',
121            optarg: None,
122            opterr: true,
123            nextchar_idx: 0,
124        }
125    }
126
127    /// Returns the next option character, or `None` if parsing is complete.
128    ///
129    /// This function mimics the behavior of the standard `getopt`:
130    ///
131    /// - Returns `Some(char)` for a valid option
132    /// - Returns `Some('?')` for an unknown option
133    /// - Returns `Some(':')` if an argument is missing and `optstring` starts with `:`
134    /// - Returns `None` when no more options are available
135    ///
136    /// # Side Effects
137    ///
138    /// - Updates `optind`, `optarg`, and `optopt`
139    /// - May print errors to stderr if `opterr` is `true`
140    ///
141    /// # Rules
142    ///
143    /// - Options can be grouped: `-abc`
144    /// - Options with arguments:
145    ///   - `-o value`
146    ///   - `-ovalue`
147    /// - `--` stops option parsing
148    ///
149    /// # Example
150    ///
151    /// ```rust
152    /// use bsd_getopt::Getopt;
153    ///
154    /// let args = vec![
155    ///     "prog".to_string(),
156    ///     "-a".to_string(),
157    ///     "-b".to_string(),
158    ///     "foo".to_string(),
159    /// ];
160    ///
161    /// let mut g = Getopt::new("ab:", args);
162    ///
163    /// assert_eq!(g.next(), Some('a'));
164    /// assert_eq!(g.next(), Some('b'));
165    /// assert_eq!(g.optarg, Some("foo".to_string()));
166    /// assert_eq!(g.next(), None);
167    /// ```
168    pub fn next(&mut self) -> Option<char> {
169        self.optarg = None;
170
171        if self.optind >= self.args.len() {
172            return None;
173        }
174
175        let arg = &self.args[self.optind];
176
177        if self.nextchar_idx == 0 {
178            if !arg.starts_with('-') || arg == "-" {
179                return None;
180            }
181            if arg == "--" {
182                self.optind += 1;
183                return None;
184            }
185            self.nextchar_idx = 1; 
186        }
187
188        let c = arg.chars().nth(self.nextchar_idx).unwrap();
189        self.optopt = c;
190        self.nextchar_idx += 1;
191
192        match self.optstring.find(c) {
193            None => {
194                if self.nextchar_idx >= arg.len() {
195                    self.optind += 1;
196                    self.nextchar_idx = 0;
197                }
198                if self.opterr && !self.optstring.starts_with(':') {
199                    eprintln!("{}: illegal option -- {}", self.args[0], c);
200                }
201                Some('?')
202            }
203            Some(idx) => {
204                if self.optstring.as_bytes().get(idx + 1) == Some(&b':') {
205                    if self.nextchar_idx < arg.len() {
206                        self.optarg = Some(arg[self.nextchar_idx..].to_string());
207                        self.optind += 1;
208                        self.nextchar_idx = 0;
209                    } else {
210                        self.optind += 1;
211                        if self.optind < self.args.len() {
212                            self.optarg = Some(self.args[self.optind].clone());
213                            self.optind += 1;
214                        } else {
215                            // 缺少参数
216                            self.nextchar_idx = 0;
217                            if self.optstring.starts_with(':') {
218                                return Some(':');
219                            } else {
220                                if self.opterr {
221                                    eprintln!("{}: option requires an argument -- {}", self.args[0], c);
222                                }
223                                return Some('?');
224                            }
225                        }
226                    }
227                    self.nextchar_idx = 0;
228                } else {
229                    if self.nextchar_idx >= arg.len() {
230                        self.optind += 1;
231                        self.nextchar_idx = 0;
232                    }
233                }
234                Some(c)
235            }
236        }
237    }
238}
239
240#[cfg(test)]
241mod tests;