1use std::ffi::{OsStr, OsString};
17
18use ready_set_sdk::OutputMode;
19use ready_set_sdk::context::{ColorMode, LogLevel};
20
21#[derive(Debug)]
23pub enum ParsedArgs {
24 Help,
26 Version,
28 List {
30 all: bool,
32 globals: GlobalFlags,
34 },
35 Subcommand {
37 name: String,
39 args: Vec<OsString>,
41 globals: GlobalFlags,
43 },
44 Empty {
46 globals: GlobalFlags,
48 },
49}
50
51#[derive(Debug, Clone, Default)]
53pub struct GlobalFlags {
54 pub output: Option<OutputMode>,
56 pub log: Option<LogLevel>,
58 pub color: Option<ColorMode>,
60}
61
62#[must_use]
67pub fn parse(mut args: impl Iterator<Item = OsString>) -> ParsedArgs {
68 drop(args.next()); let mut globals = GlobalFlags::default();
71 let mut list_seen = false;
72 let mut list_all = false;
73
74 while let Some(arg) = args.next() {
75 if arg == OsStr::new("--help") || arg == OsStr::new("-h") {
76 return ParsedArgs::Help;
77 }
78 if arg == OsStr::new("--version") || arg == OsStr::new("-V") {
79 return ParsedArgs::Version;
80 }
81 if arg == OsStr::new("--list") {
82 list_seen = true;
83 continue;
84 }
85 if list_seen && arg == OsStr::new("--all") {
86 list_all = true;
87 continue;
88 }
89 if arg == OsStr::new("--quiet") {
90 globals.log = Some(LogLevel::Quiet);
91 continue;
92 }
93 if arg == OsStr::new("--verbose") {
94 globals.log = Some(LogLevel::Verbose);
95 continue;
96 }
97 if arg == OsStr::new("--json") {
98 globals.output = Some(OutputMode::Json);
99 continue;
100 }
101 if arg == OsStr::new("--color") {
102 if let Some(value) = args.next() {
103 globals.color = Some(match value.to_string_lossy().as_ref() {
104 "always" => ColorMode::Always,
105 "never" => ColorMode::Never,
106 _ => ColorMode::Auto,
107 });
108 }
109 continue;
110 }
111
112 if list_seen {
114 }
116 let name = arg.to_string_lossy().into_owned();
117 let rest: Vec<OsString> = args.collect();
118 return ParsedArgs::Subcommand {
119 name,
120 args: rest,
121 globals,
122 };
123 }
124
125 if list_seen {
126 return ParsedArgs::List {
127 all: list_all,
128 globals,
129 };
130 }
131 ParsedArgs::Empty { globals }
132}
133
134#[cfg(test)]
135#[allow(clippy::panic, clippy::missing_panics_doc)]
136mod tests {
137 use super::*;
138
139 fn parse_str(args: &[&str]) -> ParsedArgs {
140 parse(args.iter().map(OsString::from))
141 }
142
143 #[test]
144 fn parses_help() {
145 assert!(matches!(
146 parse_str(&["ready-set", "--help"]),
147 ParsedArgs::Help
148 ));
149 assert!(matches!(parse_str(&["ready-set", "-h"]), ParsedArgs::Help));
150 }
151
152 #[test]
153 fn parses_version() {
154 assert!(matches!(
155 parse_str(&["ready-set", "--version"]),
156 ParsedArgs::Version
157 ));
158 assert!(matches!(
159 parse_str(&["ready-set", "-V"]),
160 ParsedArgs::Version
161 ));
162 }
163
164 #[test]
165 fn parses_list_with_all() {
166 let ParsedArgs::List { all, .. } = parse_str(&["ready-set", "--list", "--all"]) else {
167 panic!("expected List variant");
168 };
169 assert!(all);
170 }
171
172 #[test]
173 fn list_carries_global_flags() {
174 let ParsedArgs::List { globals, .. } = parse_str(&["ready-set", "--json", "--list"]) else {
175 panic!("expected List variant");
176 };
177 assert_eq!(globals.output, Some(OutputMode::Json));
178 }
179
180 #[test]
181 fn parses_subcommand_with_passthrough() {
182 let ParsedArgs::Subcommand { name, args, .. } =
183 parse_str(&["ready-set", "scan", "--json", "--path", "/tmp"])
184 else {
185 panic!("expected Subcommand variant");
186 };
187 assert_eq!(name, "scan");
188 assert_eq!(
189 args,
190 vec![
191 OsString::from("--json"),
192 OsString::from("--path"),
193 OsString::from("/tmp"),
194 ]
195 );
196 }
197
198 #[test]
199 fn captures_global_flags_before_subcommand() {
200 let ParsedArgs::Subcommand { name, globals, .. } =
201 parse_str(&["ready-set", "--json", "--verbose", "go"])
202 else {
203 panic!("expected Subcommand variant");
204 };
205 assert_eq!(name, "go");
206 assert_eq!(globals.output, Some(OutputMode::Json));
207 assert_eq!(globals.log, Some(LogLevel::Verbose));
208 }
209}