Skip to content

Commit e9378be

Browse files
authored
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)"`
1 parent d1c7cb8 commit e9378be

File tree

3 files changed

+62
-13
lines changed

3 files changed

+62
-13
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ pub struct Cli {
7676
#[command(subcommand)]
7777
pub command: Box<Commands>,
7878

79+
#[command(flatten)]
80+
pub top_level: TopLevelArgs,
81+
}
82+
83+
#[derive(Parser)]
84+
#[command(disable_help_flag = true, disable_version_flag = true)]
85+
pub struct TopLevelArgs {
7986
#[command(flatten)]
8087
pub cache_args: Box<CacheArgs>,
8188

crates/uv/src/lib.rs

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use uv_cli::{
1616
compat::CompatArgs, CacheCommand, CacheNamespace, Cli, Commands, PipCommand, PipNamespace,
1717
ProjectCommand,
1818
};
19-
use uv_cli::{PythonCommand, PythonNamespace, ToolCommand, ToolNamespace};
19+
use uv_cli::{PythonCommand, PythonNamespace, ToolCommand, ToolNamespace, TopLevelArgs};
2020
#[cfg(feature = "self-update")]
2121
use uv_cli::{SelfCommand, SelfNamespace, SelfUpdateArgs};
2222
use uv_fs::CWD;
@@ -58,17 +58,17 @@ pub(crate) mod version;
5858
#[instrument(skip_all)]
5959
async fn run(cli: Cli) -> Result<ExitStatus> {
6060
// Enable flag to pick up warnings generated by workspace loading.
61-
if !cli.global_args.quiet {
61+
if !cli.top_level.global_args.quiet {
6262
uv_warnings::enable();
6363
}
6464

6565
// Switch directories as early as possible.
66-
if let Some(directory) = cli.global_args.directory.as_ref() {
66+
if let Some(directory) = cli.top_level.global_args.directory.as_ref() {
6767
std::env::set_current_dir(directory)?;
6868
}
6969

7070
// The `--isolated` argument is deprecated on preview APIs, and warns on non-preview APIs.
71-
let deprecated_isolated = if cli.global_args.isolated {
71+
let deprecated_isolated = if cli.top_level.global_args.isolated {
7272
match &*cli.command {
7373
// Supports `--isolated` as its own argument, so we can't warn either way.
7474
Commands::Tool(ToolNamespace {
@@ -106,15 +106,15 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
106106
// If found, this file is combined with the user configuration file.
107107
// 3. The nearest configuration file (`uv.toml` or `pyproject.toml`) in the directory tree,
108108
// starting from the current directory.
109-
let filesystem = if let Some(config_file) = cli.config_file.as_ref() {
109+
let filesystem = if let Some(config_file) = cli.top_level.config_file.as_ref() {
110110
if config_file
111111
.file_name()
112112
.is_some_and(|file_name| file_name == "pyproject.toml")
113113
{
114114
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.");
115115
}
116116
Some(FilesystemOptions::from_file(config_file)?)
117-
} else if deprecated_isolated || cli.no_config {
117+
} else if deprecated_isolated || cli.top_level.no_config {
118118
None
119119
} else if matches!(&*cli.command, Commands::Tool(_)) {
120120
// For commands that operate at the user-level, ignore local configuration.
@@ -175,10 +175,10 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
175175
.combine(filesystem);
176176

177177
// Resolve the global settings.
178-
let globals = GlobalSettings::resolve(&cli.global_args, filesystem.as_ref());
178+
let globals = GlobalSettings::resolve(&cli.top_level.global_args, filesystem.as_ref());
179179

180180
// Resolve the cache settings.
181-
let cache_settings = CacheSettings::resolve(*cli.cache_args, filesystem.as_ref());
181+
let cache_settings = CacheSettings::resolve(*cli.top_level.cache_args, filesystem.as_ref());
182182

183183
// Configure the `tracing` crate, which controls internal logging.
184184
#[cfg(feature = "tracing-durations-export")]
@@ -687,7 +687,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
687687
args.hash_checking,
688688
args.python,
689689
args.settings,
690-
cli.no_config,
690+
cli.top_level.no_config,
691691
globals.python_preference,
692692
globals.python_downloads,
693693
globals.connectivity,
@@ -743,7 +743,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
743743
args.settings.exclude_newer,
744744
globals.concurrency,
745745
globals.native_tls,
746-
cli.no_config,
746+
cli.top_level.no_config,
747747
args.no_project,
748748
&cache,
749749
printer,
@@ -757,7 +757,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
757757
run_command,
758758
script,
759759
globals,
760-
cli.no_config,
760+
cli.top_level.no_config,
761761
filesystem,
762762
cache,
763763
printer,
@@ -777,7 +777,30 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
777777
Ok(ExitStatus::Success)
778778
}
779779
Commands::GenerateShellCompletion(args) => {
780+
// uv
780781
args.shell.generate(&mut Cli::command(), &mut stdout());
782+
783+
// uvx: combine `uv tool uvx` with the top-level arguments
784+
let mut uvx = Cli::command()
785+
.find_subcommand("tool")
786+
.unwrap()
787+
.find_subcommand("uvx")
788+
.unwrap()
789+
.clone()
790+
// Avoid duplicating the `--help` and `--version` flags from the top-level arguments.
791+
.disable_help_flag(true)
792+
.disable_version_flag(true)
793+
.version(env!("CARGO_PKG_VERSION"));
794+
795+
// Copy the top-level arguments into the `uvx` command. (Like `Args::augment_args`, but
796+
// expanded to skip collisions.)
797+
for arg in TopLevelArgs::command().get_arguments() {
798+
if arg.get_id() != "isolated" {
799+
uvx = uvx.arg(arg);
800+
}
801+
}
802+
args.shell.generate(&mut uvx, &mut stdout());
803+
781804
Ok(ExitStatus::Success)
782805
}
783806
Commands::Tool(ToolNamespace {
@@ -974,7 +997,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
974997
globals.python_downloads,
975998
globals.native_tls,
976999
globals.connectivity,
977-
cli.no_config,
1000+
cli.top_level.no_config,
9781001
printer,
9791002
)
9801003
.await
@@ -1000,7 +1023,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
10001023
commands::python_find(
10011024
args.request,
10021025
args.no_project,
1003-
cli.no_config,
1026+
cli.top_level.no_config,
10041027
args.system,
10051028
globals.python_preference,
10061029
&cache,

docs/getting-started/installation.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,25 @@ To enable shell autocompletion for uv commands, run one of the following:
181181

182182
Then restart the shell or source the shell config file.
183183

184+
You can also enable shell autocompletion for uvx by running the same commands, replacing `uv` with
185+
`uvx`:
186+
187+
=== "Linux and macOS"
188+
189+
```bash
190+
# Determine your shell (e.g., with `echo $SHELL`), then run one of:
191+
echo 'eval "$(uvx generate-shell-completion bash)"' >> ~/.bashrc
192+
echo 'eval "$(uvx generate-shell-completion zsh)"' >> ~/.zshrc
193+
echo 'uvx generate-shell-completion fish | source' >> ~/.config/fish/config.fish
194+
echo 'eval (uvx generate-shell-completion elvish | slurp)' >> ~/.elvish/rc.elv
195+
```
196+
197+
=== "Windows"
198+
199+
```powershell
200+
Add-Content -Path $PROFILE -Value '(& uvx generate-shell-completion powershell) | Out-String | Invoke-Expression'
201+
```
202+
184203
## Uninstallation
185204

186205
If you need to remove uv from your system, just remove the `uv` and `uvx` binaries:

0 commit comments

Comments
 (0)