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;