]>
Commit | Line | Data |
---|---|---|
b7329c8a DM |
1 | use failure::*; |
2 | use std::collections::HashMap; | |
f46403cc | 3 | use std::collections::HashSet; |
b7329c8a | 4 | |
4968bc3a WB |
5 | use serde_json::Value; |
6 | ||
b7329c8a DM |
7 | use crate::api::schema::*; |
8 | use crate::api::router::*; | |
a27a3ee4 | 9 | //use crate::api::config::*; |
0f253593 | 10 | use super::environment::CliEnvironment; |
6049b71f | 11 | |
0f253593 | 12 | use crate::getopts; |
6049b71f | 13 | |
b7329c8a DM |
14 | pub fn print_cli_usage() { |
15 | ||
16 | eprintln!("Usage: TODO"); | |
17 | } | |
18 | ||
4968bc3a WB |
19 | #[derive(Debug, Fail)] |
20 | #[fail(display = "Usage error: {}", _0)] | |
21 | pub struct UsageError(Error); | |
22 | ||
23 | pub struct Invocation<'a>(&'a CliCommand, Value); | |
24 | ||
25 | fn handle_simple_command(cli_cmd: &CliCommand, args: Vec<String>) -> Result<Invocation, Error> { | |
b7329c8a DM |
26 | |
27 | let (params, rest) = getopts::parse_arguments( | |
28 | &args, &cli_cmd.arg_param, &cli_cmd.info.parameters)?; | |
29 | ||
30 | if !rest.is_empty() { | |
31 | bail!("got additional arguments: {:?}", rest); | |
32 | } | |
33 | ||
4968bc3a | 34 | Ok(Invocation(cli_cmd, params)) |
b7329c8a DM |
35 | } |
36 | ||
6460764d | 37 | fn find_command<'a>(def: &'a CliCommandMap, name: &str) -> Option<&'a CommandLineInterface> { |
baed30b7 | 38 | |
6460764d | 39 | if let Some(sub_cmd) = def.commands.get(name) { |
baed30b7 DM |
40 | return Some(sub_cmd); |
41 | }; | |
42 | ||
43 | let mut matches: Vec<&str> = vec![]; | |
44 | ||
6460764d | 45 | for cmd in def.commands.keys() { |
baed30b7 DM |
46 | if cmd.starts_with(name) { |
47 | matches.push(cmd); } | |
48 | } | |
49 | ||
50 | if matches.len() != 1 { return None; } | |
51 | ||
6460764d | 52 | if let Some(sub_cmd) = def.commands.get(matches[0]) { |
baed30b7 DM |
53 | return Some(sub_cmd); |
54 | }; | |
55 | ||
56 | None | |
57 | } | |
58 | ||
4968bc3a | 59 | fn handle_nested_command(def: &CliCommandMap, mut args: Vec<String>) -> Result<Invocation, Error> { |
b7329c8a DM |
60 | |
61 | if args.len() < 1 { | |
6460764d | 62 | let mut cmds: Vec<&String> = def.commands.keys().collect(); |
b7329c8a DM |
63 | cmds.sort(); |
64 | ||
65 | let list = cmds.iter().fold(String::new(),|mut s,item| { | |
66 | if !s.is_empty() { s+= ", "; } | |
67 | s += item; | |
68 | s | |
69 | }); | |
70 | ||
71 | bail!("expected command argument, but no command specified.\nPossible commands: {}", list); | |
72 | } | |
73 | ||
74 | let command = args.remove(0); | |
75 | ||
baed30b7 | 76 | let sub_cmd = match find_command(def, &command) { |
b7329c8a | 77 | Some(cmd) => cmd, |
baed30b7 | 78 | None => bail!("no such command '{}'", command), |
b7329c8a DM |
79 | }; |
80 | ||
81 | match sub_cmd { | |
9f6ab1fc | 82 | CommandLineInterface::Simple(cli_cmd) => { |
4968bc3a | 83 | handle_simple_command(cli_cmd, args) |
b7329c8a | 84 | } |
9f6ab1fc | 85 | CommandLineInterface::Nested(map) => { |
4968bc3a | 86 | handle_nested_command(map, args) |
b7329c8a DM |
87 | } |
88 | } | |
b7329c8a DM |
89 | } |
90 | ||
30d2e99c DM |
91 | fn print_property_completion( |
92 | schema: &Schema, | |
93 | name: &str, | |
94 | completion_functions: &HashMap<String, CompletionFunction>, | |
95 | arg: &str) | |
96 | { | |
97 | if let Some(callback) = completion_functions.get(name) { | |
244d9b17 | 98 | let list = (callback)(arg); |
30d2e99c DM |
99 | for value in list { |
100 | if value.starts_with(arg) { | |
101 | println!("{}", value); | |
102 | } | |
103 | } | |
104 | return; | |
105 | } | |
106 | ||
38555b50 DM |
107 | if let Schema::String(StringSchema { format: Some(format), ..} ) = schema { |
108 | if let ApiStringFormat::Enum(list) = format.as_ref() { | |
109 | for value in list { | |
110 | if value.starts_with(arg) { | |
111 | println!("{}", value); | |
112 | } | |
113 | } | |
114 | return; | |
115 | } | |
116 | } | |
f46403cc DM |
117 | println!(""); |
118 | } | |
b6e8dd39 | 119 | |
f46403cc DM |
120 | fn record_done_arguments(done: &mut HashSet<String>, parameters: &ObjectSchema, list: &[String]) { |
121 | ||
122 | for arg in list { | |
123 | if arg.starts_with("--") && arg.len() > 2 { | |
124 | let prop_name = arg[2..].to_owned(); | |
379ea0ed | 125 | if let Some((_, schema)) = parameters.properties.get::<str>(&prop_name) { |
f46403cc DM |
126 | match schema.as_ref() { |
127 | Schema::Array(_) => { /* do nothing */ } | |
128 | _ => { done.insert(prop_name); } | |
129 | } | |
130 | } | |
131 | } | |
132 | } | |
133 | } | |
134 | ||
135 | fn print_simple_completion( | |
136 | cli_cmd: &CliCommand, | |
ca9caffa | 137 | done: &mut HashSet<String>, |
f46403cc DM |
138 | arg_param: &[&str], |
139 | mut args: Vec<String>, | |
140 | ) { | |
141 | // fixme: arg_param, fixed_param | |
142 | //eprintln!("COMPL: {:?} {:?} {}", arg_param, args, args.len()); | |
143 | ||
144 | if !arg_param.is_empty() { | |
145 | let prop_name = arg_param[0]; | |
146 | done.insert(prop_name.into()); | |
147 | if args.len() > 1 { | |
148 | args.remove(0); | |
149 | print_simple_completion(cli_cmd, done, &arg_param[1..], args); | |
150 | return; | |
7f4e639b | 151 | } else if args.len() == 1 { |
379ea0ed | 152 | if let Some((_, schema)) = cli_cmd.info.parameters.properties.get(prop_name) { |
30d2e99c | 153 | print_property_completion(schema, prop_name, &cli_cmd.completion_functions, &args[0]); |
b6e8dd39 | 154 | } |
f46403cc DM |
155 | } |
156 | return; | |
157 | } | |
158 | if args.is_empty() { return; } | |
159 | ||
ca9caffa | 160 | record_done_arguments(done, &cli_cmd.info.parameters, &args); |
2f025895 | 161 | |
f46403cc DM |
162 | let prefix = args.pop().unwrap(); // match on last arg |
163 | ||
2f025895 DM |
164 | // complete option-name or option-value ? |
165 | if !prefix.starts_with("-") && args.len() > 0 { | |
166 | let last = &args[args.len()-1]; | |
167 | if last.starts_with("--") && last.len() > 2 { | |
ca9caffa | 168 | let prop_name = &last[2..]; |
379ea0ed | 169 | if let Some((_, schema)) = cli_cmd.info.parameters.properties.get(prop_name) { |
30d2e99c | 170 | print_property_completion(schema, prop_name, &cli_cmd.completion_functions, &prefix); |
2f025895 DM |
171 | } |
172 | return; | |
173 | } | |
174 | } | |
f46403cc | 175 | |
379ea0ed | 176 | for (name, (_optional, _schema)) in &cli_cmd.info.parameters.properties { |
f46403cc | 177 | if done.contains(*name) { continue; } |
2f025895 DM |
178 | let option = String::from("--") + name; |
179 | if option.starts_with(&prefix) { | |
180 | println!("{}", option); | |
f46403cc DM |
181 | } |
182 | } | |
183 | } | |
184 | ||
185 | fn print_nested_completion(def: &CommandLineInterface, mut args: Vec<String>) { | |
186 | ||
187 | match def { | |
188 | CommandLineInterface::Simple(cli_cmd) => { | |
189 | let mut done = HashSet::new(); | |
190 | let fixed: Vec<String> = cli_cmd.fixed_param.iter().map(|s| s.to_string()).collect(); | |
191 | record_done_arguments(&mut done, &cli_cmd.info.parameters, &fixed); | |
192 | print_simple_completion(cli_cmd, &mut done, &cli_cmd.arg_param, args); | |
b6e8dd39 DM |
193 | return; |
194 | } | |
195 | CommandLineInterface::Nested(map) => { | |
196 | if args.is_empty() { | |
197 | for cmd in map.commands.keys() { | |
198 | println!("{}", cmd); | |
199 | } | |
200 | return; | |
201 | } | |
202 | let first = args.remove(0); | |
203 | if let Some(sub_cmd) = map.commands.get(&first) { | |
f46403cc | 204 | print_nested_completion(sub_cmd, args); |
b6e8dd39 DM |
205 | return; |
206 | } | |
207 | for cmd in map.commands.keys() { | |
208 | if cmd.starts_with(&first) { | |
209 | println!("{}", cmd); | |
210 | } | |
211 | } | |
212 | } | |
213 | } | |
214 | } | |
215 | ||
216 | pub fn print_bash_completion(def: &CommandLineInterface) { | |
217 | ||
218 | let comp_point: usize = match std::env::var("COMP_POINT") { | |
219 | Ok(val) => { | |
220 | match usize::from_str_radix(&val, 10) { | |
221 | Ok(i) => i, | |
a27a3ee4 | 222 | Err(_) => return, |
b6e8dd39 DM |
223 | } |
224 | } | |
a27a3ee4 | 225 | Err(_) => return, |
b6e8dd39 DM |
226 | }; |
227 | ||
228 | let cmdline = match std::env::var("COMP_LINE") { | |
229 | Ok(val) => val[0..comp_point].to_owned(), | |
a27a3ee4 | 230 | Err(_) => return, |
b6e8dd39 DM |
231 | }; |
232 | ||
f46403cc | 233 | |
b6e8dd39 DM |
234 | let mut args = match shellwords::split(&cmdline) { |
235 | Ok(v) => v, | |
236 | Err(_) => return, | |
237 | }; | |
238 | ||
239 | args.remove(0); //no need for program name | |
240 | ||
f46403cc DM |
241 | if cmdline.ends_with(char::is_whitespace) { |
242 | //eprintln!("CMDLINE {:?}", cmdline); | |
243 | args.push("".into()); | |
244 | } | |
245 | ||
b6e8dd39 DM |
246 | //eprintln!("COMP_ARGS {:?}", args); |
247 | ||
f46403cc | 248 | print_nested_completion(def, args); |
b6e8dd39 DM |
249 | } |
250 | ||
9f6ab1fc | 251 | pub fn run_cli_command(def: &CommandLineInterface) -> Result<(), Error> { |
b7329c8a DM |
252 | |
253 | let args: Vec<String> = std::env::args().skip(1).collect(); | |
254 | ||
c21de379 | 255 | if !args.is_empty() && args[0] == "bashcomplete" { |
b6e8dd39 | 256 | print_bash_completion(def); |
c21de379 DM |
257 | return Ok(()); |
258 | } | |
259 | ||
4968bc3a | 260 | let invocation = match def { |
9f6ab1fc DM |
261 | CommandLineInterface::Simple(cli_cmd) => handle_simple_command(cli_cmd, args), |
262 | CommandLineInterface::Nested(map) => handle_nested_command(map, args), | |
4968bc3a WB |
263 | }; |
264 | ||
6049b71f | 265 | let mut rpcenv = CliEnvironment::new(); |
4968bc3a WB |
266 | |
267 | let res = match invocation { | |
268 | Err(e) => return Err(UsageError(e).into()), | |
6049b71f | 269 | Ok(invocation) => (invocation.0.info.handler)(invocation.1, &invocation.0.info, &mut rpcenv)?, |
4968bc3a WB |
270 | }; |
271 | ||
272 | println!("Result: {}", serde_json::to_string_pretty(&res).unwrap()); | |
273 | ||
274 | Ok(()) | |
b7329c8a DM |
275 | } |
276 | ||
244d9b17 | 277 | pub type CompletionFunction = fn(&str) -> Vec<String>; |
30d2e99c | 278 | |
b7329c8a DM |
279 | pub struct CliCommand { |
280 | pub info: ApiMethod, | |
281 | pub arg_param: Vec<&'static str>, | |
282 | pub fixed_param: Vec<&'static str>, | |
30d2e99c | 283 | pub completion_functions: HashMap<String, CompletionFunction>, |
b7329c8a DM |
284 | } |
285 | ||
bf7f1039 DM |
286 | impl CliCommand { |
287 | ||
288 | pub fn new(info: ApiMethod) -> Self { | |
30d2e99c DM |
289 | Self { |
290 | info, arg_param: vec![], | |
291 | fixed_param: vec![], | |
292 | completion_functions: HashMap::new(), | |
293 | } | |
bf7f1039 DM |
294 | } |
295 | ||
296 | pub fn arg_param(mut self, names: Vec<&'static str>) -> Self { | |
297 | self.arg_param = names; | |
298 | self | |
299 | } | |
300 | ||
301 | pub fn fixed_param(mut self, args: Vec<&'static str>) -> Self { | |
302 | self.fixed_param = args; | |
303 | self | |
304 | } | |
30d2e99c DM |
305 | |
306 | pub fn completion_cb(mut self, param_name: &str, cb: CompletionFunction) -> Self { | |
307 | self.completion_functions.insert(param_name.into(), cb); | |
308 | self | |
309 | } | |
bf7f1039 DM |
310 | } |
311 | ||
6460764d DM |
312 | pub struct CliCommandMap { |
313 | pub commands: HashMap<String, CommandLineInterface>, | |
314 | } | |
315 | ||
316 | impl CliCommandMap { | |
317 | ||
318 | pub fn new() -> Self { | |
319 | Self { commands: HashMap:: new() } | |
320 | } | |
321 | ||
322 | pub fn insert<S: Into<String>>(mut self, name: S, cli: CommandLineInterface) -> Self { | |
323 | self.commands.insert(name.into(), cli); | |
324 | self | |
325 | } | |
326 | } | |
327 | ||
9f6ab1fc | 328 | pub enum CommandLineInterface { |
b7329c8a | 329 | Simple(CliCommand), |
6460764d | 330 | Nested(CliCommandMap), |
b7329c8a | 331 | } |
8f62336b DM |
332 | |
333 | impl From<CliCommand> for CommandLineInterface { | |
334 | fn from(cli_cmd: CliCommand) -> Self { | |
335 | CommandLineInterface::Simple(cli_cmd) | |
336 | } | |
337 | } | |
338 | ||
6460764d DM |
339 | impl From<CliCommandMap> for CommandLineInterface { |
340 | fn from(list: CliCommandMap) -> Self { | |
341 | CommandLineInterface::Nested(list) | |
8f62336b DM |
342 | } |
343 | } |