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

Shell completions for Cargo #1646

Merged
merged 13 commits into from
Apr 14, 2019
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ but the gist is as simple as using one of the following:

```
# Bash
$ rustup completions bash > /etc/bash_completion.d/rustup.bash-completion
$ rustup completions bash > ~/.local/share/bash_completion/completions/rustup
kinnison marked this conversation as resolved.
Show resolved Hide resolved

# Bash (macOS/Homebrew)
$ rustup completions bash > $(brew --prefix)/etc/bash_completion.d/rustup.bash-completion
Expand Down
7 changes: 7 additions & 0 deletions src/cli/errors.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#![allow(dead_code)]

use crate::rustup_mode::CompletionCommand;

use std::io;
use std::path::PathBuf;

use clap::Shell;
use error_chain::error_chain;
use error_chain::error_chain_processing;
use error_chain::{impl_error_chain_kind, impl_error_chain_processed, impl_extract_backtrace};
Expand Down Expand Up @@ -46,5 +49,9 @@ error_chain! {
WindowsUninstallMadness {
description("failure during windows uninstall")
}
UnsupportedCompletionShell(shell: Shell, cmd: CompletionCommand) {
description("completion script for shell not yet supported for tool")
display("{} does not currently support completions for {}", cmd, shell)
}
}
}
23 changes: 20 additions & 3 deletions src/cli/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,12 @@ r"DISCUSSION:
BASH:
Completion files are commonly stored in `/etc/bash_completion.d/`.
Completion files are commonly stored in `/etc/bash_completion.d/` for
system-wide commands, but can be stored in in
`~/.local/share/bash_completion/completions` for user-specific commands.
Run the command:
$ rustup completions bash > /etc/bash_completion.d/rustup.bash-completion
$ rustup completions bash >> ~/.local/share/bash_completion/completions/rustup
This installs the completion script. You may have to log out and
log back in to your shell session for the changes to take affect.
Expand Down Expand Up @@ -249,7 +251,22 @@ r"DISCUSSION:
into a separate file and source it inside our profile. To save the
completions into our profile simply use
PS C:\> rustup completions powershell >> ${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1";
PS C:\> rustup completions powershell >> ${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
CARGO:
Rustup can also generate a completion script for `cargo`. The script output
by `rustup` will source the completion script distributed with your default
toolchain. Not all shells are currently supported. Here are examples for
the currently supported shells.
BASH:
$ rustup completions bash cargo >> ~/.local/share/bash_completion/completions/cargo
ZSH:
$ rustup completions zsh cargo > ~/.zfunc/_cargo";

pub static TOOLCHAIN_ARG_HELP: &'static str = "Toolchain name, such as 'stable', 'nightly', \
or '1.8.0'. For more information see `rustup \
Expand Down
202 changes: 168 additions & 34 deletions src/cli/rustup_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ use rustup::dist::manifest::Component;
use rustup::utils::utils::{self, ExitCode};
use rustup::{command, Cfg, Toolchain};
use std::error::Error;
use std::io::{self, Write};
use std::fmt;
use std::io::Write;
use std::iter;
use std::path::Path;
use std::process::{self, Command};
use std::str::FromStr;

fn handle_epipe(res: Result<()>) -> Result<()> {
match res {
Expand Down Expand Up @@ -85,11 +87,12 @@ pub fn main() -> Result<()> {
},
("completions", Some(c)) => {
if let Some(shell) = c.value_of("shell") {
cli().gen_completions_to(
"rustup",
output_completion_script(
shell.parse::<Shell>().unwrap(),
&mut io::stdout(),
);
c.value_of("command")
.and_then(|cmd| cmd.parse::<CompletionCommand>().ok())
.unwrap_or(CompletionCommand::Rustup),
)?;
}
}
(_, _) => unreachable!(),
Expand Down Expand Up @@ -442,40 +445,98 @@ pub fn cli() -> App<'static, 'static> {
);
}

app = app
.subcommand(
SubCommand::with_name("self")
.about("Modify the rustup installation")
.setting(AppSettings::VersionlessSubcommands)
.setting(AppSettings::DeriveDisplayOrder)
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("update").about("Download and install updates to rustup"),
)
.subcommand(
SubCommand::with_name("uninstall")
.about("Uninstall rustup.")
.arg(Arg::with_name("no-prompt").short("y")),
)
.subcommand(
SubCommand::with_name("upgrade-data")
.about("Upgrade the internal data format."),
),
)
.subcommand(
SubCommand::with_name("set")
.about("Alter rustup settings")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("default-host")
.about("The triple used to identify toolchains when not specified")
.arg(Arg::with_name("host_triple").required(true)),
),
)
.subcommand(
SubCommand::with_name("self")
.about("Modify the rustup installation")
.setting(AppSettings::VersionlessSubcommands)
.setting(AppSettings::DeriveDisplayOrder)
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("update").about("Download and install updates to rustup"),
)
.subcommand(
SubCommand::with_name("uninstall")
.about("Uninstall rustup.")
.arg(Arg::with_name("no-prompt").short("y")),
)
.subcommand(
SubCommand::with_name("upgrade-data")
.about("Upgrade the internal data format."),
),
)
.subcommand(
SubCommand::with_name("telemetry")
.about("rustup telemetry commands")
.setting(AppSettings::Hidden)
.setting(AppSettings::VersionlessSubcommands)
.setting(AppSettings::DeriveDisplayOrder)
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(SubCommand::with_name("enable").about("Enable rustup telemetry"))
.subcommand(SubCommand::with_name("disable").about("Disable rustup telemetry"))
.subcommand(SubCommand::with_name("analyze").about("Analyze stored telemetry")),
)
.subcommand(
SubCommand::with_name("set")
.about("Alter rustup settings")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("default-host")
.about("The triple used to identify toolchains when not specified")
.arg(Arg::with_name("host_triple").required(true)),
),
);

// Clap provides no good way to say that help should be printed in all
// cases where an argument without a default is not provided. The following
// creates lists out all the conditions where the "shell" argument are
// provided and give the default of "rustup". This way if "shell" is not
// provided then the help will still be printed.
let completion_defaults = Shell::variants()
.iter()
.map(|&shell| ("shell", Some(shell), "rustup"))
.collect::<Vec<_>>();

app.subcommand(
SubCommand::with_name("self")
.about("Modify the rustup installation")
.setting(AppSettings::VersionlessSubcommands)
.setting(AppSettings::DeriveDisplayOrder)
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("update").about("Download and install updates to rustup"),
)
.subcommand(
SubCommand::with_name("uninstall")
.about("Uninstall rustup.")
.arg(Arg::with_name("no-prompt").short("y")),
)
.subcommand(
SubCommand::with_name("upgrade-data").about("Upgrade the internal data format."),
),
)
.subcommand(
SubCommand::with_name("set")
.about("Alter rustup settings")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("default-host")
.about("The triple used to identify toolchains when not specified")
.arg(Arg::with_name("host_triple").required(true)),
),
)
.subcommand(
SubCommand::with_name("completions")
.about("Generate completion scripts for your shell")
.after_help(COMPLETIONS_HELP)
.setting(AppSettings::ArgRequiredElseHelp)
.arg(Arg::with_name("shell").possible_values(&Shell::variants())),
.arg(Arg::with_name("shell").possible_values(&Shell::variants()))
.arg(
Arg::with_name("command")
.possible_values(&CompletionCommand::variants())
.default_value_ifs(&completion_defaults[..]),
),
)
}

Expand Down Expand Up @@ -1050,3 +1111,76 @@ fn set_default_host_triple(cfg: &Cfg, m: &ArgMatches<'_>) -> Result<()> {
cfg.set_default_host_triple(m.value_of("host_triple").expect(""))?;
Ok(())
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum CompletionCommand {
Rustup,
Cargo,
}

static COMPLETIONS: &[(&'static str, CompletionCommand)] = &[
("rustup", CompletionCommand::Rustup),
("cargo", CompletionCommand::Cargo),
];

impl CompletionCommand {
fn variants() -> Vec<&'static str> {
COMPLETIONS.iter().map(|&(s, _)| s).collect::<Vec<_>>()
}
}

impl FromStr for CompletionCommand {
type Err = String;

fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match COMPLETIONS
.iter()
.filter(|&(val, _)| val.eq_ignore_ascii_case(s))
.next()
{
Some(&(_, cmd)) => Ok(cmd),
None => {
let completion_options = COMPLETIONS
.iter()
.map(|&(v, _)| v)
.fold("".to_owned(), |s, v| format!("{}{}, ", s, v));
Err(format!(
"[valid values: {}]",
completion_options.trim_end_matches(", ")
))
}
}
}
}

impl fmt::Display for CompletionCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match COMPLETIONS.iter().filter(|&(_, cmd)| cmd == self).next() {
Some(&(val, _)) => write!(f, "{}", val),
None => unreachable!(),
}
}
}

fn output_completion_script(shell: Shell, command: CompletionCommand) -> Result<()> {
match command {
CompletionCommand::Rustup => {
cli().gen_completions_to("rustup", shell, &mut term2::stdout());
}
CompletionCommand::Cargo => {
let script = match shell {
Shell::Bash => "/etc/bash_completion.d/cargo",
Shell::Zsh => "/share/zsh/site-functions/_cargo",
_ => return Err(ErrorKind::UnsupportedCompletionShell(shell, command).into()),
};

writeln!(
&mut term2::stdout(),
"source $(rustc --print sysroot){}",
script,
)?;
}
}

Ok(())
}
67 changes: 65 additions & 2 deletions tests/cli-misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
pub mod mock;

use crate::mock::clitools::{
self, expect_err, expect_ok, expect_ok_ex, expect_stderr_ok, expect_stdout_ok, run,
set_current_dist_date, this_host_triple, Config, Scenario,
self, expect_err, expect_ok, expect_ok_eq, expect_ok_ex, expect_stderr_ok, expect_stdout_ok,
run, set_current_dist_date, this_host_triple, Config, Scenario,
};
use rustup::dist::errors::TOOLSTATE_MSG;
use rustup::utils::{raw, utils};
Expand Down Expand Up @@ -789,3 +789,66 @@ fn update_unavailable_rustc() {
expect_stdout_ok(config, &["rustc", "--version"], "hash-n-1");
});
}

#[test]
fn completion_rustup() {
setup(&|config| {
expect_ok(config, &["rustup", "completions", "bash", "rustup"]);
});
}

#[test]
fn completion_cargo() {
setup(&|config| {
expect_ok(config, &["rustup", "completions", "bash", "cargo"]);
});
}

#[test]
fn completion_default() {
setup(&|config| {
expect_ok_eq(
config,
&["rustup", "completions", "bash"],
&["rustup", "completions", "bash", "rustup"],
);
});
}

#[test]
fn completion_bad_shell() {
setup(&|config| {
expect_err(
config,
&["rustup", "completions", "fake"],
"error: 'fake' isn't a valid value for '<shell>'",
);
expect_err(
config,
&["rustup", "completions", "fake", "cargo"],
"error: 'fake' isn't a valid value for '<shell>'",
);
});
}

#[test]
fn completion_bad_tool() {
setup(&|config| {
expect_err(
config,
&["rustup", "completions", "bash", "fake"],
"error: 'fake' isn't a valid value for '<command>'",
);
});
}

#[test]
fn completion_cargo_unsupported_shell() {
setup(&|config| {
expect_err(
config,
&["rustup", "completions", "fish", "cargo"],
"error: cargo does not currently support completions for ",
);
});
}
Loading