]> git.proxmox.com Git - proxmox.git/blob - proxmox-router/src/cli/getopts.rs
6ab8218766e9236cbd4ebde1a8bff095ffa45bf2
[proxmox.git] / proxmox-router / src / cli / getopts.rs
1 use std::collections::HashMap;
2
3 use anyhow::format_err;
4 use serde_json::Value;
5
6 use proxmox_schema::*;
7
8 #[derive(Debug)]
9 enum RawArgument {
10 Separator,
11 Argument { value: String },
12 Option { name: String, value: Option<String> },
13 }
14
15 fn parse_argument(arg: &str) -> RawArgument {
16 let bytes = arg.as_bytes();
17
18 let length = bytes.len();
19
20 if length < 2 || bytes[0] != b'-' {
21 return RawArgument::Argument {
22 value: arg.to_string(),
23 };
24 }
25
26 let first = if bytes[1] == b'-' {
27 if length == 2 {
28 return RawArgument::Separator;
29 }
30 2
31 } else {
32 1
33 };
34
35 if let Some(i) = bytes[first..length].iter().position(|b| *b == b'=') {
36 let start = i + first;
37 // Since we take a &str, we know the contents of it are valid utf8.
38 // Since bytes[start] == b'=', we know the byte beginning at start is a single-byte
39 // code pointer. We also know that 'first' points exactly after a single-byte code
40 // point as it points to the first byte after a hyphen.
41 // Therefore we know arg[first..start] is valid utf-8, therefore it is safe to use
42 // get_unchecked() to speed things up.
43 return RawArgument::Option {
44 name: unsafe { arg.get_unchecked(first..start).to_string() },
45 value: Some(unsafe { arg.get_unchecked((start + 1)..).to_string() }),
46 };
47 }
48
49 RawArgument::Option {
50 name: unsafe { arg.get_unchecked(first..).to_string() },
51 value: None,
52 }
53 }
54
55 /// parse as many arguments as possible into a Vec<String, String>. This does not
56 /// verify the schema.
57 /// Returns parsed data and the remaining arguments as two separate array
58 pub(crate) fn parse_argument_list<T: AsRef<str>>(
59 args: &[T],
60 schema: ParameterSchema,
61 errors: &mut ParameterError,
62 ) -> (Vec<(String, String)>, Vec<String>) {
63 let mut data: Vec<(String, String)> = vec![];
64 let mut remaining: Vec<String> = vec![];
65
66 let mut pos = 0;
67
68 while pos < args.len() {
69 match parse_argument(args[pos].as_ref()) {
70 RawArgument::Separator => {
71 break;
72 }
73 RawArgument::Option { name, value } => match value {
74 None => {
75 let mut want_bool = false;
76 let mut can_default = false;
77 if let Some((_opt, Schema::Boolean(boolean_schema))) = schema.lookup(&name) {
78 want_bool = true;
79 match boolean_schema.default {
80 Some(false) | None => can_default = true,
81 Some(true) => (),
82 }
83 }
84
85 let mut next_is_argument = false;
86 let mut next_is_bool = false;
87
88 if (pos + 1) < args.len() {
89 let next = args[pos + 1].as_ref();
90 if let RawArgument::Argument { .. } = parse_argument(next) {
91 next_is_argument = true;
92 if parse_boolean(next).is_ok() {
93 next_is_bool = true;
94 }
95 }
96 }
97
98 if want_bool {
99 if next_is_bool {
100 pos += 1;
101 data.push((name, args[pos].as_ref().to_string()));
102 } else if can_default {
103 data.push((name, "true".to_string()));
104 } else {
105 errors.push(name.to_string(), format_err!("missing boolean value."));
106 }
107 } else if next_is_argument {
108 pos += 1;
109 data.push((name, args[pos].as_ref().to_string()));
110 } else {
111 errors.push(name.to_string(), format_err!("missing parameter value."));
112 }
113 }
114 Some(v) => {
115 data.push((name, v));
116 }
117 },
118 RawArgument::Argument { value } => {
119 remaining.push(value);
120 }
121 }
122
123 pos += 1;
124 }
125
126 remaining.reserve(args.len() - pos);
127 for i in &args[pos..] {
128 remaining.push(i.as_ref().to_string());
129 }
130
131 (data, remaining)
132 }
133
134 /// Parses command line arguments using a `Schema`
135 ///
136 /// Returns parsed options as json object, together with the
137 /// list of additional command line arguments.
138 pub fn parse_arguments<T: AsRef<str>>(
139 args: &[T],
140 arg_param: &[&str],
141 fixed_param: &HashMap<&'static str, String>,
142 schema: ParameterSchema,
143 ) -> Result<(Value, Vec<String>), ParameterError> {
144 let mut errors = ParameterError::new();
145
146 // first check if all arg_param exists in schema
147
148 let mut last_arg_param_is_optional = false;
149 let mut last_arg_param_is_array = false;
150
151 for i in 0..arg_param.len() {
152 let name = arg_param[i];
153 if let Some((optional, param_schema)) = schema.lookup(name) {
154 if i == arg_param.len() - 1 {
155 last_arg_param_is_optional = optional;
156 if let Schema::Array(_) = param_schema {
157 last_arg_param_is_array = true;
158 }
159 } else if optional {
160 panic!("positional argument '{}' may not be optional", name);
161 }
162 } else {
163 panic!("no such property '{}' in schema", name);
164 }
165 }
166
167 let (mut data, mut remaining) = parse_argument_list(args, schema, &mut errors);
168
169 for i in 0..arg_param.len() {
170 let name = arg_param[i];
171 let is_last_arg_param = i == (arg_param.len() - 1);
172
173 if remaining.is_empty() {
174 if !(is_last_arg_param && last_arg_param_is_optional) {
175 errors.push(name.to_string(), format_err!("missing argument"));
176 }
177 } else if is_last_arg_param && last_arg_param_is_array {
178 for value in remaining {
179 data.push((name.to_string(), value));
180 }
181 remaining = vec![];
182 } else {
183 data.push((name.to_string(), remaining.remove(0)));
184 }
185 }
186
187 if !errors.is_empty() {
188 return Err(errors);
189 }
190
191 for (name, value) in fixed_param.iter() {
192 data.push((name.to_string(), value.to_string()));
193 }
194
195 let options = schema.parse_parameter_strings(&data, true)?;
196
197 Ok((options, remaining))
198 }
199
200 #[test]
201 fn test_boolean_arg() {
202 const PARAMETERS: ObjectSchema = ObjectSchema::new(
203 "Parameters:",
204 &[("enable", false, &BooleanSchema::new("Enable").schema())],
205 );
206
207 let mut variants: Vec<(Vec<&str>, bool)> = vec![];
208 variants.push((vec!["-enable"], true));
209 variants.push((vec!["-enable=1"], true));
210 variants.push((vec!["-enable", "yes"], true));
211 variants.push((vec!["-enable", "Yes"], true));
212 variants.push((vec!["--enable", "1"], true));
213 variants.push((vec!["--enable", "ON"], true));
214 variants.push((vec!["--enable", "true"], true));
215
216 variants.push((vec!["--enable", "0"], false));
217 variants.push((vec!["--enable", "no"], false));
218 variants.push((vec!["--enable", "off"], false));
219 variants.push((vec!["--enable", "false"], false));
220
221 for (args, expect) in variants {
222 let res = parse_arguments(
223 &args,
224 &[],
225 &HashMap::new(),
226 ParameterSchema::from(&PARAMETERS),
227 );
228 assert!(res.is_ok());
229 if let Ok((options, remaining)) = res {
230 assert!(options["enable"] == expect);
231 assert!(remaining.is_empty());
232 }
233 }
234 }
235
236 #[test]
237 fn test_argument_paramenter() {
238 use proxmox_schema::*;
239
240 const PARAMETERS: ObjectSchema = ObjectSchema::new(
241 "Parameters:",
242 &[
243 ("enable", false, &BooleanSchema::new("Enable.").schema()),
244 ("storage", false, &StringSchema::new("Storage.").schema()),
245 ],
246 );
247
248 let args = vec!["-enable", "local"];
249 let res = parse_arguments(
250 &args,
251 &["storage"],
252 &HashMap::new(),
253 ParameterSchema::from(&PARAMETERS),
254 );
255 assert!(res.is_ok());
256 if let Ok((options, remaining)) = res {
257 assert!(options["enable"] == true);
258 assert!(options["storage"] == "local");
259 assert!(remaining.is_empty());
260 }
261 }