Skip to content

Commit

Permalink
Merge pull request #1646 from yodaldevoid/shell-completions
Browse files Browse the repository at this point in the history
Shell completions for Cargo
  • Loading branch information
kinnison committed Apr 14, 2019
2 parents 88364b1 + 0312b11 commit ed08330
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 40 deletions.
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:

```console
# Bash
$ rustup completions bash > /etc/bash_completion.d/rustup.bash-completion
$ rustup completions bash > ~/.local/share/bash_completion/completions/rustup

# 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 @@ -44,5 +47,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 @@ -1056,3 +1117,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::errors::TOOLSTATE_MSG;
use rustup::utils::{raw, utils};
Expand Down Expand Up @@ -697,3 +697,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

0 comments on commit ed08330

Please sign in to comment.