Skip to main content

linuxutils_misc/
getopt.rs

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    /// Allow long options to start with a single -
18    #[arg(short = 'a', long = "alternative")]
19    alternative: bool,
20
21    /// Long options to recognize (comma-separated)
22    #[arg(short = 'l', long = "longoptions", value_delimiter = ',')]
23    longoptions: Vec<String>,
24
25    /// Program name for error messages
26    #[arg(short = 'n', long = "name")]
27    name: Option<String>,
28
29    /// Short options string to recognize
30    #[arg(short = 'o', long = "options")]
31    options: Option<String>,
32
33    /// Disable error output from getopt
34    #[arg(short = 'q', long = "quiet")]
35    quiet: bool,
36
37    /// Suppress normal output
38    #[arg(short = 'Q', long = "quiet-output")]
39    quiet_output: bool,
40
41    /// Set quoting conventions for shell (sh, bash, csh, tcsh)
42    #[arg(short = 's', long = "shell", default_value = "bash")]
43    shell: String,
44
45    /// Test for enhanced getopt version (returns exit code 4)
46    #[arg(short = 'T', long = "test")]
47    test: bool,
48
49    /// Don't quote the output
50    #[arg(short = 'u', long = "unquoted")]
51    unquoted: bool,
52
53    /// Parameters to parse
54    #[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    // Leading : suppresses error reporting (handled separately via -q)
102    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            // Try alternative long option (single - prefix)
285            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            // Short options
312            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}