4 use std
::collections
::{HashMap, HashSet}
;
6 use crate::api_schema
::*;
7 use crate::api_schema
::router
::*;
8 use crate::api_schema
::format
::*;
9 //use crate::api_schema::config::*;
10 use super::environment
::CliEnvironment
;
14 pub const OUTPUT_FORMAT
: Schema
=
15 StringSchema
::new("Output format.")
16 .format(&ApiStringFormat
::Enum(&["text", "json", "json-pretty"]))
19 /// Helper function to format and print result
21 /// This is implemented for machine generatable formats 'json' and
22 /// 'json-pretty'. The 'text' format needs to be handled somewhere
24 pub fn format_and_print_result(result
: &Value
, output_format
: &str) {
26 if output_format
== "json-pretty" {
27 println
!("{}", serde_json
::to_string_pretty(&result
).unwrap());
28 } else if output_format
== "json" {
29 println
!("{}", serde_json
::to_string(&result
).unwrap());
35 fn generate_usage_str(
38 format
: DocumentationFormat
,
39 indent
: &str) -> String
{
41 let arg_param
= &cli_cmd
.arg_param
;
42 let fixed_param
= &cli_cmd
.fixed_param
;
43 let schema
= cli_cmd
.info
.parameters
;
45 let mut done_hash
= HashSet
::<&str>::new();
46 let mut args
= String
::new();
48 for positional_arg
in arg_param
{
49 match schema
.lookup(positional_arg
) {
50 Some((optional
, param_schema
)) => {
53 let is_array
= if let Schema
::Array(_
) = param_schema { true }
else { false }
;
54 if optional { args.push('['); }
55 if is_array { args.push('{'); }
56 args
.push('
<'
); args
.push_str(positional_arg
); args
.push('
>'
);
57 if is_array { args.push('}'
); }
58 if optional { args.push(']'); }
60 done_hash
.insert(positional_arg
);
62 None
=> panic
!("no such property '{}' in schema", positional_arg
),
66 let mut arg_descr
= String
::new();
67 for positional_arg
in arg_param
{
68 let (_optional
, param_schema
) = schema
.lookup(positional_arg
).unwrap();
69 let param_descr
= get_property_description(
70 positional_arg
, param_schema
, ParameterDisplayStyle
::Fixed
, format
);
71 arg_descr
.push_str(¶m_descr
);
74 let mut options
= String
::new();
76 for (prop
, optional
, param_schema
) in schema
.properties
{
77 if done_hash
.contains(prop
) { continue; }
78 if fixed_param
.contains_key(prop
) { continue; }
80 let type_text
= get_schema_type_text(param_schema
, ParameterDisplayStyle
::Arg
);
84 if options
.len() > 0 { options.push('\n'); }
85 options
.push_str(&get_property_description(prop
, param_schema
, ParameterDisplayStyle
::Arg
, format
));
88 args
.push_str(" --"); args
.push_str(prop
);
90 args
.push_str(&type_text
);
93 done_hash
.insert(prop
);
96 let option_indicator
= if options
.len() > 0 { " [OPTIONS]" }
else { "" }
;
98 let mut text
= match format
{
99 DocumentationFormat
::Short
=> {
100 return format
!("{}{}{}{}\n\n", indent
, prefix
, args
, option_indicator
);
102 DocumentationFormat
::Long
=> {
103 format
!("{}{}{}{}\n\n", indent
, prefix
, args
, option_indicator
)
105 DocumentationFormat
::Full
=> {
106 format
!("{}{}{}{}\n\n{}\n\n", indent
, prefix
, args
, option_indicator
, schema
.description
)
108 DocumentationFormat
::ReST
=> {
109 format
!("``{}{}{}``\n\n{}\n\n", prefix
, args
, option_indicator
, schema
.description
)
113 if arg_descr
.len() > 0 {
114 text
.push_str(&arg_descr
);
117 if options
.len() > 0 {
118 text
.push_str(&options
);
124 fn print_simple_usage_error(prefix
: &str, cli_cmd
: &CliCommand
, err
: Error
) {
126 let usage
= generate_usage_str(prefix
, cli_cmd
, DocumentationFormat
::Long
, "");
127 eprint
!("Error: {}\nUsage: {}", err
, usage
);
131 top_def
: &CommandLineInterface
,
134 verbose
: Option
<bool
>,
136 let mut iface
= top_def
;
139 if let CommandLineInterface
::Nested(map
) = iface
{
140 if let Some(subcmd
) = find_command(map
, cmd
) {
143 prefix
.push_str(cmd
);
147 eprintln
!("no such command '{}'", cmd
);
148 std
::process
::exit(-1);
151 let format
= match verbose
.unwrap_or(false) {
152 true => DocumentationFormat
::Full
,
153 false => DocumentationFormat
::Short
,
157 CommandLineInterface
::Nested(map
) => {
158 println
!("Usage:\n\n{}", generate_nested_usage(&prefix
, map
, format
));
160 CommandLineInterface
::Simple(cli_cmd
) => {
161 println
!("Usage: {}", generate_usage_str(&prefix
, cli_cmd
, format
, ""));
166 fn handle_simple_command(
167 _top_def
: &CommandLineInterface
,
169 cli_cmd
: &CliCommand
,
173 let (params
, rest
) = match getopts
::parse_arguments(
174 &args
, &cli_cmd
.arg_param
, &cli_cmd
.info
.parameters
) {
175 Ok((p
, r
)) => (p
, r
),
177 print_simple_usage_error(prefix
, cli_cmd
, err
.into());
178 std
::process
::exit(-1);
182 if !rest
.is_empty() {
183 let err
= format_err
!("got additional arguments: {:?}", rest
);
184 print_simple_usage_error(prefix
, cli_cmd
, err
);
185 std
::process
::exit(-1);
188 let mut rpcenv
= CliEnvironment
::new();
190 match cli_cmd
.info
.handler
{
191 ApiHandler
::Sync(handler
) => {
192 match (handler
)(params
, &cli_cmd
.info
, &mut rpcenv
) {
194 if value
!= Value
::Null
{
195 println
!("Result: {}", serde_json
::to_string_pretty(&value
).unwrap());
199 eprintln
!("Error: {}", err
);
200 std
::process
::exit(-1);
204 ApiHandler
::Async(_
) => {
211 fn find_command
<'a
>(def
: &'a CliCommandMap
, name
: &str) -> Option
<&'a CommandLineInterface
> {
213 if let Some(sub_cmd
) = def
.commands
.get(name
) {
214 return Some(sub_cmd
);
217 let mut matches
: Vec
<&str> = vec
![];
219 for cmd
in def
.commands
.keys() {
220 if cmd
.starts_with(name
) {
224 if matches
.len() != 1 { return None; }
226 if let Some(sub_cmd
) = def
.commands
.get(matches
[0]) {
227 return Some(sub_cmd
);
233 fn print_nested_usage_error(prefix
: &str, def
: &CliCommandMap
, err
: Error
) {
235 let usage
= generate_nested_usage(prefix
, def
, DocumentationFormat
::Short
);
237 eprintln
!("Error: {}\n\nUsage:\n\n{}", err
, usage
);
240 fn generate_nested_usage(prefix
: &str, def
: &CliCommandMap
, format
: DocumentationFormat
) -> String
{
242 let mut cmds
: Vec
<&String
> = def
.commands
.keys().collect();
245 let mut usage
= String
::new();
248 let new_prefix
= format
!("{} {}", prefix
, cmd
);
250 match def
.commands
.get(cmd
).unwrap() {
251 CommandLineInterface
::Simple(cli_cmd
) => {
252 if usage
.len() > 0 && format
== DocumentationFormat
::ReST
{
253 usage
.push_str("----\n\n");
255 usage
.push_str(&generate_usage_str(&new_prefix
, cli_cmd
, format
, ""));
257 CommandLineInterface
::Nested(map
) => {
258 usage
.push_str(&generate_nested_usage(&new_prefix
, map
, format
));
266 fn handle_nested_command(
267 top_def
: &CommandLineInterface
,
270 mut args
: Vec
<String
>,
274 let mut cmds
: Vec
<&String
> = def
.commands
.keys().collect();
277 let list
= cmds
.iter().fold(String
::new(),|mut s
,item
| {
278 if !s
.is_empty() { s+= ", "; }
283 let err
= format_err
!("no command specified.\nPossible commands: {}", list
);
284 print_nested_usage_error(prefix
, def
, err
);
285 std
::process
::exit(-1);
288 let command
= args
.remove(0);
290 let sub_cmd
= match find_command(def
, &command
) {
293 let err
= format_err
!("no such command '{}'", command
);
294 print_nested_usage_error(prefix
, def
, err
);
295 std
::process
::exit(-1);
299 let new_prefix
= format
!("{} {}", prefix
, command
);
302 CommandLineInterface
::Simple(cli_cmd
) => {
303 handle_simple_command(top_def
, &new_prefix
, cli_cmd
, args
);
305 CommandLineInterface
::Nested(map
) => {
306 handle_nested_command(top_def
, &new_prefix
, map
, args
);
311 fn print_property_completion(
314 completion_functions
: &HashMap
<String
, CompletionFunction
>,
316 param
: &HashMap
<String
, String
>,
318 if let Some(callback
) = completion_functions
.get(name
) {
319 let list
= (callback
)(arg
, param
);
321 if value
.starts_with(arg
) {
322 println
!("{}", value
);
328 if let Schema
::String(StringSchema { format: Some(format), ..}
) = schema
{
329 if let ApiStringFormat
::Enum(list
) = format
{
330 for value
in list
.iter() {
331 if value
.starts_with(arg
) {
332 println
!("{}", value
);
341 fn record_done_argument(done
: &mut HashMap
<String
, String
>, parameters
: &ObjectSchema
, key
: &str, value
: &str) {
343 if let Some((_
, schema
)) = parameters
.lookup(key
) {
345 Schema
::Array(_
) => { /* do nothing ?? */ }
346 _
=> { done.insert(key.to_owned(), value.to_owned()); }
351 fn print_simple_completion(
352 cli_cmd
: &CliCommand
,
353 done
: &mut HashMap
<String
, String
>,
354 all_arg_param
: &[&str], // this is always the full list
355 arg_param
: &[&str], // we remove done arguments
358 // fixme: arg_param, fixed_param
359 //eprintln!("COMPL: {:?} {:?} {}", arg_param, args, args.len());
361 if !arg_param
.is_empty() {
362 let prop_name
= arg_param
[0];
364 record_done_argument(done
, cli_cmd
.info
.parameters
, prop_name
, &args
[0]);
365 print_simple_completion(cli_cmd
, done
, arg_param
, &arg_param
[1..], &args
[1..]);
367 } else if args
.len() == 1 {
368 record_done_argument(done
, cli_cmd
.info
.parameters
, prop_name
, &args
[0]);
369 if let Some((_
, schema
)) = cli_cmd
.info
.parameters
.lookup(prop_name
) {
370 print_property_completion(schema
, prop_name
, &cli_cmd
.completion_functions
, &args
[0], done
);
375 if args
.is_empty() { return; }
377 // Try to parse all argumnets but last, record args already done
379 let mut errors
= ParameterError
::new(); // we simply ignore any parsing errors here
380 let (data
, _rest
) = getopts
::parse_argument_list(&args
[0..args
.len()-1], &cli_cmd
.info
.parameters
, &mut errors
);
381 for (key
, value
) in &data
{
382 record_done_argument(done
, &cli_cmd
.info
.parameters
, key
, value
);
386 let prefix
= &args
[args
.len()-1]; // match on last arg
388 // complete option-name or option-value ?
389 if !prefix
.starts_with("-") && args
.len() > 1 {
390 let last
= &args
[args
.len()-2];
391 if last
.starts_with("--") && last
.len() > 2 {
392 let prop_name
= &last
[2..];
393 if let Some((_
, schema
)) = cli_cmd
.info
.parameters
.lookup(prop_name
) {
394 print_property_completion(schema
, prop_name
, &cli_cmd
.completion_functions
, &prefix
, done
);
400 for (name
, _optional
, _schema
) in cli_cmd
.info
.parameters
.properties
{
401 if done
.contains_key(*name
) { continue; }
402 if all_arg_param
.contains(name
) { continue; }
403 let option
= String
::from("--") + name
;
404 if option
.starts_with(prefix
) {
405 println
!("{}", option
);
410 fn print_help_completion(def
: &CommandLineInterface
, help_cmd
: &CliCommand
, args
: &[String
]) {
412 let mut done
= HashMap
::new();
415 CommandLineInterface
::Simple(_
) => {
416 print_simple_completion(help_cmd
, &mut done
, &help_cmd
.arg_param
, &help_cmd
.arg_param
, args
);
418 CommandLineInterface
::Nested(map
) => {
420 for cmd
in map
.commands
.keys() {
426 let first
= &args
[0];
428 if first
.starts_with("-") {
429 print_simple_completion(help_cmd
, &mut done
, &help_cmd
.arg_param
, &help_cmd
.arg_param
, args
);
433 if let Some(sub_cmd
) = map
.commands
.get(first
) {
434 print_help_completion(sub_cmd
, help_cmd
, &args
[1..]);
438 for cmd
in map
.commands
.keys() {
439 if cmd
.starts_with(first
) {
447 fn print_nested_completion(def
: &CommandLineInterface
, args
: &[String
]) {
450 CommandLineInterface
::Simple(cli_cmd
) => {
451 let mut done
: HashMap
<String
, String
> = HashMap
::new();
452 cli_cmd
.fixed_param
.iter().for_each(|(key
, value
)| {
453 record_done_argument(&mut done
, &cli_cmd
.info
.parameters
, &key
, &value
);
455 print_simple_completion(cli_cmd
, &mut done
, &cli_cmd
.arg_param
, &cli_cmd
.arg_param
, args
);
457 CommandLineInterface
::Nested(map
) => {
459 for cmd
in map
.commands
.keys() {
464 let first
= &args
[0];
466 if let Some(sub_cmd
) = map
.commands
.get(first
) {
467 print_nested_completion(sub_cmd
, &args
[1..]);
471 for cmd
in map
.commands
.keys() {
472 if cmd
.starts_with(first
) {
480 pub fn print_bash_completion(def
: &CommandLineInterface
) {
482 let comp_point
: usize = match std
::env
::var("COMP_POINT") {
484 match usize::from_str_radix(&val
, 10) {
492 let cmdline
= match std
::env
::var("COMP_LINE") {
493 Ok(val
) => val
[0..comp_point
].to_owned(),
497 let mut args
= match shellwords
::split(&cmdline
) {
502 if args
.len() == 0 { return; }
504 args
.remove(0); //no need for program name
506 if cmdline
.ends_with(char::is_whitespace
) {
507 //eprintln!("CMDLINE {:?}", cmdline);
508 args
.push("".into());
511 if !args
.is_empty() && args
[0] == "help" {
512 print_help_completion(def
, &help_command_def(), &args
[1..]);
514 print_nested_completion(def
, &args
);
518 const VERBOSE_HELP_SCHEMA
: Schema
= BooleanSchema
::new("Verbose help.").schema();
519 const COMMAND_HELP
: ObjectSchema
= ObjectSchema
::new(
520 "Get help about specified command.",
521 &[ ("verbose", true, &VERBOSE_HELP_SCHEMA
) ]
524 const API_METHOD_COMMAND_HELP
: ApiMethod
= ApiMethod
::new_dummy(&COMMAND_HELP
);
526 fn help_command_def() -> CliCommand
{
527 CliCommand
::new(&API_METHOD_COMMAND_HELP
)
530 pub fn run_cli_command(def
: CommandLineInterface
) {
532 let def
= match def
{
533 CommandLineInterface
::Simple(cli_cmd
) => CommandLineInterface
::Simple(cli_cmd
),
534 CommandLineInterface
::Nested(map
) =>
535 CommandLineInterface
::Nested(map
.insert("help", help_command_def().into())),
538 let top_def
= &def
; // we pass this to the help function ...
540 let mut args
= std
::env
::args();
542 let prefix
= args
.next().unwrap();
543 let prefix
= prefix
.rsplit('
/'
).next().unwrap(); // without path
545 let args
: Vec
<String
> = args
.collect();
547 if !args
.is_empty() {
548 if args
[0] == "bashcomplete" {
549 print_bash_completion(&def
);
553 if args
[0] == "printdoc" {
554 let usage
= match def
{
555 CommandLineInterface
::Simple(cli_cmd
) => {
556 generate_usage_str(&prefix
, &cli_cmd
, DocumentationFormat
::ReST
, "")
558 CommandLineInterface
::Nested(map
) => {
559 generate_nested_usage(&prefix
, &map
, DocumentationFormat
::ReST
)
562 println
!("{}", usage
);
568 CommandLineInterface
::Simple(ref cli_cmd
) => handle_simple_command(top_def
, &prefix
, &cli_cmd
, args
),
569 CommandLineInterface
::Nested(ref map
) => handle_nested_command(top_def
, &prefix
, &map
, args
),
573 pub type CompletionFunction
= fn(&str, &HashMap
<String
, String
>) -> Vec
<String
>;
575 pub struct CliCommand
{
576 pub info
: &'
static ApiMethod
,
577 pub arg_param
: Vec
<&'
static str>,
578 pub fixed_param
: HashMap
<&'
static str, String
>,
579 pub completion_functions
: HashMap
<String
, CompletionFunction
>,
584 pub fn new(info
: &'
static ApiMethod
) -> Self {
586 info
, arg_param
: vec
![],
587 fixed_param
: HashMap
::new(),
588 completion_functions
: HashMap
::new(),
592 pub fn arg_param(mut self, names
: Vec
<&'
static str>) -> Self {
593 self.arg_param
= names
;
597 pub fn fixed_param(mut self, key
: &'
static str, value
: String
) -> Self {
598 self.fixed_param
.insert(key
, value
);
602 pub fn completion_cb(mut self, param_name
: &str, cb
: CompletionFunction
) -> Self {
603 self.completion_functions
.insert(param_name
.into(), cb
);
608 pub struct CliCommandMap
{
609 pub commands
: HashMap
<String
, CommandLineInterface
>,
614 pub fn new() -> Self {
615 Self { commands: HashMap:: new() }
618 pub fn insert
<S
: Into
<String
>>(mut self, name
: S
, cli
: CommandLineInterface
) -> Self {
619 self.commands
.insert(name
.into(), cli
);
624 pub enum CommandLineInterface
{
626 Nested(CliCommandMap
),
629 impl From
<CliCommand
> for CommandLineInterface
{
630 fn from(cli_cmd
: CliCommand
) -> Self {
631 CommandLineInterface
::Simple(cli_cmd
)
635 impl From
<CliCommandMap
> for CommandLineInterface
{
636 fn from(list
: CliCommandMap
) -> Self {
637 CommandLineInterface
::Nested(list
)