oxedyne/fe2o3/fe2o3_syntax/src/argv.rs
4.4 KiB, 1 run
created by r1870400018:62022, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! The driver between a process's command line and a syntax: `help`, `--help` and `--version` |
| 2 | //! are answered here, so they never reach the caller's dispatch, and the rest is parsed as one |
| 3 | //! message. |
| 4 | use crate::{ |
| 5 | core::SyntaxRef, |
| 6 | help::Page, |
| 7 | msg::Msg, |
| 8 | }; |
| 9 | |
| 10 | use oxedyne_fe2o3_core::prelude::*; |
| 11 | |
| 12 | use std::ffi::OsString; |
| 13 | |
| 14 | |
| 15 | /// What a command line asked for. |
| 16 | #[derive(Clone, Debug)] |
| 17 | pub enum Parsed { |
| 18 | Run(Msg), |
| 19 | Help(Page), |
| 20 | Version, |
| 21 | } |
| 22 | |
| 23 | /// How close a misspelt command or option must be to a real one to be suggested. |
| 24 | pub const SIMILARITY_THRESHOLD: f64 = 0.4; |
| 25 | |
| 26 | /// Reads a process's arguments, `std::env::args_os()` or the like, program name first. |
| 27 | /// |
| 28 | /// With no words, or `help`, `-h` or `--help` first, the answer is the command table. |
| 29 | /// `help <command>` or `help <topic>` is that page, and `help --all` is everything. A |
| 30 | /// command followed by `-h` or `--help` where an option could stand is the command's page; |
| 31 | /// as an option's value, after a `--`, or among the words of a verbatim value, either is a |
| 32 | /// word like any other. `--version` or `-V` alone is the version. Otherwise the words are a |
| 33 | /// message. |
| 34 | pub fn parse<I, S>(syntax: &SyntaxRef, args: I) -> Outcome<Parsed> |
| 35 | where |
| 36 | I: IntoIterator<Item=S>, |
| 37 | S: Into<OsString>, |
| 38 | { |
| 39 | let words = res!(words(args)); |
| 40 | read(syntax, words) |
| 41 | } |
| 42 | |
| 43 | /// The words after the program name, each of which must be UTF-8. A word that is not is |
| 44 | /// refused by position rather than panicking as `std::env::args` does. |
| 45 | pub fn words<I, S>(args: I) -> Outcome<Vec<String>> |
| 46 | where |
| 47 | I: IntoIterator<Item=S>, |
| 48 | S: Into<OsString>, |
| 49 | { |
| 50 | let mut out = Vec::new(); |
| 51 | for (i, arg) in args.into_iter().enumerate().skip(1) { |
| 52 | match arg.into().into_string() { |
| 53 | Ok(s) => out.push(s), |
| 54 | Err(os) => return Err(err!( |
| 55 | "The word at position {} is not UTF-8 ('{}'), and command line words must \ |
| 56 | be.", i, os.to_string_lossy(); |
| 57 | Input, Invalid, UTF8)), |
| 58 | } |
| 59 | } |
| 60 | Ok(out) |
| 61 | } |
| 62 | |
| 63 | /// As `parse`, for words already taken from the command line. |
| 64 | pub fn read(syntax: &SyntaxRef, words: Vec<String>) -> Outcome<Parsed> { |
| 65 | let first = match words.first() { |
| 66 | Some(w) => w.as_str(), |
| 67 | None => return Ok(Parsed::Help(Page::Summary)), |
| 68 | }; |
| 69 | if words.len() == 1 && (first == "--version" || first == "-V") { |
| 70 | return Ok(Parsed::Version); |
| 71 | } |
| 72 | if Msg::is_help_flag(first) && words.len() == 1 { |
| 73 | return Ok(Parsed::Help(Page::Summary)); |
| 74 | } |
| 75 | if first == "help" && syntax.get_cmd("help").is_none() { |
| 76 | return match words.get(1).map(|w| w.as_str()) { |
| 77 | None => Ok(Parsed::Help(Page::Summary)), |
| 78 | Some("--all") => Ok(Parsed::Help(Page::All)), |
| 79 | Some(name) => { |
| 80 | if words.len() > 2 { |
| 81 | return Err(err!( |
| 82 | "The word '{}' at position 3 is more than help takes: help names one \ |
| 83 | command or topic.", words[2]; |
| 84 | Input, Excessive)); |
| 85 | } |
| 86 | page_for(syntax, name) |
| 87 | }, |
| 88 | }; |
| 89 | } |
| 90 | // The parser says where a help flag stands, since only it knows whether the word is a value. |
| 91 | let msg = Msg::new(syntax.clone()); |
| 92 | let msg = res!(msg.rx_argv(words, Some(SIMILARITY_THRESHOLD))); |
| 93 | if msg.end.help { |
| 94 | return Ok(Parsed::Help(match &msg.end.cmd { |
| 95 | Some(name) => Page::Command(name.clone()), |
| 96 | None => Page::Summary, |
| 97 | })); |
| 98 | } |
| 99 | Ok(Parsed::Run(msg)) |
| 100 | } |
| 101 | |
| 102 | fn page_for(syntax: &SyntaxRef, name: &str) -> Outcome<Parsed> { |
| 103 | if syntax.get_cmd(name).is_some() { |
| 104 | return Ok(Parsed::Help(Page::Command(name.to_string()))); |
| 105 | } |
| 106 | if syntax.get_topic(name).is_some() { |
| 107 | return Ok(Parsed::Help(Page::Topic(name.to_string()))); |
| 108 | } |
| 109 | let mut names: Vec<String> = syntax.cmds_in_order().into_iter() |
| 110 | .map(|c| c.config().name.clone()).collect(); |
| 111 | for topic in &syntax.config().topics { |
| 112 | names.push(topic.name.clone()); |
| 113 | } |
| 114 | Err(match Msg::closest(name, &names, SIMILARITY_THRESHOLD) { |
| 115 | Some(suggestion) => err!( |
| 116 | "Did you mean '{}'? There is no command or help topic '{}' in '{}'.", |
| 117 | suggestion, name, syntax.config().name; |
| 118 | Input, Invalid, Suggestion), |
| 119 | None => err!( |
| 120 | "There is no command or help topic '{}' in '{}'.", |
| 121 | name, syntax.config().name; |
| 122 | Input, Invalid), |
| 123 | }) |
| 124 | } |