Skip to content
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

Display embedded man pages for built-in commands. #8456

Merged
merged 3 commits into from
Aug 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ features = [
cargo-test-macro = { path = "crates/cargo-test-macro", version = "0.1.0" }
cargo-test-support = { path = "crates/cargo-test-support", version = "0.1.0" }

[build-dependencies]
flate2 = { version = "1.0.3", default-features = false, features = ["zlib"] }
tar = { version = "0.4.26", default-features = false }

[[bin]]
name = "cargo"
test = false
Expand Down
34 changes: 34 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use flate2::{Compression, GzBuilder};
use std::ffi::OsStr;
use std::fs;
use std::path::Path;

fn main() {
compress_man();
}

fn compress_man() {
let out_path = Path::new(&std::env::var("OUT_DIR").unwrap()).join("man.tgz");
let dst = fs::File::create(out_path).unwrap();
let encoder = GzBuilder::new()
.filename("man.tar")
.write(dst, Compression::best());
let mut ar = tar::Builder::new(encoder);

let mut add_files = |dir, extension| {
for entry in fs::read_dir(dir).unwrap() {
let path = entry.unwrap().path();
if path.extension() != Some(extension) {
continue;
}
println!("cargo:rerun-if-changed={}", path.display());
ar.append_path_with_name(&path, path.file_name().unwrap())
.unwrap();
}
};

add_files(Path::new("src/etc/man"), OsStr::new("1"));
add_files(Path::new("src/doc/man/generated_txt"), OsStr::new("txt"));
let encoder = ar.into_inner().unwrap();
encoder.finish().unwrap();
}
5 changes: 5 additions & 0 deletions src/bin/cargo/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ pub fn main(config: &mut Config) -> CliResult {
// CAUTION: Be careful with using `config` until it is configured below.
// In general, try to avoid loading config values unless necessary (like
// the [alias] table).

if commands::help::handle_embedded_help(config) {
return Ok(());
}

let args = match cli().get_matches_safe() {
Ok(args) => args,
Err(e) => {
Expand Down
25 changes: 1 addition & 24 deletions src/bin/cargo/commands/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,7 @@ pub fn cli() -> App {
"Run all benchmarks regardless of failure",
))
.arg_unit_graph()
.after_help(
"\
The benchmark filtering argument BENCHNAME and all the arguments following the
two dashes (`--`) are passed to the benchmark binaries and thus to libtest
(rustc's built in unit-test and micro-benchmarking framework). If you're
passing arguments to both Cargo and the binary, the ones after `--` go to the
binary, the ones before go to Cargo. For details about libtest's arguments see
the output of `cargo bench -- --help`.

If the `--package` argument is given, then SPEC is a package ID specification
which indicates which package should be benchmarked. If it is not given, then
the current package is benchmarked. For more information on SPEC and its format,
see the `cargo help pkgid` command.

All packages in the workspace are benchmarked if the `--workspace` flag is supplied. The
`--workspace` flag is automatically assumed for a virtual manifest.
Note that `--exclude` has to be specified in conjunction with the `--workspace` flag.

The `--jobs` argument affects the building of the benchmark executable but does
not affect how many jobs are used when running the benchmarks.

Compilation can be customized with the `bench` profile in the manifest.
",
)
.after_help("Run `cargo help bench` for more detailed information.\n")
}

pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
Expand Down
12 changes: 1 addition & 11 deletions src/bin/cargo/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,7 @@ pub fn cli() -> App {
.arg_message_format()
.arg_build_plan()
.arg_unit_graph()
.after_help(
"\
All packages in the workspace are built if the `--workspace` flag is supplied. The
`--workspace` flag is automatically assumed for a virtual manifest.
Note that `--exclude` has to be specified in conjunction with the `--workspace` flag.

Compilation can be configured via the use of profiles which are configured in
the manifest. The default profile for this command is `dev`, but passing
the --release flag will use the `release` profile instead.
",
)
.after_help("Run `cargo help build` for more detailed information.\n")
}

pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
Expand Down
20 changes: 1 addition & 19 deletions src/bin/cargo/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,7 @@ pub fn cli() -> App {
.arg_manifest_path()
.arg_message_format()
.arg_unit_graph()
.after_help(
"\
If the `--package` argument is given, then SPEC is a package ID specification
which indicates which package should be built. If it is not given, then the
current package is built. For more information on SPEC and its format, see the
`cargo help pkgid` command.

All packages in the workspace are checked if the `--workspace` flag is supplied. The
`--workspace` flag is automatically assumed for a virtual manifest.
Note that `--exclude` has to be specified in conjunction with the `--workspace` flag.

Compilation can be configured via the use of profiles which are configured in
the manifest. The default profile for this command is `dev`, but passing
the `--release` flag will use the `release` profile instead.

The `--profile test` flag can be used to check unit tests with the
`#[cfg(test)]` attribute.
",
)
.after_help("Run `cargo help check` for more detailed information.\n")
}

pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
Expand Down
9 changes: 1 addition & 8 deletions src/bin/cargo/commands/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,7 @@ pub fn cli() -> App {
.arg_release("Whether or not to clean release artifacts")
.arg_profile("Clean artifacts of the specified profile")
.arg_doc("Whether or not to clean just the documentation directory")
.after_help(
"\
If the `--package` argument is given, then SPEC is a package ID specification
which indicates which package's artifacts should be cleaned out. If it is not
given, then all packages' artifacts are removed. For more information on SPEC
and its format, see the `cargo help pkgid` command.
",
)
.after_help("Run `cargo help clean` for more detailed information.\n")
}

pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
Expand Down
17 changes: 1 addition & 16 deletions src/bin/cargo/commands/doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,7 @@ pub fn cli() -> App {
.arg_manifest_path()
.arg_message_format()
.arg_unit_graph()
.after_help(
"\
By default the documentation for the local package and all dependencies is
built. The output is all placed in `target/doc` in rustdoc's usual format.

All packages in the workspace are documented if the `--workspace` flag is
supplied. The `--workspace` flag is automatically assumed for a virtual
manifest. Note that `--exclude` has to be specified in conjunction with the
`--workspace` flag.

If the `--package` argument is given, then SPEC is a package ID specification
which indicates which package should be documented. If it is not given, then the
current package is documented. For more information on SPEC and its format, see
the `cargo help pkgid` command.
",
)
.after_help("Run `cargo help doc` for more detailed information.\n")
}

pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
Expand Down
13 changes: 1 addition & 12 deletions src/bin/cargo/commands/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,7 @@ pub fn cli() -> App {
.arg(opt("quiet", "No output printed to stdout").short("q"))
.arg_manifest_path()
.arg_target_triple("Fetch dependencies for the target triple")
.after_help(
"\
If a lock file is available, this command will ensure that all of the Git
dependencies and/or registries dependencies are downloaded and locally
available. The network is never touched after a `cargo fetch` unless
the lock file changes.

If the lock file is not available, then this is the equivalent of
`cargo generate-lockfile`. A lock file is generated and dependencies are also
all updated.
",
)
.after_help("Run `cargo help fetch` for more detailed information.\n")
}

pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
Expand Down
32 changes: 1 addition & 31 deletions src/bin/cargo/commands/fix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,37 +72,7 @@ pub fn cli() -> App {
.long("allow-staged")
.help("Fix code even if the working directory has staged changes"),
)
.after_help(
"\
This Cargo subcommand will automatically take rustc's suggestions from
diagnostics like warnings and apply them to your source code. This is intended
to help automate tasks that rustc itself already knows how to tell you to fix!
The `cargo fix` subcommand is also being developed for the Rust 2018 edition
to provide code the ability to easily opt-in to the new edition without having
to worry about any breakage.

Executing `cargo fix` will under the hood execute `cargo check`. Any warnings
applicable to your crate will be automatically fixed (if possible) and all
remaining warnings will be displayed when the check process is finished. For
example if you'd like to prepare for the 2018 edition, you can do so by
executing:

cargo fix --edition

which behaves the same as `cargo check --all-targets`. Similarly if you'd like
to fix code for different platforms you can do:

cargo fix --edition --target x86_64-pc-windows-gnu

or if your crate has optional features:

cargo fix --edition --no-default-features --features foo

If you encounter any problems with `cargo fix` or otherwise have any questions
or feature requests please don't hesitate to file an issue at
https://github.com/rust-lang/cargo
",
)
.after_help("Run `cargo help fix` for more detailed information.\n")
}

pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
Expand Down
1 change: 1 addition & 0 deletions src/bin/cargo/commands/generate_lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub fn cli() -> App {
.about("Generate the lockfile for a package")
.arg(opt("quiet", "No output printed to stdout").short("q"))
.arg_manifest_path()
.after_help("Run `cargo help generate-lockfile` for more detailed information.\n")
}

pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
Expand Down
130 changes: 130 additions & 0 deletions src/bin/cargo/commands/help.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use crate::aliased_command;
use cargo::util::errors::CargoResult;
use cargo::util::paths::resolve_executable;
use cargo::Config;
use flate2::read::GzDecoder;
use std::ffi::OsString;
use std::io::Read;
use std::io::Write;
use std::path::Path;

const COMPRESSED_MAN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/man.tgz"));

/// Checks if the `help` command is being issued.
///
/// This runs before clap processing, because it needs to intercept the `help`
/// command if a man page is available.
///
/// Returns `true` if a man page was displayed. In this case, Cargo should
/// exit.
pub fn handle_embedded_help(config: &Config) -> bool {
match try_help(config) {
Ok(true) => true,
Ok(false) => false,
Err(e) => {
log::warn!("man failed: {:?}", e);
false
}
}
}

fn try_help(config: &Config) -> CargoResult<bool> {
let mut args = std::env::args_os()
.skip(1)
.skip_while(|arg| arg.to_str().map_or(false, |s| s.starts_with('-')));
if !args
.next()
.map_or(false, |arg| arg.to_str() == Some("help"))
{
return Ok(false);
}
let subcommand = match args.next() {
Some(arg) => arg,
None => return Ok(false),
};
let subcommand = match subcommand.to_str() {
Some(s) => s,
None => return Ok(false),
};
// Check if this is a built-in command (or alias);
let subcommand = match check_alias(config, subcommand) {
Some(s) => s,
None => return Ok(false),
};
if resolve_executable(Path::new("man")).is_ok() {
let man = match extract_man(&subcommand, "1") {
Some(man) => man,
None => return Ok(false),
};
write_and_spawn(&man, "man")?;
} else {
let txt = match extract_man(&subcommand, "txt") {
Some(txt) => txt,
None => return Ok(false),
};
if resolve_executable(Path::new("less")).is_ok() {
write_and_spawn(&txt, "less")?;
} else if resolve_executable(Path::new("more")).is_ok() {
write_and_spawn(&txt, "more")?;
} else {
drop(std::io::stdout().write_all(&txt));
}
}
Ok(true)
}

/// Checks if the given subcommand is a built-in command (possibly via an alias).
///
/// Returns None if it is not a built-in command.
fn check_alias(config: &Config, subcommand: &str) -> Option<String> {
if super::builtin_exec(subcommand).is_some() {
return Some(subcommand.to_string());
}
match aliased_command(config, subcommand) {
Ok(Some(alias)) => {
let alias = alias.into_iter().next()?;
if super::builtin_exec(&alias).is_some() {
Some(alias)
} else {
None
}
}
_ => None,
}
}

/// Extracts the given man page from the compressed archive.
///
/// Returns None if the command wasn't found.
fn extract_man(subcommand: &str, extension: &str) -> Option<Vec<u8>> {
let extract_name = OsString::from(format!("cargo-{}.{}", subcommand, extension));
let gz = GzDecoder::new(COMPRESSED_MAN);
let mut ar = tar::Archive::new(gz);
// Unwraps should be safe here, since this is a static archive generated
// by our build script. It should never be an invalid format!
for entry in ar.entries().unwrap() {
let mut entry = entry.unwrap();
let path = entry.path().unwrap();
if path.file_name().unwrap() != extract_name {
continue;
}
let mut result = Vec::new();
entry.read_to_end(&mut result).unwrap();
return Some(result);
}
None
}

/// Write the contents of a man page to disk and spawn the given command to
/// display it.
fn write_and_spawn(contents: &[u8], command: &str) -> CargoResult<()> {
let mut tmp = tempfile::Builder::new().prefix("cargo-man").tempfile()?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the tempfile required here? Programs like less I think we can just write everything to a pipe?

I'm not actually sure what other common CLI apps do here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, AFAIK, the man program does not support piped content.

let f = tmp.as_file_mut();
f.write_all(&contents)?;
f.flush()?;
let mut cmd = std::process::Command::new(command)
.arg(tmp.path())
.spawn()?;
drop(cmd.wait());
Ok(())
}
1 change: 1 addition & 0 deletions src/bin/cargo/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub fn cli() -> App {
.arg(Arg::with_name("path").default_value("."))
.arg(opt("registry", "Registry to use").value_name("REGISTRY"))
.arg_new_opts()
.after_help("Run `cargo help init` for more detailed information.\n")
}

pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
Expand Down
Loading