Improve command-line argument parsing

This commit is contained in:
Matt Ickstadt 2018-08-02 15:48:22 -05:00
parent ac4e00c7c6
commit b8f8e76899
7 changed files with 116 additions and 59 deletions

View File

@ -1,21 +1,21 @@
use clap::{App, ArgMatches, SubCommand}; use clap::{App, ArgMatches, SubCommand};
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::MDBook; use mdbook::MDBook;
use std::path::PathBuf;
use {get_book_dir, open}; use {get_book_dir, open};
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("build") SubCommand::with_name("build")
.about("Build the book from the markdown files") .about("Builds a book from its markdown files")
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
.arg_from_usage( .arg_from_usage(
"-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book \ "-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
when omitted)'", (If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
) )
.arg_from_usage( .arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'", "[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
) )
.arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
} }
// Build command implementation // Build command implementation
@ -24,7 +24,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let mut book = MDBook::load(&book_dir)?; let mut book = MDBook::load(&book_dir)?;
if let Some(dest_dir) = args.value_of("dest-dir") { if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = PathBuf::from(dest_dir); book.config.build.build_dir = dest_dir.into();
} }
book.build()?; book.build()?;

View File

@ -3,15 +3,18 @@ use get_book_dir;
use mdbook::errors::*; use mdbook::errors::*;
use mdbook::MDBook; use mdbook::MDBook;
use std::fs; use std::fs;
use std::path::PathBuf;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("clean") SubCommand::with_name("clean")
.about("Delete built book") .about("Deletes a built book")
.arg_from_usage( .arg_from_usage(
"-d, --dest-dir=[dest-dir] 'The directory of built book{n}(Defaults to ./book when \ "-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
omitted)'", (If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
) )
} }
@ -21,7 +24,7 @@ pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
let book = MDBook::load(&book_dir)?; let book = MDBook::load(&book_dir)?;
let dir_to_remove = match args.value_of("dest-dir") { let dir_to_remove = match args.value_of("dest-dir") {
Some(dest_dir) => PathBuf::from(dest_dir), Some(dest_dir) => dest_dir.into(),
None => book.root.join(&book.config.build.build_dir), None => book.root.join(&book.config.build.build_dir),
}; };
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?; fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;

View File

@ -10,12 +10,12 @@ use std::process::Command;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("init") SubCommand::with_name("init")
.about("Create boilerplate structure and files in the directory") .about("Creates the boilerplate structure and files for a new book")
// the {n} denotes a newline which will properly aligned in all help messages // the {n} denotes a newline which will properly aligned in all help messages
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory \ .arg_from_usage("[dir] 'Directory to create the book in{n}\
when omitted)'") (Defaults to the Current Directory when omitted)'")
.arg_from_usage("--theme 'Copies the default theme into your source folder'") .arg_from_usage("--theme 'Copies the default theme into your source folder'")
.arg_from_usage("--force 'skip confirmation prompts'") .arg_from_usage("--force 'Skips confirmation prompts'")
} }
// Init command implementation // Init command implementation

View File

@ -7,7 +7,7 @@ use self::iron::{
}; };
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
use super::watch; use super::watch;
use clap::{App, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use mdbook::errors::*; use mdbook::errors::*;
use mdbook::utils; use mdbook::utils;
use mdbook::MDBook; use mdbook::MDBook;
@ -19,23 +19,52 @@ struct ErrorRecover;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("serve") SubCommand::with_name("serve")
.about("Serve the book at http://localhost:3000. Rebuild and reload on change.") .about("Serves a book at http://localhost:3000, and rebuilds it on changes")
.arg_from_usage( .arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'", "-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
) (If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
.arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
.arg_from_usage(
"-w, --websocket-port=[ws-port] 'Use another port for the websocket connection \
(livereload){n}(Defaults to 3001)'",
) )
.arg_from_usage( .arg_from_usage(
"-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'", "[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
) )
.arg_from_usage( .arg(
"-a, --address=[address] 'Address that the browser can reach the websocket server \ Arg::with_name("hostname")
from{n}(Defaults to the interface address)'", .short("n")
.long("hostname")
.takes_value(true)
.default_value("localhost")
.empty_values(false)
.help("Hostname to listen on for HTTP connections"),
) )
.arg_from_usage("-o, --open 'Open the book server in a web browser'") .arg(
Arg::with_name("port")
.short("p")
.long("port")
.takes_value(true)
.default_value("3000")
.empty_values(false)
.help("Port to use for HTTP connections"),
)
.arg(
Arg::with_name("websocket-hostname")
.long("websocket-hostname")
.takes_value(true)
.empty_values(false)
.help(
"Hostname to connect to for WebSockets connections (Defaults to the HTTP hostname)",
),
)
.arg(
Arg::with_name("websocket-port")
.short("w")
.long("websocket-port")
.takes_value(true)
.default_value("3001")
.empty_values(false)
.help("Port to use for WebSockets livereload connections"),
)
.arg_from_usage("-o, --open 'Opens the book server in a web browser'")
} }
// Watch command implementation // Watch command implementation
@ -43,19 +72,23 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?; let mut book = MDBook::load(&book_dir)?;
let port = args.value_of("port").unwrap_or("3000"); let port = args.value_of("port").unwrap();
let ws_port = args.value_of("websocket-port").unwrap_or("3001"); let ws_port = args.value_of("websocket-port").unwrap();
let interface = args.value_of("interface").unwrap_or("localhost"); let hostname = args.value_of("hostname").unwrap();
let public_address = args.value_of("address").unwrap_or(interface); let public_address = args.value_of("websocket-address").unwrap_or(hostname);
let open_browser = args.is_present("open"); let open_browser = args.is_present("open");
let address = format!("{}:{}", interface, port); let address = format!("{}:{}", hostname, port);
let ws_address = format!("{}:{}", interface, ws_port); let ws_address = format!("{}:{}", hostname, ws_port);
let livereload_url = format!("ws://{}:{}", public_address, ws_port); let livereload_url = format!("ws://{}:{}", public_address, ws_port);
book.config book.config
.set("output.html.livereload-url", &livereload_url)?; .set("output.html.livereload-url", &livereload_url)?;
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into();
}
book.build()?; book.build()?;
let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html"))); let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html")));
@ -87,10 +120,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
// FIXME: This area is really ugly because we need to re-set livereload :( // FIXME: This area is really ugly because we need to re-set livereload :(
let livereload_url = livereload_url.clone();
let result = MDBook::load(&book_dir) let result = MDBook::load(&book_dir)
.and_then(move |mut b| { .and_then(|mut b| {
b.config.set("output.html.livereload-url", &livereload_url)?; b.config.set("output.html.livereload-url", &livereload_url)?;
Ok(b) Ok(b)
}) })

View File

@ -1,4 +1,4 @@
use clap::{App, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use get_book_dir; use get_book_dir;
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::MDBook; use mdbook::MDBook;
@ -6,11 +6,24 @@ use mdbook::MDBook;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("test") SubCommand::with_name("test")
.about("Test that code samples compile") .about("Tests that a book's Rust code samples compile")
.arg_from_usage("-L, --library-path [DIR]... 'directories to add to crate search path'")
.arg_from_usage( .arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'", "-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
) )
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
)
.arg(Arg::with_name("library-path")
.short("L")
.long("library-path")
.value_name("dir")
.takes_value(true)
.require_delimiter(true)
.multiple(true)
.empty_values(false)
.help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
} }
// test command implementation // test command implementation
@ -21,6 +34,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?; let mut book = MDBook::load(&book_dir)?;
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into();
}
book.test(library_paths)?; book.test(library_paths)?;
Ok(()) Ok(())

View File

@ -13,11 +13,16 @@ use {get_book_dir, open};
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("watch") SubCommand::with_name("watch")
.about("Watch the files for changes") .about("Watches a book's files and rebuilds it on changes")
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
.arg_from_usage( .arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'", "-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
) )
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
)
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
} }
// Watch command implementation // Watch command implementation

View File

@ -20,22 +20,23 @@ use std::path::{Path, PathBuf};
mod cmd; mod cmd;
const NAME: &'static str = "mdbook"; const NAME: &'static str = "mdBook";
const VERSION: &'static str = concat!("v", crate_version!());
fn main() { fn main() {
init_logger(); init_logger();
// Create a list of valid arguments and sub-commands // Create a list of valid arguments and sub-commands
let app = App::new(NAME) let app = App::new(NAME)
.about("Create a book in form of a static website from markdown files") .about("Creates a book from markdown files")
.author("Mathieu David <mathieudavid@mathieudavid.org>") .author("Mathieu David <mathieudavid@mathieudavid.org>")
// Get the version from our Cargo.toml using clap's crate_version!() macro .version(VERSION)
.version(concat!("v",crate_version!())) .setting(AppSettings::GlobalVersion)
.setting(AppSettings::ArgRequiredElseHelp) .setting(AppSettings::ArgRequiredElseHelp)
.after_help("For more information about a specific command, \ .after_help(
try `mdbook <command> --help`\n\ "For more information about a specific command, try `mdbook <command> --help`\n\
Source code for mdbook available \ The source code for mdBook is available at: https://github.com/rust-lang-nursery/mdBook",
at: https://github.com/rust-lang-nursery/mdBook") )
.subcommand(cmd::init::make_subcommand()) .subcommand(cmd::init::make_subcommand())
.subcommand(cmd::build::make_subcommand()) .subcommand(cmd::build::make_subcommand())
.subcommand(cmd::test::make_subcommand()) .subcommand(cmd::test::make_subcommand())