1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::process::ExitCode;
7
8#[derive(Parser)]
9#[command(
10 name = "getopt",
11 about = "Parse command options (enhanced)",
12 override_usage = "getopt optstring parameters\n \
13 getopt [options] [--] optstring parameters\n \
14 getopt [options] -o|--options optstring [--] parameters"
15)]
16pub struct Args {
17 #[arg(short = 'a', long = "alternative")]
19 alternative: bool,
20
21 #[arg(short = 'l', long = "longoptions", value_delimiter = ',')]
23 longoptions: Vec<String>,
24
25 #[arg(short = 'n', long = "name")]
27 name: Option<String>,
28
29 #[arg(short = 'o', long = "options")]
31 options: Option<String>,
32
33 #[arg(short = 'q', long = "quiet")]
35 quiet: bool,
36
37 #[arg(short = 'Q', long = "quiet-output")]
39 quiet_output: bool,
40
41 #[arg(short = 's', long = "shell", default_value = "bash")]
43 shell: String,
44
45 #[arg(short = 'T', long = "test")]
47 test: bool,
48
49 #[arg(short = 'u', long = "unquoted")]
51 unquoted: bool,
52
53 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
55 params: Vec<String>,
56}
57
58#[derive(Clone, Copy)]
59enum ArgReq {
60 None,
61 Required,
62 Optional,
63}
64
65struct ShortOpt {
66 ch: char,
67 arg_req: ArgReq,
68}
69
70struct LongOpt {
71 name: String,
72 arg_req: ArgReq,
73}
74
75#[derive(Clone, Copy)]
76enum ScanMode {
77 Permute,
78 StopAtFirst,
79 InPlace,
80}
81
82#[derive(Clone, Copy)]
83enum Shell {
84 Sh,
85 Csh,
86}
87
88fn parse_optstring(s: &str) -> (Vec<ShortOpt>, ScanMode) {
89 let mut opts = Vec::new();
90 let mut mode = ScanMode::Permute;
91 let mut chars = s.chars().peekable();
92
93 if chars.peek() == Some(&'+') {
94 mode = ScanMode::StopAtFirst;
95 chars.next();
96 } else if chars.peek() == Some(&'-') {
97 mode = ScanMode::InPlace;
98 chars.next();
99 }
100
101 if chars.peek() == Some(&':') {
103 chars.next();
104 }
105
106 while let Some(ch) = chars.next() {
107 let arg_req = if chars.peek() == Some(&':') {
108 chars.next();
109 if chars.peek() == Some(&':') {
110 chars.next();
111 ArgReq::Optional
112 } else {
113 ArgReq::Required
114 }
115 } else {
116 ArgReq::None
117 };
118 opts.push(ShortOpt { ch, arg_req });
119 }
120
121 (opts, mode)
122}
123
124fn parse_longopt_spec(spec: &str) -> LongOpt {
125 if let Some(name) = spec.strip_suffix("::") {
126 LongOpt {
127 name: name.to_string(),
128 arg_req: ArgReq::Optional,
129 }
130 } else if let Some(name) = spec.strip_suffix(':') {
131 LongOpt {
132 name: name.to_string(),
133 arg_req: ArgReq::Required,
134 }
135 } else {
136 LongOpt {
137 name: spec.to_string(),
138 arg_req: ArgReq::None,
139 }
140 }
141}
142
143fn quote(s: &str, shell: Shell, unquoted: bool) -> String {
144 if unquoted {
145 return s.to_string();
146 }
147 match shell {
148 Shell::Sh => {
149 if s.is_empty() {
150 return "''".to_string();
151 }
152 format!("'{}'", s.replace('\'', "'\\''"))
153 }
154 Shell::Csh => {
155 if s.is_empty() {
156 return "''".to_string();
157 }
158 let mut out = String::from("'");
159 for ch in s.chars() {
160 match ch {
161 '\'' => out.push_str("'\\''"),
162 '!' => out.push_str("\\!"),
163 '\n' => out.push_str("'\\\n'"),
164 _ => out.push(ch),
165 }
166 }
167 out.push('\'');
168 out
169 }
170 }
171}
172
173struct Ctx<'a> {
174 long_opts: &'a [LongOpt],
175 prog_name: &'a str,
176 quiet: bool,
177 shell: Shell,
178 unquoted: bool,
179}
180
181impl Ctx<'_> {
182 fn quote(&self, s: &str) -> String {
183 quote(s, self.shell, self.unquoted)
184 }
185}
186
187fn handle_long_option(
188 name: &str,
189 value: Option<&str>,
190 ctx: &Ctx<'_>,
191 args: &[String],
192 i: &mut usize,
193 output: &mut Vec<String>,
194) -> bool {
195 let matches: Vec<_> = ctx
196 .long_opts
197 .iter()
198 .filter(|o| o.name.starts_with(name))
199 .collect();
200
201 match matches.len() {
202 0 => {
203 if !ctx.quiet {
204 eprintln!("{}: unrecognized option '--{name}'", ctx.prog_name);
205 }
206 true
207 }
208 1 => {
209 let opt = matches[0];
210 output.push(format!("--{}", opt.name));
211 match opt.arg_req {
212 ArgReq::Required => {
213 if let Some(v) = value {
214 output.push(ctx.quote(v));
215 } else {
216 *i += 1;
217 if *i < args.len() {
218 output.push(ctx.quote(&args[*i]));
219 } else {
220 if !ctx.quiet {
221 eprintln!(
222 "{}: option '--{}' requires an argument",
223 ctx.prog_name, opt.name
224 );
225 }
226 return true;
227 }
228 }
229 }
230 ArgReq::Optional => {
231 output.push(ctx.quote(value.unwrap_or("")));
232 }
233 ArgReq::None => {}
234 }
235 false
236 }
237 _ => {
238 if !ctx.quiet {
239 let names: Vec<_> =
240 matches.iter().map(|o| format!("'--{}'", o.name)).collect();
241 eprintln!(
242 "{}: option '--{name}' is ambiguous; possibilities: {}",
243 ctx.prog_name,
244 names.join(" ")
245 );
246 }
247 true
248 }
249 }
250}
251
252fn parse_user_args(
253 short_opts: &[ShortOpt],
254 ctx: &Ctx<'_>,
255 scan_mode: ScanMode,
256 alternative: bool,
257 args: &[String],
258) -> (Vec<String>, bool) {
259 let mut output: Vec<String> = Vec::new();
260 let mut non_opts: Vec<String> = Vec::new();
261 let mut errors = false;
262 let mut i = 0;
263
264 while i < args.len() {
265 let arg = &args[i];
266
267 if arg == "--" {
268 non_opts.extend_from_slice(&args[i + 1..]);
269 break;
270 }
271
272 if arg.starts_with("--") && arg.len() > 2 {
273 let rest = &arg[2..];
274 let (name, value) = match rest.split_once('=') {
275 Some((n, v)) => (n, Some(v)),
276 None => (rest, None),
277 };
278 if handle_long_option(name, value, ctx, args, &mut i, &mut output) {
279 errors = true;
280 }
281 } else if arg.starts_with('-') && arg.len() > 1 {
282 let rest = &arg[1..];
283
284 if alternative && rest.len() > 1 && !rest.starts_with('-') {
286 let (name, value) = match rest.split_once('=') {
287 Some((n, v)) => (n, Some(v)),
288 None => (rest, None),
289 };
290 let matches: Vec<_> = ctx
291 .long_opts
292 .iter()
293 .filter(|o| o.name.starts_with(name))
294 .collect();
295 if matches.len() == 1 {
296 if handle_long_option(
297 name,
298 value,
299 ctx,
300 args,
301 &mut i,
302 &mut output,
303 ) {
304 errors = true;
305 }
306 i += 1;
307 continue;
308 }
309 }
310
311 let chars: Vec<char> = rest.chars().collect();
313 let mut j = 0;
314 while j < chars.len() {
315 let ch = chars[j];
316 if let Some(opt) = short_opts.iter().find(|o| o.ch == ch) {
317 output.push(format!("-{ch}"));
318 match opt.arg_req {
319 ArgReq::Required => {
320 if j + 1 < chars.len() {
321 let val: String =
322 chars[j + 1..].iter().collect();
323 output.push(ctx.quote(&val));
324 j = chars.len();
325 } else {
326 i += 1;
327 if i < args.len() {
328 output.push(ctx.quote(&args[i]));
329 } else {
330 if !ctx.quiet {
331 eprintln!(
332 "{}: option requires an argument -- '{ch}'",
333 ctx.prog_name
334 );
335 }
336 errors = true;
337 }
338 }
339 }
340 ArgReq::Optional => {
341 if j + 1 < chars.len() {
342 let val: String =
343 chars[j + 1..].iter().collect();
344 output.push(ctx.quote(&val));
345 j = chars.len();
346 } else {
347 output.push(ctx.quote(""));
348 }
349 }
350 ArgReq::None => {}
351 }
352 } else {
353 if !ctx.quiet {
354 eprintln!(
355 "{}: invalid option -- '{ch}'",
356 ctx.prog_name
357 );
358 }
359 errors = true;
360 }
361 j += 1;
362 }
363 } else {
364 match scan_mode {
365 ScanMode::StopAtFirst => {
366 non_opts.push(arg.clone());
367 non_opts.extend(args[i + 1..].iter().cloned());
368 break;
369 }
370 ScanMode::InPlace => {
371 output.push(ctx.quote(arg));
372 }
373 ScanMode::Permute => {
374 non_opts.push(arg.clone());
375 }
376 }
377 }
378
379 i += 1;
380 }
381
382 output.push("--".to_string());
383 for no in &non_opts {
384 output.push(ctx.quote(no));
385 }
386
387 (output, errors)
388}
389
390pub fn run(args: Args) -> ExitCode {
391 if args.test {
392 return ExitCode::from(4);
393 }
394
395 let shell = match args.shell.as_str() {
396 "sh" | "bash" => Shell::Sh,
397 "csh" | "tcsh" => Shell::Csh,
398 other => {
399 eprintln!("getopt: unknown shell: {other}");
400 return ExitCode::from(2);
401 }
402 };
403
404 let (optstring, user_args) = if let Some(ref opts) = args.options {
405 (opts.as_str(), args.params.as_slice())
406 } else if !args.params.is_empty() {
407 (args.params[0].as_str(), &args.params[1..])
408 } else {
409 eprintln!("getopt: missing optstring argument");
410 return ExitCode::from(2);
411 };
412
413 let (short_opts, scan_mode) = parse_optstring(optstring);
414
415 let scan_mode = if std::env::var("POSIXLY_CORRECT").is_ok() {
416 ScanMode::StopAtFirst
417 } else {
418 scan_mode
419 };
420
421 let long_opts: Vec<LongOpt> = args
422 .longoptions
423 .iter()
424 .filter(|s| !s.is_empty())
425 .map(|s| parse_longopt_spec(s))
426 .collect();
427
428 let ctx = Ctx {
429 long_opts: &long_opts,
430 prog_name: args.name.as_deref().unwrap_or("getopt"),
431 quiet: args.quiet,
432 shell,
433 unquoted: args.unquoted,
434 };
435
436 let (output, errors) = parse_user_args(
437 &short_opts,
438 &ctx,
439 scan_mode,
440 args.alternative,
441 user_args,
442 );
443
444 if !args.quiet_output {
445 println!(" {}", output.join(" "));
446 }
447
448 if errors {
449 ExitCode::FAILURE
450 } else {
451 ExitCode::SUCCESS
452 }
453}