1 use crate::{Action, Command, Context, Flag, FlagType, Help}; 2 3 /// Multiple action application entry point 4 #[derive(Default)] 5 pub struct App { 6 /// Application name 7 pub name: String, 8 /// Application author 9 pub author: Option<String>, 10 /// Application description 11 pub description: Option<String>, 12 /// Application usage 13 pub usage: Option<String>, 14 /// Application version 15 pub version: Option<String>, 16 /// Application commands 17 pub commands: Option<Vec<Command>>, 18 /// Application action 19 pub action: Option<Action>, 20 /// Application flags 21 pub flags: Option<Vec<Flag>>, 22 } 23 24 impl App { 25 /// Create new instance of `App` 26 /// 27 /// Example 28 /// 29 /// ``` 30 /// use seahorse::App; 31 /// 32 /// let app = App::new("cli"); 33 /// ``` new<T: Into<String>>(name: T) -> Self34 pub fn new<T: Into<String>>(name: T) -> Self { 35 Self { 36 name: name.into(), 37 ..Self::default() 38 } 39 } 40 41 /// Set author of the app 42 /// 43 /// Example 44 /// 45 /// ``` 46 /// use seahorse::App; 47 /// 48 /// let app = App::new("cli") 49 /// .author(env!("CARGO_PKG_AUTHORS")); 50 /// ``` author<T: Into<String>>(mut self, author: T) -> Self51 pub fn author<T: Into<String>>(mut self, author: T) -> Self { 52 self.author = Some(author.into()); 53 self 54 } 55 56 /// Set description of the app 57 /// 58 /// Example 59 /// 60 /// ``` 61 /// use seahorse::App; 62 /// 63 /// let app = App::new("cli") 64 /// .description(env!("CARGO_PKG_DESCRIPTION")); 65 /// ``` description<T: Into<String>>(mut self, description: T) -> Self66 pub fn description<T: Into<String>>(mut self, description: T) -> Self { 67 self.description = Some(description.into()); 68 self 69 } 70 71 /// Set usage of the app 72 /// 73 /// Example 74 /// 75 /// ``` 76 /// use seahorse::App; 77 /// 78 /// let app = App::new("cli"); 79 /// app.usage("cli [command] [arg]"); 80 /// ``` usage<T: Into<String>>(mut self, usage: T) -> Self81 pub fn usage<T: Into<String>>(mut self, usage: T) -> Self { 82 self.usage = Some(usage.into()); 83 self 84 } 85 86 /// Set version of the app 87 /// 88 /// Example 89 /// 90 /// ``` 91 /// use seahorse::App; 92 /// 93 /// let app = App::new("cli"); 94 /// app.version(env!("CARGO_PKG_VERSION")); 95 /// ``` version<T: Into<String>>(mut self, version: T) -> Self96 pub fn version<T: Into<String>>(mut self, version: T) -> Self { 97 self.version = Some(version.into()); 98 self 99 } 100 101 /// Set command of the app 102 /// 103 /// Example 104 /// 105 /// ``` 106 /// use seahorse::{App, Command}; 107 /// 108 /// let command = Command::new("hello") 109 /// .usage("cli hello [arg]") 110 /// .action(|c| println!("{:?}", c.args)); 111 /// 112 /// let app = App::new("cli") 113 /// .command(command); 114 /// ``` 115 /// 116 /// # Panics 117 /// 118 /// You cannot set a command named as same as registered ones. 119 /// 120 /// ```should_panic 121 /// use seahorse::{App, Command}; 122 /// 123 /// let command1 = Command::new("hello") 124 /// .usage("cli hello [arg]") 125 /// .action(|c| println!("{:?}", c.args)); 126 /// 127 /// let command2 = Command::new("hello") 128 /// .usage("cli hello [arg]") 129 /// .action(|c| println!("{:?}", c.args)); 130 /// 131 /// let app = App::new("cli") 132 /// .command(command1) 133 /// .command(command2); 134 /// ``` command(mut self, command: Command) -> Self135 pub fn command(mut self, command: Command) -> Self { 136 if let Some(ref mut commands) = self.commands { 137 if commands 138 .iter() 139 .any(|registered| registered.name == command.name) 140 { 141 panic!(format!( 142 r#"Command name "{}" is already registered."#, 143 command.name 144 )); 145 } 146 (*commands).push(command); 147 } else { 148 self.commands = Some(vec![command]); 149 } 150 self 151 } 152 153 /// Set action of the app 154 /// 155 /// Example 156 /// 157 /// ``` 158 /// use seahorse::{Action, App, Context}; 159 /// 160 /// let action: Action = |c: &Context| println!("{:?}", c.args); 161 /// let app = App::new("cli") 162 /// .action(action); 163 /// ``` action(mut self, action: Action) -> Self164 pub fn action(mut self, action: Action) -> Self { 165 self.action = Some(action); 166 self 167 } 168 169 /// Set flag of the app 170 /// 171 /// Example 172 /// 173 /// ``` 174 /// use seahorse::{App, Flag, FlagType}; 175 /// 176 /// let app = App::new("cli") 177 /// .flag(Flag::new("bool", FlagType::Bool)) 178 /// .flag(Flag::new("int", FlagType::Int)); 179 /// ``` flag(mut self, flag: Flag) -> Self180 pub fn flag(mut self, flag: Flag) -> Self { 181 if let Some(ref mut flags) = self.flags { 182 (*flags).push(flag); 183 } else { 184 self.flags = Some(vec![flag]); 185 } 186 self 187 } 188 189 /// Run app 190 /// 191 /// Example 192 /// 193 /// ``` 194 /// use std::env; 195 /// use seahorse::App; 196 /// 197 /// let args: Vec<String> = env::args().collect(); 198 /// let app = App::new("cli"); 199 /// app.run(args); 200 /// ``` run(&self, args: Vec<String>)201 pub fn run(&self, args: Vec<String>) { 202 let args = Self::normalized_args(args); 203 let (cmd_v, args_v) = match args.len() { 204 1 => args.split_at(1), 205 _ => args[1..].split_at(1), 206 }; 207 208 let cmd = match cmd_v.first() { 209 Some(c) => c, 210 None => { 211 self.help(); 212 return; 213 } 214 }; 215 216 match self.select_command(&cmd) { 217 Some(command) => command.run(args_v.to_vec()), 218 None => match self.action { 219 Some(action) => { 220 if args.contains(&"-h".to_string()) || args.contains(&"--help".to_string()) { 221 self.help(); 222 return; 223 } 224 action(&Context::new( 225 args[1..].to_vec(), 226 self.flags.clone(), 227 self.help_text(), 228 )); 229 } 230 None => self.help(), 231 }, 232 } 233 } 234 235 /// Select command 236 /// Gets the Command that matches the string passed in the argument select_command(&self, cmd: &str) -> Option<&Command>237 fn select_command(&self, cmd: &str) -> Option<&Command> { 238 match &self.commands { 239 Some(commands) => commands.iter().find(|command| match &command.alias { 240 Some(alias) => command.name == cmd || alias.iter().any(|a| a == cmd), 241 None => command.name == cmd, 242 }), 243 None => None, 244 } 245 } 246 247 /// Split arg with "=" to unify arg notations. 248 /// --flag=value => ["--flag", "value"] 249 /// --flag value => ["--flag", "value"] normalized_args(raw_args: Vec<String>) -> Vec<String>250 fn normalized_args(raw_args: Vec<String>) -> Vec<String> { 251 raw_args.iter().fold(Vec::<String>::new(), |mut acc, cur| { 252 if cur.starts_with('-') && cur.contains('=') { 253 let mut splitted_flag: Vec<String> = 254 cur.splitn(2, '=').map(|s| s.to_owned()).collect(); 255 acc.append(&mut splitted_flag); 256 } else { 257 acc.push(cur.to_owned()); 258 } 259 acc 260 }) 261 } 262 flag_help_text(&self) -> String263 fn flag_help_text(&self) -> String { 264 let mut text = String::new(); 265 text += "Flags:\n"; 266 let help_flag = "-h, --help"; 267 268 if let Some(flags) = &self.flags { 269 let int_val = "<int>"; 270 let float_val = "<float>"; 271 let string_val = "<string>"; 272 273 let flag_helps = &flags.iter().map(|f| { 274 let alias = match &f.alias { 275 Some(alias) => alias 276 .iter() 277 .map(|a| format!("-{}", a)) 278 .collect::<Vec<String>>() 279 .join(", "), 280 None => String::new(), 281 }; 282 let val = match f.flag_type { 283 FlagType::Int => int_val, 284 FlagType::Float => float_val, 285 FlagType::String => string_val, 286 _ => "", 287 }; 288 289 let help = if alias.is_empty() { 290 format!("--{} {}", f.name, val) 291 } else { 292 format!("{}, --{} {}", alias, f.name, val) 293 }; 294 295 (help, f.description.clone()) 296 }); 297 298 let flag_name_max_len = flag_helps 299 .clone() 300 .map(|h| h.0.len()) 301 .chain(vec![help_flag.len()].into_iter()) 302 .max() 303 .unwrap(); 304 305 for flag_help in flag_helps.clone().into_iter() { 306 text += &format!("\t{}", flag_help.0); 307 308 if let Some(usage) = &flag_help.1 { 309 let flag_name_len = flag_help.0.len(); 310 text += &format!( 311 "{} : {}\n", 312 " ".repeat(flag_name_max_len - flag_name_len), 313 usage 314 ); 315 } 316 } 317 318 text += &format!( 319 "\t{}{} : Show help\n", 320 help_flag, 321 " ".repeat(flag_name_max_len - help_flag.len()) 322 ); 323 } else { 324 text += &format!("\t{} : Show help\n", help_flag); 325 } 326 327 text 328 } 329 command_help_text(&self) -> String330 fn command_help_text(&self) -> String { 331 let mut text = String::new(); 332 333 if let Some(commands) = &self.commands { 334 text += "\nCommands:\n"; 335 336 let name_max_len = &commands 337 .iter() 338 .map(|c| { 339 if let Some(alias) = &c.alias { 340 format!("{}, {}", alias.join(", "), c.name).len() 341 } else { 342 c.name.len() 343 } 344 }) 345 .max() 346 .unwrap(); 347 348 for c in commands { 349 let command_name = if let Some(alias) = &c.alias { 350 format!("{}, {}", alias.join(", "), c.name) 351 } else { 352 c.name.clone() 353 }; 354 355 let description = match &c.description { 356 Some(description) => description, 357 None => "", 358 }; 359 360 text += &format!( 361 "\t{} {}: {}\n", 362 command_name, 363 " ".repeat(name_max_len - command_name.len()), 364 description 365 ); 366 } 367 368 text += "\n" 369 } 370 371 text 372 } 373 } 374 375 impl Help for App { help_text(&self) -> String376 fn help_text(&self) -> String { 377 let mut text = String::new(); 378 379 text += &format!("Name\n\t{}\n\n", self.name); 380 381 if let Some(author) = &self.author { 382 text += &format!("Author:\n\t{}\n\n", author); 383 } 384 385 if let Some(description) = &self.description { 386 text += &format!("Description:\n\t{}\n\n", description); 387 } 388 389 if let Some(usage) = &self.usage { 390 text += &format!("Usage:\n\t{}\n\n", usage); 391 } 392 393 text += &self.flag_help_text(); 394 text += &self.command_help_text(); 395 396 if let Some(version) = &self.version { 397 text += &format!("Version:\n\t{}\n", version); 398 } 399 400 text 401 } 402 } 403 404 #[cfg(test)] 405 mod tests { 406 use crate::{Action, App, Command, Context, Flag, FlagType}; 407 408 #[test] app_new_only_test()409 fn app_new_only_test() { 410 let app = App::new("cli"); 411 app.run(vec!["cli".to_string()]); 412 413 assert_eq!(app.name, "cli".to_string()); 414 assert_eq!(app.usage, None); 415 assert_eq!(app.author, None); 416 assert_eq!(app.description, None); 417 assert_eq!(app.version, None); 418 } 419 420 #[test] multiple_app_test()421 fn multiple_app_test() { 422 let a: Action = |c: &Context| { 423 assert_eq!(true, c.bool_flag("bool")); 424 match c.string_flag("string") { 425 Ok(flag) => assert_eq!("string".to_string(), flag), 426 _ => assert!(false, "string test false..."), 427 } 428 match c.int_flag("int") { 429 Ok(flag) => assert_eq!(100, flag), 430 _ => assert!(false, "int test false..."), 431 } 432 match c.float_flag("float") { 433 Ok(flag) => assert_eq!(1.23, flag), 434 _ => assert!(false, "float test false..."), 435 } 436 }; 437 let c = Command::new("hello") 438 .alias("h") 439 .description("hello command") 440 .usage("test hello(h) args") 441 .action(a) 442 .flag(Flag::new("bool", FlagType::Bool)) 443 .flag(Flag::new("string", FlagType::String)) 444 .flag(Flag::new("int", FlagType::Int)) 445 .flag(Flag::new("float", FlagType::Float)); 446 447 let app = App::new("test") 448 .author("Author <author@example.com>") 449 .description("This is a great tool.") 450 .usage("test [command] [arg]") 451 .version("0.0.1") 452 .command(c); 453 454 app.run(vec![ 455 "test".to_string(), 456 "hello".to_string(), 457 "args".to_string(), 458 "--bool".to_string(), 459 "--string".to_string(), 460 "string".to_string(), 461 "--int".to_string(), 462 "100".to_string(), 463 "--float".to_string(), 464 "1.23".to_string(), 465 ]); 466 467 app.run(vec![ 468 "test".to_string(), 469 "h".to_string(), 470 "args".to_string(), 471 "--bool".to_string(), 472 "--string".to_string(), 473 "string".to_string(), 474 "--int".to_string(), 475 "100".to_string(), 476 "--float".to_string(), 477 "1.23".to_string(), 478 ]); 479 480 assert_eq!(app.name, "test".to_string()); 481 assert_eq!(app.usage, Some("test [command] [arg]".to_string())); 482 assert_eq!(app.author, Some("Author <author@example.com>".to_string())); 483 assert_eq!(app.description, Some("This is a great tool.".to_string())); 484 assert_eq!(app.version, Some("0.0.1".to_string())); 485 } 486 487 #[test] single_app_test()488 fn single_app_test() { 489 let action: Action = |c: &Context| { 490 assert_eq!(true, c.bool_flag("bool")); 491 match c.string_flag("string") { 492 Ok(flag) => assert_eq!("string".to_string(), flag), 493 _ => assert!(false, "string test false..."), 494 } 495 match c.int_flag("int") { 496 Ok(flag) => assert_eq!(100, flag), 497 _ => assert!(false, "int test false..."), 498 } 499 match c.float_flag("float") { 500 Ok(flag) => assert_eq!(1.23, flag), 501 _ => assert!(false, "float test false..."), 502 } 503 }; 504 505 let app = App::new("test") 506 .author("Author <author@example.com>") 507 .description("This is a great tool.") 508 .usage("test [arg]") 509 .version("0.0.1") 510 .action(action) 511 .flag(Flag::new("bool", FlagType::Bool)) 512 .flag(Flag::new("string", FlagType::String)) 513 .flag(Flag::new("int", FlagType::Int)) 514 .flag(Flag::new("float", FlagType::Float)); 515 516 app.run(vec![ 517 "test".to_string(), 518 "args".to_string(), 519 "--bool".to_string(), 520 "--string".to_string(), 521 "string".to_string(), 522 "--int".to_string(), 523 "100".to_string(), 524 "--float".to_string(), 525 "1.23".to_string(), 526 ]); 527 528 assert_eq!(app.name, "test".to_string()); 529 assert_eq!(app.usage, Some("test [arg]".to_string())); 530 assert_eq!(app.author, Some("Author <author@example.com>".to_string())); 531 assert_eq!(app.description, Some("This is a great tool.".to_string())); 532 assert_eq!(app.version, Some("0.0.1".to_string())); 533 } 534 535 #[test] flag_only_app_test()536 fn flag_only_app_test() { 537 let action: Action = |c: &Context| { 538 assert_eq!(true, c.bool_flag("bool")); 539 match c.string_flag("string") { 540 Ok(flag) => assert_eq!("string".to_string(), flag), 541 _ => assert!(false, "string test false..."), 542 } 543 match c.int_flag("int") { 544 Ok(flag) => assert_eq!(100, flag), 545 _ => assert!(false, "int test false..."), 546 } 547 match c.float_flag("float") { 548 Ok(flag) => assert_eq!(1.23, flag), 549 _ => assert!(false, "float test false..."), 550 } 551 }; 552 553 let app = App::new("test") 554 .author("Author <author@example.com>") 555 .description("This is a great tool.") 556 .usage("test") 557 .version("0.0.1") 558 .action(action) 559 .flag(Flag::new("bool", FlagType::Bool)) 560 .flag(Flag::new("string", FlagType::String)) 561 .flag(Flag::new("int", FlagType::Int)) 562 .flag(Flag::new("float", FlagType::Float)); 563 564 app.run(vec![ 565 "test".to_string(), 566 "--bool".to_string(), 567 "--string".to_string(), 568 "string".to_string(), 569 "--int".to_string(), 570 "100".to_string(), 571 "--float".to_string(), 572 "1.23".to_string(), 573 ]); 574 575 assert_eq!(app.name, "test".to_string()); 576 assert_eq!(app.usage, Some("test".to_string())); 577 assert_eq!(app.author, Some("Author <author@example.com>".to_string())); 578 assert_eq!(app.description, Some("This is a great tool.".to_string())); 579 assert_eq!(app.version, Some("0.0.1".to_string())); 580 } 581 582 #[test] single_app_equal_notation_test()583 fn single_app_equal_notation_test() { 584 let action: Action = |c: &Context| { 585 assert_eq!(true, c.bool_flag("bool")); 586 match c.string_flag("string") { 587 Ok(flag) => assert_eq!("str=ing".to_string(), flag), 588 _ => assert!(false, "string test false..."), 589 } 590 match c.int_flag("int") { 591 Ok(flag) => assert_eq!(100, flag), 592 _ => assert!(false, "int test false..."), 593 } 594 match c.float_flag("float") { 595 Ok(flag) => assert_eq!(1.23, flag), 596 _ => assert!(false, "float test false..."), 597 } 598 }; 599 600 let app = App::new("test") 601 .author("Author <author@example.com>") 602 .description("This is a great tool.") 603 .usage("test [arg]") 604 .version("0.0.1") 605 .action(action) 606 .flag(Flag::new("bool", FlagType::Bool)) 607 .flag(Flag::new("string", FlagType::String)) 608 .flag(Flag::new("int", FlagType::Int)) 609 .flag(Flag::new("float", FlagType::Float).alias("f")); 610 611 app.run(vec![ 612 "test".to_string(), 613 "args".to_string(), 614 "--bool".to_string(), 615 "--string=str=ing".to_string(), 616 "--int=100".to_string(), 617 "-f=1.23".to_string(), 618 ]); 619 620 assert_eq!(app.name, "test".to_string()); 621 assert_eq!(app.usage, Some("test [arg]".to_string())); 622 assert_eq!(app.author, Some("Author <author@example.com>".to_string())); 623 assert_eq!(app.description, Some("This is a great tool.".to_string())); 624 assert_eq!(app.version, Some("0.0.1".to_string())); 625 } 626 } 627