Skip to content

Commit ed08330

Browse files
authored
Merge pull request #1646 from yodaldevoid/shell-completions
Shell completions for Cargo
2 parents 88364b1 + 0312b11 commit ed08330

File tree

6 files changed

+273
-40
lines changed

6 files changed

+273
-40
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ but the gist is as simple as using one of the following:
6767

6868
```console
6969
# Bash
70-
$ rustup completions bash > /etc/bash_completion.d/rustup.bash-completion
70+
$ rustup completions bash > ~/.local/share/bash_completion/completions/rustup
7171

7272
# Bash (macOS/Homebrew)
7373
$ rustup completions bash > $(brew --prefix)/etc/bash_completion.d/rustup.bash-completion

src/cli/errors.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#![allow(dead_code)]
22

3+
use crate::rustup_mode::CompletionCommand;
4+
35
use std::io;
46
use std::path::PathBuf;
57

8+
use clap::Shell;
69
use error_chain::error_chain;
710
use error_chain::error_chain_processing;
811
use error_chain::{impl_error_chain_kind, impl_error_chain_processed, impl_extract_backtrace};
@@ -44,5 +47,9 @@ error_chain! {
4447
WindowsUninstallMadness {
4548
description("failure during windows uninstall")
4649
}
50+
UnsupportedCompletionShell(shell: Shell, cmd: CompletionCommand) {
51+
description("completion script for shell not yet supported for tool")
52+
display("{} does not currently support completions for {}", cmd, shell)
53+
}
4754
}
4855
}

src/cli/help.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,12 @@ r"DISCUSSION:
164164
165165
BASH:
166166
167-
Completion files are commonly stored in `/etc/bash_completion.d/`.
167+
Completion files are commonly stored in `/etc/bash_completion.d/` for
168+
system-wide commands, but can be stored in in
169+
`~/.local/share/bash_completion/completions` for user-specific commands.
168170
Run the command:
169171
170-
$ rustup completions bash > /etc/bash_completion.d/rustup.bash-completion
172+
$ rustup completions bash >> ~/.local/share/bash_completion/completions/rustup
171173
172174
This installs the completion script. You may have to log out and
173175
log back in to your shell session for the changes to take affect.
@@ -249,7 +251,22 @@ r"DISCUSSION:
249251
into a separate file and source it inside our profile. To save the
250252
completions into our profile simply use
251253
252-
PS C:\> rustup completions powershell >> ${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1";
254+
PS C:\> rustup completions powershell >> ${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
255+
256+
CARGO:
257+
258+
Rustup can also generate a completion script for `cargo`. The script output
259+
by `rustup` will source the completion script distributed with your default
260+
toolchain. Not all shells are currently supported. Here are examples for
261+
the currently supported shells.
262+
263+
BASH:
264+
265+
$ rustup completions bash cargo >> ~/.local/share/bash_completion/completions/cargo
266+
267+
ZSH:
268+
269+
$ rustup completions zsh cargo > ~/.zfunc/_cargo";
253270

254271
pub static TOOLCHAIN_ARG_HELP: &'static str = "Toolchain name, such as 'stable', 'nightly', \
255272
or '1.8.0'. For more information see `rustup \

src/cli/rustup_mode.rs

Lines changed: 168 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ use rustup::dist::manifest::Component;
99
use rustup::utils::utils::{self, ExitCode};
1010
use rustup::{command, Cfg, Toolchain};
1111
use std::error::Error;
12-
use std::io::{self, Write};
12+
use std::fmt;
13+
use std::io::Write;
1314
use std::iter;
1415
use std::path::Path;
1516
use std::process::{self, Command};
17+
use std::str::FromStr;
1618

1719
fn handle_epipe(res: Result<()>) -> Result<()> {
1820
match res {
@@ -85,11 +87,12 @@ pub fn main() -> Result<()> {
8587
},
8688
("completions", Some(c)) => {
8789
if let Some(shell) = c.value_of("shell") {
88-
cli().gen_completions_to(
89-
"rustup",
90+
output_completion_script(
9091
shell.parse::<Shell>().unwrap(),
91-
&mut io::stdout(),
92-
);
92+
c.value_of("command")
93+
.and_then(|cmd| cmd.parse::<CompletionCommand>().ok())
94+
.unwrap_or(CompletionCommand::Rustup),
95+
)?;
9396
}
9497
}
9598
(_, _) => unreachable!(),
@@ -442,40 +445,98 @@ pub fn cli() -> App<'static, 'static> {
442445
);
443446
}
444447

448+
app = app
449+
.subcommand(
450+
SubCommand::with_name("self")
451+
.about("Modify the rustup installation")
452+
.setting(AppSettings::VersionlessSubcommands)
453+
.setting(AppSettings::DeriveDisplayOrder)
454+
.setting(AppSettings::SubcommandRequiredElseHelp)
455+
.subcommand(
456+
SubCommand::with_name("update").about("Download and install updates to rustup"),
457+
)
458+
.subcommand(
459+
SubCommand::with_name("uninstall")
460+
.about("Uninstall rustup.")
461+
.arg(Arg::with_name("no-prompt").short("y")),
462+
)
463+
.subcommand(
464+
SubCommand::with_name("upgrade-data")
465+
.about("Upgrade the internal data format."),
466+
),
467+
)
468+
.subcommand(
469+
SubCommand::with_name("set")
470+
.about("Alter rustup settings")
471+
.setting(AppSettings::SubcommandRequiredElseHelp)
472+
.subcommand(
473+
SubCommand::with_name("default-host")
474+
.about("The triple used to identify toolchains when not specified")
475+
.arg(Arg::with_name("host_triple").required(true)),
476+
),
477+
)
478+
.subcommand(
479+
SubCommand::with_name("self")
480+
.about("Modify the rustup installation")
481+
.setting(AppSettings::VersionlessSubcommands)
482+
.setting(AppSettings::DeriveDisplayOrder)
483+
.setting(AppSettings::SubcommandRequiredElseHelp)
484+
.subcommand(
485+
SubCommand::with_name("update").about("Download and install updates to rustup"),
486+
)
487+
.subcommand(
488+
SubCommand::with_name("uninstall")
489+
.about("Uninstall rustup.")
490+
.arg(Arg::with_name("no-prompt").short("y")),
491+
)
492+
.subcommand(
493+
SubCommand::with_name("upgrade-data")
494+
.about("Upgrade the internal data format."),
495+
),
496+
)
497+
.subcommand(
498+
SubCommand::with_name("telemetry")
499+
.about("rustup telemetry commands")
500+
.setting(AppSettings::Hidden)
501+
.setting(AppSettings::VersionlessSubcommands)
502+
.setting(AppSettings::DeriveDisplayOrder)
503+
.setting(AppSettings::SubcommandRequiredElseHelp)
504+
.subcommand(SubCommand::with_name("enable").about("Enable rustup telemetry"))
505+
.subcommand(SubCommand::with_name("disable").about("Disable rustup telemetry"))
506+
.subcommand(SubCommand::with_name("analyze").about("Analyze stored telemetry")),
507+
)
508+
.subcommand(
509+
SubCommand::with_name("set")
510+
.about("Alter rustup settings")
511+
.setting(AppSettings::SubcommandRequiredElseHelp)
512+
.subcommand(
513+
SubCommand::with_name("default-host")
514+
.about("The triple used to identify toolchains when not specified")
515+
.arg(Arg::with_name("host_triple").required(true)),
516+
),
517+
);
518+
519+
// Clap provides no good way to say that help should be printed in all
520+
// cases where an argument without a default is not provided. The following
521+
// creates lists out all the conditions where the "shell" argument are
522+
// provided and give the default of "rustup". This way if "shell" is not
523+
// provided then the help will still be printed.
524+
let completion_defaults = Shell::variants()
525+
.iter()
526+
.map(|&shell| ("shell", Some(shell), "rustup"))
527+
.collect::<Vec<_>>();
528+
445529
app.subcommand(
446-
SubCommand::with_name("self")
447-
.about("Modify the rustup installation")
448-
.setting(AppSettings::VersionlessSubcommands)
449-
.setting(AppSettings::DeriveDisplayOrder)
450-
.setting(AppSettings::SubcommandRequiredElseHelp)
451-
.subcommand(
452-
SubCommand::with_name("update").about("Download and install updates to rustup"),
453-
)
454-
.subcommand(
455-
SubCommand::with_name("uninstall")
456-
.about("Uninstall rustup.")
457-
.arg(Arg::with_name("no-prompt").short("y")),
458-
)
459-
.subcommand(
460-
SubCommand::with_name("upgrade-data").about("Upgrade the internal data format."),
461-
),
462-
)
463-
.subcommand(
464-
SubCommand::with_name("set")
465-
.about("Alter rustup settings")
466-
.setting(AppSettings::SubcommandRequiredElseHelp)
467-
.subcommand(
468-
SubCommand::with_name("default-host")
469-
.about("The triple used to identify toolchains when not specified")
470-
.arg(Arg::with_name("host_triple").required(true)),
471-
),
472-
)
473-
.subcommand(
474530
SubCommand::with_name("completions")
475531
.about("Generate completion scripts for your shell")
476532
.after_help(COMPLETIONS_HELP)
477533
.setting(AppSettings::ArgRequiredElseHelp)
478-
.arg(Arg::with_name("shell").possible_values(&Shell::variants())),
534+
.arg(Arg::with_name("shell").possible_values(&Shell::variants()))
535+
.arg(
536+
Arg::with_name("command")
537+
.possible_values(&CompletionCommand::variants())
538+
.default_value_ifs(&completion_defaults[..]),
539+
),
479540
)
480541
}
481542

@@ -1056,3 +1117,76 @@ fn set_default_host_triple(cfg: &Cfg, m: &ArgMatches<'_>) -> Result<()> {
10561117
cfg.set_default_host_triple(m.value_of("host_triple").expect(""))?;
10571118
Ok(())
10581119
}
1120+
1121+
#[derive(Copy, Clone, Debug, PartialEq)]
1122+
pub enum CompletionCommand {
1123+
Rustup,
1124+
Cargo,
1125+
}
1126+
1127+
static COMPLETIONS: &[(&'static str, CompletionCommand)] = &[
1128+
("rustup", CompletionCommand::Rustup),
1129+
("cargo", CompletionCommand::Cargo),
1130+
];
1131+
1132+
impl CompletionCommand {
1133+
fn variants() -> Vec<&'static str> {
1134+
COMPLETIONS.iter().map(|&(s, _)| s).collect::<Vec<_>>()
1135+
}
1136+
}
1137+
1138+
impl FromStr for CompletionCommand {
1139+
type Err = String;
1140+
1141+
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
1142+
match COMPLETIONS
1143+
.iter()
1144+
.filter(|&(val, _)| val.eq_ignore_ascii_case(s))
1145+
.next()
1146+
{
1147+
Some(&(_, cmd)) => Ok(cmd),
1148+
None => {
1149+
let completion_options = COMPLETIONS
1150+
.iter()
1151+
.map(|&(v, _)| v)
1152+
.fold("".to_owned(), |s, v| format!("{}{}, ", s, v));
1153+
Err(format!(
1154+
"[valid values: {}]",
1155+
completion_options.trim_end_matches(", ")
1156+
))
1157+
}
1158+
}
1159+
}
1160+
}
1161+
1162+
impl fmt::Display for CompletionCommand {
1163+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1164+
match COMPLETIONS.iter().filter(|&(_, cmd)| cmd == self).next() {
1165+
Some(&(val, _)) => write!(f, "{}", val),
1166+
None => unreachable!(),
1167+
}
1168+
}
1169+
}
1170+
1171+
fn output_completion_script(shell: Shell, command: CompletionCommand) -> Result<()> {
1172+
match command {
1173+
CompletionCommand::Rustup => {
1174+
cli().gen_completions_to("rustup", shell, &mut term2::stdout());
1175+
}
1176+
CompletionCommand::Cargo => {
1177+
let script = match shell {
1178+
Shell::Bash => "/etc/bash_completion.d/cargo",
1179+
Shell::Zsh => "/share/zsh/site-functions/_cargo",
1180+
_ => return Err(ErrorKind::UnsupportedCompletionShell(shell, command).into()),
1181+
};
1182+
1183+
writeln!(
1184+
&mut term2::stdout(),
1185+
"source $(rustc --print sysroot){}",
1186+
script,
1187+
)?;
1188+
}
1189+
}
1190+
1191+
Ok(())
1192+
}

tests/cli-misc.rs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
pub mod mock;
55

66
use crate::mock::clitools::{
7-
self, expect_err, expect_ok, expect_ok_ex, expect_stderr_ok, expect_stdout_ok, run,
8-
set_current_dist_date, this_host_triple, Config, Scenario,
7+
self, expect_err, expect_ok, expect_ok_eq, expect_ok_ex, expect_stderr_ok, expect_stdout_ok,
8+
run, set_current_dist_date, this_host_triple, Config, Scenario,
99
};
1010
use rustup::errors::TOOLSTATE_MSG;
1111
use rustup::utils::{raw, utils};
@@ -697,3 +697,66 @@ fn update_unavailable_rustc() {
697697
expect_stdout_ok(config, &["rustc", "--version"], "hash-n-1");
698698
});
699699
}
700+
701+
#[test]
702+
fn completion_rustup() {
703+
setup(&|config| {
704+
expect_ok(config, &["rustup", "completions", "bash", "rustup"]);
705+
});
706+
}
707+
708+
#[test]
709+
fn completion_cargo() {
710+
setup(&|config| {
711+
expect_ok(config, &["rustup", "completions", "bash", "cargo"]);
712+
});
713+
}
714+
715+
#[test]
716+
fn completion_default() {
717+
setup(&|config| {
718+
expect_ok_eq(
719+
config,
720+
&["rustup", "completions", "bash"],
721+
&["rustup", "completions", "bash", "rustup"],
722+
);
723+
});
724+
}
725+
726+
#[test]
727+
fn completion_bad_shell() {
728+
setup(&|config| {
729+
expect_err(
730+
config,
731+
&["rustup", "completions", "fake"],
732+
"error: 'fake' isn't a valid value for '<shell>'",
733+
);
734+
expect_err(
735+
config,
736+
&["rustup", "completions", "fake", "cargo"],
737+
"error: 'fake' isn't a valid value for '<shell>'",
738+
);
739+
});
740+
}
741+
742+
#[test]
743+
fn completion_bad_tool() {
744+
setup(&|config| {
745+
expect_err(
746+
config,
747+
&["rustup", "completions", "bash", "fake"],
748+
"error: 'fake' isn't a valid value for '<command>'",
749+
);
750+
});
751+
}
752+
753+
#[test]
754+
fn completion_cargo_unsupported_shell() {
755+
setup(&|config| {
756+
expect_err(
757+
config,
758+
&["rustup", "completions", "fish", "cargo"],
759+
"error: cargo does not currently support completions for ",
760+
);
761+
});
762+
}

0 commit comments

Comments
 (0)