]> git.proxmox.com Git - proxmox-backup.git/blame - src/cli/command.rs
cleanup auth code, verify CSRF prevention token
[proxmox-backup.git] / src / cli / command.rs
CommitLineData
b7329c8a
DM
1use failure::*;
2use std::collections::HashMap;
f46403cc 3use std::collections::HashSet;
b7329c8a 4
4968bc3a
WB
5use serde_json::Value;
6
b7329c8a
DM
7use crate::api::schema::*;
8use crate::api::router::*;
a27a3ee4 9//use crate::api::config::*;
0f253593 10use super::environment::CliEnvironment;
6049b71f 11
0f253593 12use crate::getopts;
6049b71f 13
b7329c8a
DM
14pub fn print_cli_usage() {
15
16 eprintln!("Usage: TODO");
17}
18
4968bc3a
WB
19#[derive(Debug, Fail)]
20#[fail(display = "Usage error: {}", _0)]
21pub struct UsageError(Error);
22
23pub struct Invocation<'a>(&'a CliCommand, Value);
24
25fn 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 37fn 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 59fn 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
91fn 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
120fn 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
135fn 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
185fn 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
216pub 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 251pub 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 277pub type CompletionFunction = fn(&str) -> Vec<String>;
30d2e99c 278
b7329c8a
DM
279pub 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
286impl 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
312pub struct CliCommandMap {
313 pub commands: HashMap<String, CommandLineInterface>,
314}
315
316impl 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 328pub enum CommandLineInterface {
b7329c8a 329 Simple(CliCommand),
6460764d 330 Nested(CliCommandMap),
b7329c8a 331}
8f62336b
DM
332
333impl From<CliCommand> for CommandLineInterface {
334 fn from(cli_cmd: CliCommand) -> Self {
335 CommandLineInterface::Simple(cli_cmd)
336 }
337}
338
6460764d
DM
339impl From<CliCommandMap> for CommandLineInterface {
340 fn from(list: CliCommandMap) -> Self {
341 CommandLineInterface::Nested(list)
8f62336b
DM
342 }
343}