-
Notifications
You must be signed in to change notification settings - Fork 13.6k
Overhaul Bootstrap (x.py) Command-Line-Parsing & Help Output #41011
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
e1b0027
8ad5c95
e1c1e09
584b405
992a59e
5ba579e
aa4bd0e
2c9ae48
6b72586
1e53898
20cb700
ea2bfae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ use std::fs; | |
use std::path::PathBuf; | ||
use std::process; | ||
|
||
use getopts::{Matches, Options}; | ||
use getopts::Options; | ||
|
||
use Build; | ||
use config::Config; | ||
|
@@ -75,7 +75,22 @@ pub enum Subcommand { | |
|
||
impl Flags { | ||
pub fn parse(args: &[String]) -> Flags { | ||
let mut extra_help = String::new(); | ||
let mut subcommand_help = format!("\ | ||
Usage: x.py <subcommand> [options] [<paths>...] | ||
|
||
Subcommands: | ||
build Compile either the compiler or libraries | ||
test Build and run some test suites | ||
bench Build and run some benchmarks | ||
doc Build documentation | ||
clean Clean out build directories | ||
dist Build and/or install distribution artifacts | ||
|
||
To learn more about a subcommand, run `./x.py <subcommand> -h`"); | ||
|
||
let mut opts = Options::new(); | ||
// Options common to all subcommands | ||
opts.optflagmulti("v", "verbose", "use verbose output (-vv for very verbose)"); | ||
opts.optflag("i", "incremental", "use incremental compilation"); | ||
opts.optopt("", "config", "TOML configuration file for build", "FILE"); | ||
|
@@ -89,21 +104,41 @@ impl Flags { | |
opts.optopt("j", "jobs", "number of jobs to run in parallel", "JOBS"); | ||
opts.optflag("h", "help", "print this help message"); | ||
|
||
let usage = |n, opts: &Options| -> ! { | ||
let command = args.get(0).map(|s| &**s); | ||
let brief = format!("Usage: x.py {} [options] [<args>...]", | ||
command.unwrap_or("<command>")); | ||
// fn usage() | ||
let usage = |exit_code: i32, opts: &Options, subcommand_help: &str, extra_help: &str| -> ! { | ||
println!("{}", opts.usage(subcommand_help)); | ||
if !extra_help.is_empty() { | ||
println!("{}", extra_help); | ||
} | ||
process::exit(exit_code); | ||
}; | ||
|
||
// We can't use getopt to parse the options until we have completed specifying which | ||
// options are valid, but under the current implementation, some options are conditional on | ||
// the subcommand. Therefore we must manually identify the subcommand first, so that we can | ||
// complete the definition of the options. Then we can use the getopt::Matches object from | ||
// there on out. | ||
let mut possible_subcommands = args.iter().collect::<Vec<_>>(); | ||
possible_subcommands.retain(|&s| !s.starts_with('-')); | ||
let subcommand = match possible_subcommands.first() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately I don't think this is quite right, for example There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! Yet another reason getopts should have some Hmmm...options with their own arguments. That's actually a tough nut to crack without rewriting a lot of our own getopt-like logic. What if we cheated and took the first argument not starting with I'll see what I can do to fix this later tonight. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I unfortunately don't really know a great solution here, I've reached the same conclusion as you have which is that most of the option are relatively unfortunate. This is why the "bug" exists today where you can't say There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I devised a pretty good solution.
This should work. The only time you would ever hit the error condition would be the pathological condition where you put an option with an argument which could have been a subcommand before the actual subcommand. Like this: ./x.py --option clean build There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm personally ok with a better error message, but if you're willing to implement it that sounds reasonable to me! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sweet. I just pushed my latest commit. Hopefully I've managed to make it all work without breaking anything... |
||
Some(s) => s, | ||
None => { | ||
// No subcommand -- show the general usage and subcommand help | ||
println!("{}\n", subcommand_help); | ||
process::exit(0); | ||
} | ||
}; | ||
|
||
println!("{}", opts.usage(&brief)); | ||
match command { | ||
Some("build") => { | ||
println!("\ | ||
// Some subcommands have specific arguments help text | ||
match subcommand.as_str() { | ||
"build" => { | ||
subcommand_help.push_str("\n | ||
Arguments: | ||
This subcommand accepts a number of positional arguments of directories to | ||
the crates and/or artifacts to compile. For example: | ||
This subcommand accepts a number of paths to directories to the crates | ||
and/or artifacts to compile. For example: | ||
|
||
./x.py build src/libcore | ||
./x.py build src/libproc_macro | ||
./x.py build src/libcore src/libproc_macro | ||
./x.py build src/libstd --stage 1 | ||
|
||
If no arguments are passed then the complete artifacts for that stage are | ||
|
@@ -114,15 +149,14 @@ Arguments: | |
|
||
For a quick build with a usable compile, you can pass: | ||
|
||
./x.py build --stage 1 src/libtest | ||
"); | ||
} | ||
|
||
Some("test") => { | ||
println!("\ | ||
./x.py build --stage 1 src/libtest"); | ||
} | ||
"test" => { | ||
opts.optmulti("", "test-args", "extra arguments", "ARGS"); | ||
subcommand_help.push_str("\n | ||
Arguments: | ||
This subcommand accepts a number of positional arguments of directories to | ||
tests that should be compiled and run. For example: | ||
This subcommand accepts a number of paths to directories to tests that | ||
should be compiled and run. For example: | ||
|
||
./x.py test src/test/run-pass | ||
./x.py test src/libstd --test-args hash_map | ||
|
@@ -132,171 +166,133 @@ Arguments: | |
compiled and tested. | ||
|
||
./x.py test | ||
./x.py test --stage 1 | ||
"); | ||
} | ||
|
||
Some("doc") => { | ||
println!("\ | ||
./x.py test --stage 1"); | ||
} | ||
"bench" => { | ||
opts.optmulti("", "test-args", "extra arguments", "ARGS"); | ||
} | ||
"doc" => { | ||
subcommand_help.push_str("\n | ||
Arguments: | ||
This subcommand accepts a number of positional arguments of directories of | ||
documentation to build. For example: | ||
This subcommand accepts a number of paths to directories of documentation | ||
to build. For example: | ||
|
||
./x.py doc src/doc/book | ||
./x.py doc src/doc/nomicon | ||
./x.py doc src/libstd | ||
./x.py doc src/doc/book src/libstd | ||
|
||
If no arguments are passed then everything is documented: | ||
|
||
./x.py doc | ||
./x.py doc --stage 1 | ||
"); | ||
} | ||
|
||
_ => {} | ||
./x.py doc --stage 1"); | ||
} | ||
|
||
if let Some(command) = command { | ||
if command == "build" || | ||
command == "dist" || | ||
command == "doc" || | ||
command == "test" || | ||
command == "bench" || | ||
command == "clean" { | ||
println!("Available invocations:"); | ||
if args.iter().any(|a| a == "-v") { | ||
let flags = Flags::parse(&["build".to_string()]); | ||
let mut config = Config::default(); | ||
config.build = flags.build.clone(); | ||
let mut build = Build::new(flags, config); | ||
metadata::build(&mut build); | ||
step::build_rules(&build).print_help(command); | ||
} else { | ||
println!(" ... elided, run `./x.py {} -h -v` to see", | ||
command); | ||
} | ||
|
||
println!(""); | ||
} | ||
} | ||
|
||
println!("\ | ||
Subcommands: | ||
build Compile either the compiler or libraries | ||
test Build and run some test suites | ||
bench Build and run some benchmarks | ||
doc Build documentation | ||
clean Clean out build directories | ||
dist Build and/or install distribution artifacts | ||
|
||
To learn more about a subcommand, run `./x.py <command> -h` | ||
"); | ||
|
||
process::exit(n); | ||
}; | ||
if args.len() == 0 { | ||
println!("a command must be passed"); | ||
usage(1, &opts); | ||
} | ||
let parse = |opts: &Options| { | ||
let m = opts.parse(&args[1..]).unwrap_or_else(|e| { | ||
println!("failed to parse options: {}", e); | ||
usage(1, opts); | ||
}); | ||
if m.opt_present("h") { | ||
usage(0, opts); | ||
"dist" => { | ||
opts.optflag("", "install", "run installer as well"); | ||
} | ||
return m | ||
_ => { } | ||
}; | ||
|
||
// Done specifying what options are possible, so do the getopts parsing | ||
let matches = opts.parse(&args[..]).unwrap_or_else(|e| { | ||
// Invalid argument/option format | ||
println!("\n{}\n", e); | ||
usage(1, &opts, &subcommand_help, &extra_help); | ||
}); | ||
// Get any optional paths which occur after the subcommand | ||
let cwd = t!(env::current_dir()); | ||
let remaining_as_path = |m: &Matches| { | ||
m.free.iter().map(|p| cwd.join(p)).collect::<Vec<_>>() | ||
}; | ||
let paths = matches.free[1..].iter().map(|p| cwd.join(p)).collect::<Vec<_>>(); | ||
|
||
|
||
// All subcommands can have an optional "Available paths" section | ||
if matches.opt_present("verbose") { | ||
let flags = Flags::parse(&["build".to_string()]); | ||
let mut config = Config::default(); | ||
config.build = flags.build.clone(); | ||
let mut build = Build::new(flags, config); | ||
metadata::build(&mut build); | ||
let maybe_rules_help = step::build_rules(&build).get_help(subcommand); | ||
if maybe_rules_help.is_some() { | ||
extra_help.push_str(maybe_rules_help.unwrap().as_str()); | ||
} | ||
} else { | ||
extra_help.push_str(format!("Run `./x.py {} -h -v` to see a list of available paths.", | ||
subcommand).as_str()); | ||
} | ||
|
||
// User passed in -h/--help? | ||
if matches.opt_present("help") { | ||
usage(0, &opts, &subcommand_help, &extra_help); | ||
} | ||
|
||
let m: Matches; | ||
let cmd = match &args[0][..] { | ||
let cmd = match subcommand.as_str() { | ||
"build" => { | ||
m = parse(&opts); | ||
Subcommand::Build { paths: remaining_as_path(&m) } | ||
} | ||
"doc" => { | ||
m = parse(&opts); | ||
Subcommand::Doc { paths: remaining_as_path(&m) } | ||
Subcommand::Build { paths: paths } | ||
} | ||
"test" => { | ||
opts.optmulti("", "test-args", "extra arguments", "ARGS"); | ||
m = parse(&opts); | ||
Subcommand::Test { | ||
paths: remaining_as_path(&m), | ||
test_args: m.opt_strs("test-args"), | ||
paths: paths, | ||
test_args: matches.opt_strs("test-args"), | ||
} | ||
} | ||
"bench" => { | ||
opts.optmulti("", "test-args", "extra arguments", "ARGS"); | ||
m = parse(&opts); | ||
Subcommand::Bench { | ||
paths: remaining_as_path(&m), | ||
test_args: m.opt_strs("test-args"), | ||
paths: paths, | ||
test_args: matches.opt_strs("test-args"), | ||
} | ||
} | ||
"doc" => { | ||
Subcommand::Doc { paths: paths } | ||
} | ||
"clean" => { | ||
m = parse(&opts); | ||
if m.free.len() > 0 { | ||
println!("clean takes no arguments"); | ||
usage(1, &opts); | ||
if matches.free.len() > 0 { | ||
println!("\nclean takes no arguments\n"); | ||
usage(1, &opts, &subcommand_help, &extra_help); | ||
} | ||
Subcommand::Clean | ||
} | ||
"dist" => { | ||
opts.optflag("", "install", "run installer as well"); | ||
m = parse(&opts); | ||
Subcommand::Dist { | ||
paths: remaining_as_path(&m), | ||
install: m.opt_present("install"), | ||
paths: paths, | ||
install: matches.opt_present("install"), | ||
} | ||
} | ||
"--help" => usage(0, &opts), | ||
cmd => { | ||
println!("unknown command: {}", cmd); | ||
usage(1, &opts); | ||
_ => { | ||
usage(1, &opts, &subcommand_help, &extra_help); | ||
} | ||
}; | ||
|
||
|
||
let cfg_file = m.opt_str("config").map(PathBuf::from).or_else(|| { | ||
let cfg_file = matches.opt_str("config").map(PathBuf::from).or_else(|| { | ||
if fs::metadata("config.toml").is_ok() { | ||
Some(PathBuf::from("config.toml")) | ||
} else { | ||
None | ||
} | ||
}); | ||
|
||
let mut stage = m.opt_str("stage").map(|j| j.parse().unwrap()); | ||
|
||
let incremental = m.opt_present("i"); | ||
let mut stage = matches.opt_str("stage").map(|j| j.parse().unwrap()); | ||
|
||
if incremental { | ||
if matches.opt_present("incremental") { | ||
if stage.is_none() { | ||
stage = Some(1); | ||
} | ||
} | ||
|
||
Flags { | ||
verbose: m.opt_count("v"), | ||
verbose: matches.opt_count("verbose"), | ||
stage: stage, | ||
on_fail: m.opt_str("on-fail"), | ||
keep_stage: m.opt_str("keep-stage").map(|j| j.parse().unwrap()), | ||
build: m.opt_str("build").unwrap_or_else(|| { | ||
on_fail: matches.opt_str("on-fail"), | ||
keep_stage: matches.opt_str("keep-stage").map(|j| j.parse().unwrap()), | ||
build: matches.opt_str("build").unwrap_or_else(|| { | ||
env::var("BUILD").unwrap() | ||
}), | ||
host: split(m.opt_strs("host")), | ||
target: split(m.opt_strs("target")), | ||
host: split(matches.opt_strs("host")), | ||
target: split(matches.opt_strs("target")), | ||
config: cfg_file, | ||
src: m.opt_str("src").map(PathBuf::from), | ||
jobs: m.opt_str("jobs").map(|j| j.parse().unwrap()), | ||
src: matches.opt_str("src").map(PathBuf::from), | ||
jobs: matches.opt_str("jobs").map(|j| j.parse().unwrap()), | ||
cmd: cmd, | ||
incremental: incremental, | ||
incremental: matches.opt_present("incremental"), | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Referencing your commit message, how about leaving a mention that options can be used in any position. I'm only reading the code, so apologies if this is actually present.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries. It is mentioned in both the commit message where the change was made (but not on the first line of the message) and in the PR text. I also stated in the PR text that this resolves #38373, which is a request to accept positional arguments after options.
If there is something I can do to make it more clear, I'd be happy to. Just let me know. Should I amend the commit it was in to get that info into the first commit line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see; I did notice this in the PR text and commit messages. I was rather wondering if you could adjust the usage string to mention this - after all, users of the script might not always look up the file's history.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see. What do you suggest would make it clearer? I haven't observed a standard help message that indicates such a capability, rather it seems to be generally assumed. My first inclination would be to annotate the "Options:" header in some way, but that header is part of the generated help string by getopts in this instance.