1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
use lexopt::ValueExt as _;
use rustix::fd::FromRawFd as _;
use std::fs::File;

pub(crate) enum Command {
    Check,
    Daemon { ready_fd: Option<File> },
    Sync { dry_run: bool },
    ResolveConflicts { dry_run: bool },
    Discover,
    Version,
}

pub(crate) struct Cli {
    pub command: Command,
    pub log_level: log::LevelFilter,
    pub config_file: Option<String>,
    pub pairs: Option<Vec<String>>,
}

impl Cli {
    pub fn parse(mut args: impl Iterator<Item = String>) -> Result<Cli, lexopt::Error> {
        let mut command = None;
        let mut log_level = log::LevelFilter::Warn;
        let mut config_file: Option<String> = None;
        let mut pairs = Vec::new();

        args.next(); // Skip arg0
        let mut parser = lexopt::Parser::from_args(args);
        while let Some(arg) = parser.next()? {
            match arg {
                lexopt::Arg::Short('v') => log_level = parser.value()?.parse()?,
                lexopt::Arg::Short('c') => config_file = Some(parser.value()?.string()?),
                lexopt::Arg::Short('p') => {
                    let pair_name = parser.value()?.string()?;
                    pairs.push(pair_name);
                }
                lexopt::Arg::Value(raw_cmd) => {
                    command = match raw_cmd.string()?.as_str() {
                        "check" => Some(Command::Check),
                        "daemon" => {
                            let mut ready_fd = None;
                            while let Some(arg) = parser.next()? {
                                match arg {
                                    lexopt::Arg::Short('r') => {
                                        let raw_fd = parser.value()?.parse()?;
                                        if raw_fd < 3 {
                                            return Err(
                                                "Readiness fd must be greater than 2".into()
                                            );
                                        }
                                        // SAFETY: this file descriptor is not accessed elsewhere.
                                        // The user is responsible for ensuring that they have
                                        // supplied a valid open file.
                                        ready_fd = Some(unsafe { File::from_raw_fd(raw_fd) });
                                    }
                                    _ => return Err(arg.unexpected()),
                                };
                            }
                            Some(Command::Daemon { ready_fd })
                        }
                        "sync" => {
                            let mut dry_run = false;
                            while let Some(arg) = parser.next()? {
                                match arg {
                                    lexopt::Arg::Short('d') => dry_run = true,
                                    _ => return Err(arg.unexpected()),
                                };
                            }
                            Some(Command::Sync { dry_run })
                        }
                        "resolve-conflicts" => {
                            let mut dry_run = false;
                            while let Some(arg) = parser.next()? {
                                match arg {
                                    lexopt::Arg::Short('d') => dry_run = true,
                                    _ => return Err(arg.unexpected()),
                                };
                            }
                            Some(Command::ResolveConflicts { dry_run })
                        }
                        "discover" => Some(Command::Discover),
                        "version" => Some(Command::Version),
                        cmd => return Err(format!("Unknown command: {cmd}").into()),
                    };
                    break;
                }
                _ => return Err(arg.unexpected()),
            };
        }

        Ok(Cli {
            command: command.ok_or(lexopt::Error::from("No command specified"))?,
            log_level,
            config_file,
            pairs: if pairs.is_empty() { None } else { Some(pairs) },
        })
    }
}