Generate shell completion for uvx (#7388)

## Summary

Generate shell completion for uvx.

Create a `uvx` toplevel command just for completion by combining `uv
tool uvx` (hidden alias for `uv tool run`) with global arguments. This
explicit combination is needed otherwise global arguments are missing
(if they are missing, clap debug assertions fail when `uv tool run`
arguments refer to global arguments in directives like conflicts with).


Fixes #7258 

## Test Plan

- Tested using bash using `eval "$(cargo run --bin uv
generate-shell-completion bash)"`
This commit is contained in:
bluss 2024-09-17 05:27:19 +02:00 committed by GitHub
parent d1c7cb8bc2
commit e9378be919
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 62 additions and 13 deletions

View file

@ -76,6 +76,13 @@ pub struct Cli {
#[command(subcommand)] #[command(subcommand)]
pub command: Box<Commands>, pub command: Box<Commands>,
#[command(flatten)]
pub top_level: TopLevelArgs,
}
#[derive(Parser)]
#[command(disable_help_flag = true, disable_version_flag = true)]
pub struct TopLevelArgs {
#[command(flatten)] #[command(flatten)]
pub cache_args: Box<CacheArgs>, pub cache_args: Box<CacheArgs>,

View file

@ -16,7 +16,7 @@ use uv_cli::{
compat::CompatArgs, CacheCommand, CacheNamespace, Cli, Commands, PipCommand, PipNamespace, compat::CompatArgs, CacheCommand, CacheNamespace, Cli, Commands, PipCommand, PipNamespace,
ProjectCommand, ProjectCommand,
}; };
use uv_cli::{PythonCommand, PythonNamespace, ToolCommand, ToolNamespace}; use uv_cli::{PythonCommand, PythonNamespace, ToolCommand, ToolNamespace, TopLevelArgs};
#[cfg(feature = "self-update")] #[cfg(feature = "self-update")]
use uv_cli::{SelfCommand, SelfNamespace, SelfUpdateArgs}; use uv_cli::{SelfCommand, SelfNamespace, SelfUpdateArgs};
use uv_fs::CWD; use uv_fs::CWD;
@ -58,17 +58,17 @@ pub(crate) mod version;
#[instrument(skip_all)] #[instrument(skip_all)]
async fn run(cli: Cli) -> Result<ExitStatus> { async fn run(cli: Cli) -> Result<ExitStatus> {
// Enable flag to pick up warnings generated by workspace loading. // Enable flag to pick up warnings generated by workspace loading.
if !cli.global_args.quiet { if !cli.top_level.global_args.quiet {
uv_warnings::enable(); uv_warnings::enable();
} }
// Switch directories as early as possible. // Switch directories as early as possible.
if let Some(directory) = cli.global_args.directory.as_ref() { if let Some(directory) = cli.top_level.global_args.directory.as_ref() {
std::env::set_current_dir(directory)?; std::env::set_current_dir(directory)?;
} }
// The `--isolated` argument is deprecated on preview APIs, and warns on non-preview APIs. // The `--isolated` argument is deprecated on preview APIs, and warns on non-preview APIs.
let deprecated_isolated = if cli.global_args.isolated { let deprecated_isolated = if cli.top_level.global_args.isolated {
match &*cli.command { match &*cli.command {
// Supports `--isolated` as its own argument, so we can't warn either way. // Supports `--isolated` as its own argument, so we can't warn either way.
Commands::Tool(ToolNamespace { Commands::Tool(ToolNamespace {
@ -106,7 +106,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
// If found, this file is combined with the user configuration file. // If found, this file is combined with the user configuration file.
// 3. The nearest configuration file (`uv.toml` or `pyproject.toml`) in the directory tree, // 3. The nearest configuration file (`uv.toml` or `pyproject.toml`) in the directory tree,
// starting from the current directory. // starting from the current directory.
let filesystem = if let Some(config_file) = cli.config_file.as_ref() { let filesystem = if let Some(config_file) = cli.top_level.config_file.as_ref() {
if config_file if config_file
.file_name() .file_name()
.is_some_and(|file_name| file_name == "pyproject.toml") .is_some_and(|file_name| file_name == "pyproject.toml")
@ -114,7 +114,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
warn_user!("The `--config-file` argument expects to receive a `uv.toml` file, not a `pyproject.toml`. If you're trying to run a command from another project, use the `--directory` argument instead."); warn_user!("The `--config-file` argument expects to receive a `uv.toml` file, not a `pyproject.toml`. If you're trying to run a command from another project, use the `--directory` argument instead.");
} }
Some(FilesystemOptions::from_file(config_file)?) Some(FilesystemOptions::from_file(config_file)?)
} else if deprecated_isolated || cli.no_config { } else if deprecated_isolated || cli.top_level.no_config {
None None
} else if matches!(&*cli.command, Commands::Tool(_)) { } else if matches!(&*cli.command, Commands::Tool(_)) {
// For commands that operate at the user-level, ignore local configuration. // For commands that operate at the user-level, ignore local configuration.
@ -175,10 +175,10 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
.combine(filesystem); .combine(filesystem);
// Resolve the global settings. // Resolve the global settings.
let globals = GlobalSettings::resolve(&cli.global_args, filesystem.as_ref()); let globals = GlobalSettings::resolve(&cli.top_level.global_args, filesystem.as_ref());
// Resolve the cache settings. // Resolve the cache settings.
let cache_settings = CacheSettings::resolve(*cli.cache_args, filesystem.as_ref()); let cache_settings = CacheSettings::resolve(*cli.top_level.cache_args, filesystem.as_ref());
// Configure the `tracing` crate, which controls internal logging. // Configure the `tracing` crate, which controls internal logging.
#[cfg(feature = "tracing-durations-export")] #[cfg(feature = "tracing-durations-export")]
@ -687,7 +687,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
args.hash_checking, args.hash_checking,
args.python, args.python,
args.settings, args.settings,
cli.no_config, cli.top_level.no_config,
globals.python_preference, globals.python_preference,
globals.python_downloads, globals.python_downloads,
globals.connectivity, globals.connectivity,
@ -743,7 +743,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
args.settings.exclude_newer, args.settings.exclude_newer,
globals.concurrency, globals.concurrency,
globals.native_tls, globals.native_tls,
cli.no_config, cli.top_level.no_config,
args.no_project, args.no_project,
&cache, &cache,
printer, printer,
@ -757,7 +757,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
run_command, run_command,
script, script,
globals, globals,
cli.no_config, cli.top_level.no_config,
filesystem, filesystem,
cache, cache,
printer, printer,
@ -777,7 +777,30 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
Commands::GenerateShellCompletion(args) => { Commands::GenerateShellCompletion(args) => {
// uv
args.shell.generate(&mut Cli::command(), &mut stdout()); args.shell.generate(&mut Cli::command(), &mut stdout());
// uvx: combine `uv tool uvx` with the top-level arguments
let mut uvx = Cli::command()
.find_subcommand("tool")
.unwrap()
.find_subcommand("uvx")
.unwrap()
.clone()
// Avoid duplicating the `--help` and `--version` flags from the top-level arguments.
.disable_help_flag(true)
.disable_version_flag(true)
.version(env!("CARGO_PKG_VERSION"));
// Copy the top-level arguments into the `uvx` command. (Like `Args::augment_args`, but
// expanded to skip collisions.)
for arg in TopLevelArgs::command().get_arguments() {
if arg.get_id() != "isolated" {
uvx = uvx.arg(arg);
}
}
args.shell.generate(&mut uvx, &mut stdout());
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
Commands::Tool(ToolNamespace { Commands::Tool(ToolNamespace {
@ -974,7 +997,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
globals.python_downloads, globals.python_downloads,
globals.native_tls, globals.native_tls,
globals.connectivity, globals.connectivity,
cli.no_config, cli.top_level.no_config,
printer, printer,
) )
.await .await
@ -1000,7 +1023,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
commands::python_find( commands::python_find(
args.request, args.request,
args.no_project, args.no_project,
cli.no_config, cli.top_level.no_config,
args.system, args.system,
globals.python_preference, globals.python_preference,
&cache, &cache,

View file

@ -181,6 +181,25 @@ To enable shell autocompletion for uv commands, run one of the following:
Then restart the shell or source the shell config file. Then restart the shell or source the shell config file.
You can also enable shell autocompletion for uvx by running the same commands, replacing `uv` with
`uvx`:
=== "Linux and macOS"
```bash
# Determine your shell (e.g., with `echo $SHELL`), then run one of:
echo 'eval "$(uvx generate-shell-completion bash)"' >> ~/.bashrc
echo 'eval "$(uvx generate-shell-completion zsh)"' >> ~/.zshrc
echo 'uvx generate-shell-completion fish | source' >> ~/.config/fish/config.fish
echo 'eval (uvx generate-shell-completion elvish | slurp)' >> ~/.elvish/rc.elv
```
=== "Windows"
```powershell
Add-Content -Path $PROFILE -Value '(& uvx generate-shell-completion powershell) | Out-String | Invoke-Expression'
```
## Uninstallation ## Uninstallation
If you need to remove uv from your system, just remove the `uv` and `uvx` binaries: If you need to remove uv from your system, just remove the `uv` and `uvx` binaries: