]> git.proxmox.com Git - proxmox.git/blob - proxmox-router/src/cli/command.rs
8ae889f95e4efb8b67cfb338062ba11363e71d11
[proxmox.git] / proxmox-router / src / cli / command.rs
1 use anyhow::{format_err, Error};
2 use serde_json::Value;
3 use std::cell::RefCell;
4 use std::sync::Arc;
5
6 use proxmox_schema::format::DocumentationFormat;
7 use proxmox_schema::*;
8
9 use super::environment::CliEnvironment;
10 use super::getopts;
11 use super::{
12 generate_nested_usage, generate_usage_str, print_help, print_nested_usage_error,
13 print_simple_usage_error, CliCommand, CliCommandMap, CommandLineInterface,
14 };
15 use crate::{ApiFuture, ApiHandler, ApiMethod, RpcEnvironment};
16
17 /// Schema definition for ``--output-format`` parameter.
18 ///
19 /// - ``text``: command specific text format.
20 /// - ``json``: JSON, single line.
21 /// - ``json-pretty``: JSON, human readable.
22 ///
23 pub const OUTPUT_FORMAT: Schema = StringSchema::new("Output format.")
24 .format(&ApiStringFormat::Enum(&[
25 EnumEntry::new("text", "plain text output"),
26 EnumEntry::new("json", "single-line json formatted output"),
27 EnumEntry::new("json-pretty", "pretty-printed json output"),
28 ]))
29 .schema();
30
31 fn parse_arguments(prefix: &str, cli_cmd: &CliCommand, args: Vec<String>) -> Result<Value, Error> {
32 let (params, remaining) = match getopts::parse_arguments(
33 &args,
34 cli_cmd.arg_param,
35 &cli_cmd.fixed_param,
36 cli_cmd.info.parameters,
37 ) {
38 Ok((p, r)) => (p, r),
39 Err(err) => {
40 let err_msg = err.to_string();
41 print_simple_usage_error(prefix, cli_cmd, &err_msg);
42 return Err(format_err!("{}", err_msg));
43 }
44 };
45
46 if !remaining.is_empty() {
47 let err_msg = format!("got additional arguments: {:?}", remaining);
48 print_simple_usage_error(prefix, cli_cmd, &err_msg);
49 return Err(format_err!("{}", err_msg));
50 }
51
52 Ok(params)
53 }
54
55 async fn handle_simple_command_future(
56 prefix: &str,
57 cli_cmd: &CliCommand,
58 args: Vec<String>,
59 mut rpcenv: CliEnvironment,
60 ) -> Result<(), Error> {
61 let params = parse_arguments(prefix, cli_cmd, args)?;
62
63 let result = match cli_cmd.info.handler {
64 ApiHandler::Sync(handler) => (handler)(params, cli_cmd.info, &mut rpcenv),
65 ApiHandler::StreamingSync(handler) => (handler)(params, cli_cmd.info, &mut rpcenv)
66 .and_then(|r| r.to_value().map_err(Error::from)),
67 ApiHandler::Async(handler) => (handler)(params, cli_cmd.info, &mut rpcenv).await,
68 ApiHandler::StreamingAsync(handler) => (handler)(params, cli_cmd.info, &mut rpcenv)
69 .await
70 .and_then(|r| r.to_value().map_err(Error::from)),
71 #[cfg(feature = "server")]
72 ApiHandler::AsyncHttp(_) => {
73 let err_msg = "CliHandler does not support ApiHandler::AsyncHttp - internal error";
74 print_simple_usage_error(prefix, cli_cmd, err_msg);
75 return Err(format_err!("{}", err_msg));
76 }
77 };
78
79 match result {
80 Ok(value) => {
81 if value != Value::Null {
82 println!("Result: {}", serde_json::to_string_pretty(&value).unwrap());
83 }
84 }
85 Err(err) => {
86 eprintln!("Error: {}", err);
87 return Err(err);
88 }
89 }
90
91 Ok(())
92 }
93
94 fn handle_simple_command(
95 prefix: &str,
96 cli_cmd: &CliCommand,
97 args: Vec<String>,
98 mut rpcenv: CliEnvironment,
99 run: Option<fn(ApiFuture) -> Result<Value, Error>>,
100 ) -> Result<(), Error> {
101 let params = parse_arguments(prefix, cli_cmd, args)?;
102
103 let result = match cli_cmd.info.handler {
104 ApiHandler::Sync(handler) => (handler)(params, cli_cmd.info, &mut rpcenv),
105 ApiHandler::StreamingSync(handler) => (handler)(params, cli_cmd.info, &mut rpcenv)
106 .and_then(|r| r.to_value().map_err(Error::from)),
107 ApiHandler::Async(handler) => match run {
108 Some(run) => {
109 let future = (handler)(params, cli_cmd.info, &mut rpcenv);
110 (run)(future)
111 }
112 None => {
113 let err_msg = "CliHandler does not support ApiHandler::Async - internal error";
114 print_simple_usage_error(prefix, cli_cmd, err_msg);
115 return Err(format_err!("{}", err_msg));
116 }
117 },
118 ApiHandler::StreamingAsync(_handler) => {
119 let err_msg = "CliHandler does not support ApiHandler::StreamingAsync - internal error";
120 print_simple_usage_error(prefix, cli_cmd, err_msg);
121 return Err(format_err!("{}", err_msg));
122 }
123 #[cfg(feature = "server")]
124 ApiHandler::AsyncHttp(_) => {
125 let err_msg = "CliHandler does not support ApiHandler::AsyncHttp - internal error";
126 print_simple_usage_error(prefix, cli_cmd, err_msg);
127 return Err(format_err!("{}", err_msg));
128 }
129 };
130
131 match result {
132 Ok(value) => {
133 if value != Value::Null {
134 println!("Result: {}", serde_json::to_string_pretty(&value).unwrap());
135 }
136 }
137 Err(err) => {
138 eprintln!("Error: {}", err);
139 return Err(err);
140 }
141 }
142
143 Ok(())
144 }
145
146 fn parse_nested_command<'a>(
147 prefix: &mut String,
148 def: &'a CliCommandMap,
149 args: &mut Vec<String>,
150 ) -> Result<&'a CliCommand, Error> {
151 let mut map = def;
152
153 // Note: Avoid async recursive function, because current rust compiler cant handle that
154 loop {
155 replace_aliases(args, &map.aliases);
156
157 if args.is_empty() {
158 let mut cmds: Vec<&String> = map.commands.keys().collect();
159 cmds.sort();
160
161 let list = cmds.iter().fold(String::new(), |mut s, item| {
162 if !s.is_empty() {
163 s += ", ";
164 }
165 s += item;
166 s
167 });
168
169 let err_msg = format!("no command specified.\nPossible commands: {}", list);
170 print_nested_usage_error(prefix, map, &err_msg);
171 return Err(format_err!("{}", err_msg));
172 }
173
174 let command = args.remove(0);
175
176 let (_, sub_cmd) = match map.find_command(&command) {
177 Some(cmd) => cmd,
178 None => {
179 let err_msg = format!("no such command '{}'", command);
180 print_nested_usage_error(prefix, map, &err_msg);
181 return Err(format_err!("{}", err_msg));
182 }
183 };
184
185 *prefix = format!("{} {}", prefix, command);
186
187 match sub_cmd {
188 CommandLineInterface::Simple(cli_cmd) => {
189 //return handle_simple_command(&prefix, cli_cmd, args).await;
190 return Ok(cli_cmd);
191 }
192 CommandLineInterface::Nested(new_map) => map = new_map,
193 }
194 }
195 }
196
197 const API_METHOD_COMMAND_HELP: ApiMethod = ApiMethod::new(
198 &ApiHandler::Sync(&help_command),
199 &ObjectSchema::new(
200 "Get help about specified command (or sub-command).",
201 &[
202 (
203 "command",
204 true,
205 &ArraySchema::new(
206 "Command. This may be a list in order to spefify nested sub-commands.",
207 &StringSchema::new("Name.").schema(),
208 )
209 .schema(),
210 ),
211 (
212 "verbose",
213 true,
214 &BooleanSchema::new("Verbose help.").schema(),
215 ),
216 ],
217 ),
218 );
219
220 std::thread_local! {
221 static HELP_CONTEXT: RefCell<Option<Arc<CommandLineInterface>>> = RefCell::new(None);
222 }
223
224 fn help_command(
225 param: Value,
226 _info: &ApiMethod,
227 _rpcenv: &mut dyn RpcEnvironment,
228 ) -> Result<Value, Error> {
229 let mut command: Vec<String> = param["command"]
230 .as_array()
231 .unwrap_or(&Vec::new())
232 .iter()
233 .map(|v| v.as_str().unwrap().to_string())
234 .collect();
235
236 let verbose = param["verbose"].as_bool();
237
238 HELP_CONTEXT.with(|ctx| match &*ctx.borrow() {
239 Some(def) => {
240 if let CommandLineInterface::Nested(map) = def.as_ref() {
241 replace_aliases(&mut command, &map.aliases);
242 }
243 print_help(def, String::from(""), &command, verbose);
244 }
245 None => {
246 eprintln!("Sorry, help context not set - internal error.");
247 }
248 });
249
250 Ok(Value::Null)
251 }
252
253 fn set_help_context(def: Option<Arc<CommandLineInterface>>) {
254 HELP_CONTEXT.with(|ctx| {
255 *ctx.borrow_mut() = def;
256 });
257 }
258
259 pub(crate) fn help_command_def() -> CliCommand {
260 CliCommand::new(&API_METHOD_COMMAND_HELP).arg_param(&["command"])
261 }
262
263 fn replace_aliases(args: &mut Vec<String>, aliases: &[(Vec<&'static str>, Vec<&'static str>)]) {
264 for (old, new) in aliases {
265 if args.len() < old.len() {
266 continue;
267 }
268 if old[..] == args[..old.len()] {
269 let new_args: Vec<String> = new.iter().map(|s| String::from(*s)).collect();
270 let rest = args.split_off(old.len());
271 args.truncate(0);
272 args.extend(new_args);
273 for arg in rest.iter() {
274 args.push(arg.clone());
275 }
276 return;
277 }
278 }
279 }
280
281 /// Handle command invocation.
282 ///
283 /// This command gets the command line ``args`` and tries to invoke
284 /// the corresponding API handler.
285 pub async fn handle_command_future(
286 def: Arc<CommandLineInterface>,
287 prefix: &str,
288 mut args: Vec<String>,
289 rpcenv: CliEnvironment,
290 ) -> Result<(), Error> {
291 set_help_context(Some(def.clone()));
292
293 let result = match &*def {
294 CommandLineInterface::Simple(ref cli_cmd) => {
295 handle_simple_command_future(prefix, cli_cmd, args, rpcenv).await
296 }
297 CommandLineInterface::Nested(ref map) => {
298 let mut prefix = prefix.to_string();
299 let cli_cmd = parse_nested_command(&mut prefix, map, &mut args)?;
300 handle_simple_command_future(&prefix, cli_cmd, args, rpcenv).await
301 }
302 };
303
304 set_help_context(None);
305
306 result
307 }
308
309 /// Handle command invocation.
310 ///
311 /// This command gets the command line ``args`` and tries to invoke
312 /// the corresponding API handler.
313 pub fn handle_command(
314 def: Arc<CommandLineInterface>,
315 prefix: &str,
316 mut args: Vec<String>,
317 rpcenv: CliEnvironment,
318 run: Option<fn(ApiFuture) -> Result<Value, Error>>,
319 ) -> Result<(), Error> {
320 set_help_context(Some(def.clone()));
321
322 let result = match &*def {
323 CommandLineInterface::Simple(ref cli_cmd) => {
324 handle_simple_command(prefix, cli_cmd, args, rpcenv, run)
325 }
326 CommandLineInterface::Nested(ref map) => {
327 let mut prefix = prefix.to_string();
328 let cli_cmd = parse_nested_command(&mut prefix, map, &mut args)?;
329 handle_simple_command(&prefix, cli_cmd, args, rpcenv, run)
330 }
331 };
332
333 set_help_context(None);
334
335 result
336 }
337
338 fn prepare_cli_command(def: &CommandLineInterface) -> (String, Vec<String>) {
339 let mut args = std::env::args();
340
341 let prefix = args.next().unwrap();
342 let prefix = prefix.rsplit('/').next().unwrap().to_string(); // without path
343
344 let args: Vec<String> = args.collect();
345
346 if !args.is_empty() {
347 if args[0] == "bashcomplete" {
348 def.print_bash_completion();
349 std::process::exit(0);
350 }
351
352 if args[0] == "printdoc" {
353 let usage = match def {
354 CommandLineInterface::Simple(cli_cmd) => {
355 generate_usage_str(&prefix, cli_cmd, DocumentationFormat::ReST, "", &[])
356 }
357 CommandLineInterface::Nested(map) => {
358 generate_nested_usage(&prefix, map, DocumentationFormat::ReST)
359 }
360 };
361 println!("{}", usage);
362 std::process::exit(0);
363 }
364 }
365
366 (prefix, args)
367 }
368
369 /// Helper to get arguments and invoke the command (async).
370 ///
371 /// This helper reads arguments with ``std::env::args()``. The first
372 /// argument is assumed to be the program name, and is passed as ``prefix`` to
373 /// ``handle_command()``.
374 ///
375 /// This helper automatically add the help command, and two special
376 /// sub-command:
377 ///
378 /// - ``bashcomplete``: Output bash completions instead of running the command.
379 /// - ``printdoc``: Output ReST documentation.
380 ///
381 pub async fn run_async_cli_command<C: Into<CommandLineInterface>>(def: C, rpcenv: CliEnvironment) {
382 let def = match def.into() {
383 CommandLineInterface::Simple(cli_cmd) => CommandLineInterface::Simple(cli_cmd),
384 CommandLineInterface::Nested(map) => CommandLineInterface::Nested(map.insert_help()),
385 };
386
387 let (prefix, args) = prepare_cli_command(&def);
388
389 if handle_command_future(Arc::new(def), &prefix, args, rpcenv)
390 .await
391 .is_err()
392 {
393 std::process::exit(-1);
394 }
395 }
396
397 /// Helper to get arguments and invoke the command.
398 ///
399 /// This is the synchrounous version of run_async_cli_command. You can
400 /// pass an optional ``run`` function to execute async commands (else
401 /// async commands simply fail).
402 pub fn run_cli_command<C: Into<CommandLineInterface>>(
403 def: C,
404 rpcenv: CliEnvironment,
405 run: Option<fn(ApiFuture) -> Result<Value, Error>>,
406 ) {
407 let def = match def.into() {
408 CommandLineInterface::Simple(cli_cmd) => CommandLineInterface::Simple(cli_cmd),
409 CommandLineInterface::Nested(map) => CommandLineInterface::Nested(map.insert_help()),
410 };
411
412 let (prefix, args) = prepare_cli_command(&def);
413
414 if handle_command(Arc::new(def), &prefix, args, rpcenv, run).is_err() {
415 std::process::exit(-1);
416 }
417 }