kern/cli.rs
1//! Command-line interface utilities
2
3use std::collections::HashMap;
4use std::str::FromStr;
5
6/// Builder for command-line parsing (Command)
7///
8/// # Example
9/// ```
10/// use kern::CliBuilder;
11/// use std::env;
12///
13/// let args: Vec<String> = env::args().collect();
14/// let command = CliBuilder::new().options(&["option"]).build(&args);
15/// assert_eq!(command.command().contains("rustdoctest"), true);
16/// ```
17#[derive(Clone, Debug, Default)]
18pub struct CliBuilder<'a> {
19 // raw: &'a str,
20 options: &'a [&'a str],
21 paramopts: &'a [&'a str],
22}
23
24impl<'a> CliBuilder<'a> {
25 /// Empty configuration for command-line parsing
26 pub fn new() -> Self {
27 Self {
28 // raw: "",
29 options: &[],
30 paramopts: &[],
31 }
32 }
33
34 /// Set options list
35 pub fn options(mut self, options: &'a [&str]) -> Self {
36 self.options = options;
37 self
38 }
39
40 /// Set list for parameter or option (not a parameter if value starts with "-")
41 pub fn paramopts(mut self, paramopts: &'a [&str]) -> Self {
42 self.paramopts = paramopts;
43 self
44 }
45
46 /// Create a new Command from raw command line arguments
47 pub fn build(self, raw: &'a [String]) -> Command<'a> {
48 // define command nam
49 let command = if raw.is_empty() { "" } else { &raw[0] };
50
51 // define variables
52 let mut parameters: HashMap<&str, &str> = HashMap::new();
53 let mut options: Vec<&str> = Vec::new();
54 let mut arguments: Vec<&str> = Vec::new();
55
56 // define iteration parameters
57 let mut parameter = "";
58 let mut is_parameter = false;
59
60 // iterate through raw arguments
61 for (index, argument) in raw.iter().enumerate() {
62 // check if first argument (command name)
63 if index == 0 {
64 // skip
65 continue;
66 }
67
68 // check if previous argument is a parameter
69 if is_parameter {
70 // insert parameter into map
71 parameters.insert(parameter, argument);
72
73 // empty parameter, compile safe
74 parameter = "";
75
76 // next is not a parameter
77 is_parameter = false;
78 } else {
79 // closure to process parameters using equal sign
80 let process_split = |parameters: &mut HashMap<&'a str, &'a str>,
81 parameter: &mut &'a str,
82 is_parameter: &mut bool,
83 argument: &'a str| {
84 // split argument
85 let splits = argument.splitn(2, '=');
86
87 // loop through one or two splitted parameters
88 for split in splits {
89 // check if second
90 if *is_parameter {
91 // insert parameter into map
92 parameters.insert(parameter, split);
93
94 // proceed with next argument
95 *is_parameter = false;
96 } else {
97 // store parameter name
98 *parameter = split;
99
100 // next on is a parameter
101 *is_parameter = true;
102 }
103 }
104 };
105
106 // check if argument is a parameter
107 if let Some(cut) = argument.strip_prefix("--") {
108 // check if option
109 if self.options.contains(&cut)
110 || self.paramopts.contains(&cut)
111 && raw
112 .get(index + 1)
113 .unwrap_or(&String::new())
114 .starts_with('-')
115 {
116 // add to options
117 options.push(cut);
118
119 // continue with next argument
120 continue;
121 }
122
123 // process parameter
124 process_split(&mut parameters, &mut parameter, &mut is_parameter, cut);
125 // check if argument is an option or short parameter
126 } else if let Some(cut) = argument.strip_prefix('-') {
127 // check if option
128 if self.options.contains(&cut)
129 || self.paramopts.contains(&cut)
130 && raw
131 .get(index + 1)
132 .unwrap_or(&String::new())
133 .starts_with('-')
134 {
135 // add to options
136 options.push(cut);
137
138 // continue with next argument
139 continue;
140 } else if cut.len() >= 2 && !cut.contains('=') {
141 // add all options to options
142 for i in 0..cut.len() {
143 // add only one character
144 options.push(match cut.get(i..=i) {
145 Some(option) => option,
146 None => continue,
147 });
148 }
149
150 // continue with next argument
151 continue;
152 } else if cut == "-" {
153 // process as argument
154 arguments.push(cut);
155 continue;
156 }
157
158 // process parameter
159 process_split(&mut parameters, &mut parameter, &mut is_parameter, cut);
160 } else {
161 // add to arguments
162 arguments.push(argument);
163 }
164 }
165 }
166
167 // last parameter without value must be option
168 if is_parameter {
169 // add parameter to options
170 options.push(parameter);
171 }
172
173 // return Command
174 Command::create(command, parameters, options, arguments)
175 }
176}
177
178/// Command represents a command parsed from the command-line
179/// Use CliBuilder to build a parsed Command
180///
181/// # Example
182/// ```
183/// use kern::CliBuilder;
184/// use std::env;
185///
186/// let args: Vec<String> = env::args().collect();
187/// let command = CliBuilder::new().options(&["option"]).build(&args);
188/// assert_eq!(command.command().contains("rustdoctest"), true);
189/// ```
190#[derive(Clone, Debug)]
191pub struct Command<'a> {
192 /// Command name
193 command: &'a str,
194
195 /// Map of parameters
196 parameters: HashMap<&'a str, &'a str>,
197
198 /// List of options
199 options: Vec<&'a str>,
200
201 /// List of arguments
202 arguments: Vec<&'a str>,
203}
204
205// Command implementation
206impl<'a> Command<'a> {
207 /// Get command name
208 pub fn command(&self) -> &'a str {
209 // return comand name
210 self.command
211 }
212
213 /// Get all parameters
214 pub fn parameters(&self) -> &HashMap<&'a str, &'a str> {
215 // return map of parameters
216 &self.parameters
217 }
218
219 /// Get all arguments
220 pub fn arguments(&self) -> &Vec<&'a str> {
221 // return arguments list
222 &self.arguments
223 }
224
225 /// Get specific parameter or default
226 pub fn parameter<T: FromStr>(&self, name: &str, default: T) -> T {
227 // return specific parameter or default
228 match self.parameters.get(name) {
229 Some(parameter) => parameter.parse().unwrap_or(default),
230 None => default,
231 }
232 }
233
234 /// Get specific parameter or default as &str
235 pub fn param(&self, name: &str, default: &'a str) -> &str {
236 // return specific parameter or default as &str
237 match self.parameters.get(name) {
238 Some(parameter) => parameter,
239 None => default,
240 }
241 }
242
243 /// Get all options
244 pub fn options(&self) -> &Vec<&str> {
245 // return options list
246 &self.options
247 }
248
249 /// Check if option provided
250 pub fn option(&self, name: &str) -> bool {
251 // return whether the option is provided
252 self.options.contains(&name)
253 }
254
255 /// Get argument at specific index or default
256 pub fn argument<T: FromStr>(&self, index: usize, default: T) -> T {
257 // return argument at specific index or default
258 match self.arguments.get(index) {
259 Some(argument) => argument.parse().unwrap_or(default),
260 None => default,
261 }
262 }
263
264 /// Get argument at specific index or default as &str
265 pub fn arg(&self, index: usize, default: &'a str) -> &str {
266 // return argument at specific index as &str
267 match self.arguments.get(index) {
268 Some(argument) => argument,
269 None => default,
270 }
271 }
272
273 /// Create a new Command from raw command line arguments without options
274 #[deprecated]
275 pub fn without_options(raw: &'a [String]) -> Self {
276 // return Command
277 CliBuilder::new().build(raw)
278 }
279
280 /// Create a new Command from raw command line arguments
281 /// Provide the options list as &[&str]
282 #[deprecated]
283 pub fn from(raw: &'a [String], options: &'a [&str]) -> Self {
284 // return Command
285 CliBuilder::new().options(options).build(raw)
286 }
287
288 /// Create Command from given values
289 fn create(
290 command: &'a str,
291 parameters: HashMap<&'a str, &'a str>,
292 options: Vec<&'a str>,
293 arguments: Vec<&'a str>,
294 ) -> Self {
295 Self {
296 command,
297 parameters,
298 options,
299 arguments,
300 }
301 }
302}