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;