diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index d2fc3f165780..316f31dc39ef 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3165,9 +3165,14 @@ pub struct ToolListArgs { #[arg(long)] pub show_paths: bool, + /// Whether to display the version specifier(s) used to install each tool. + #[arg(long)] + pub show_version_specifiers: bool, + // Hide unused global Python options. #[arg(long, hide = true)] pub python_preference: Option, + #[arg(long, hide = true)] pub no_python_downloads: bool, } diff --git a/crates/uv/src/commands/tool/list.rs b/crates/uv/src/commands/tool/list.rs index 2ce9a2443b15..8e5e0192836d 100644 --- a/crates/uv/src/commands/tool/list.rs +++ b/crates/uv/src/commands/tool/list.rs @@ -1,6 +1,7 @@ use std::fmt::Write; use anyhow::Result; +use itertools::Itertools; use owo_colors::OwoColorize; use uv_cache::Cache; @@ -12,7 +13,12 @@ use crate::commands::ExitStatus; use crate::printer::Printer; /// List installed tools. -pub(crate) async fn list(show_paths: bool, cache: &Cache, printer: Printer) -> Result { +pub(crate) async fn list( + show_paths: bool, + show_version_specifiers: bool, + cache: &Cache, + printer: Printer, +) -> Result { let installed_tools = InstalledTools::from_settings()?; let _lock = match installed_tools.lock().await { Ok(lock) => lock, @@ -50,15 +56,32 @@ pub(crate) async fn list(show_paths: bool, cache: &Cache, printer: Printer) -> R } }; + let version_specifier = if show_version_specifiers { + let specifiers = tool + .requirements() + .iter() + .filter(|req| req.name == name) + .map(|req| req.source.to_string()) + .filter(|s| !s.is_empty()) + .join(", "); + format!(" [required: {specifiers}]") + } else { + String::new() + }; + if show_paths { writeln!( printer.stdout(), "{} ({})", - format!("{name} v{version}").bold(), - installed_tools.tool_dir(&name).simplified_display().cyan() + format!("{name} v{version}{version_specifier}").bold(), + installed_tools.tool_dir(&name).simplified_display().cyan(), )?; } else { - writeln!(printer.stdout(), "{}", format!("{name} v{version}").bold())?; + writeln!( + printer.stdout(), + "{}", + format!("{name} v{version}{version_specifier}").bold() + )?; } // Output tool entrypoints diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 8f65c561e37a..cf85e93d0b4b 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -80,7 +80,7 @@ pub(crate) async fn run( ) -> anyhow::Result { // treat empty command as `uv tool list` let Some(command) = command else { - return tool_list(false, &cache, printer).await; + return tool_list(false, false, &cache, printer).await; }; let (target, args) = command.split(); diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index eb331b996128..ec211c035a47 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -872,7 +872,13 @@ async fn run(cli: Cli) -> Result { // Initialize the cache. let cache = cache.init()?; - commands::tool_list(args.show_paths, &cache, printer).await + commands::tool_list( + args.show_paths, + args.show_version_specifiers, + &cache, + printer, + ) + .await } Commands::Tool(ToolNamespace { command: ToolCommand::Upgrade(args), diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 86941d507046..50c2470ad23d 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -457,15 +457,24 @@ impl ToolUpgradeSettings { #[derive(Debug, Clone)] pub(crate) struct ToolListSettings { pub(crate) show_paths: bool, + pub(crate) show_version_specifiers: bool, } impl ToolListSettings { /// Resolve the [`ToolListSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] pub(crate) fn resolve(args: ToolListArgs, _filesystem: Option) -> Self { - let ToolListArgs { show_paths, .. } = args; + let ToolListArgs { + show_paths, + show_version_specifiers, + python_preference: _, + no_python_downloads: _, + } = args; - Self { show_paths } + Self { + show_paths, + show_version_specifiers, + } } } diff --git a/crates/uv/tests/tool_list.rs b/crates/uv/tests/tool_list.rs index 8f2633af39c0..b8ff45c5a18f 100644 --- a/crates/uv/tests/tool_list.rs +++ b/crates/uv/tests/tool_list.rs @@ -252,3 +252,46 @@ fn tool_list_deprecated() -> Result<()> { Ok(()) } + +#[test] +fn tool_list_show_version_specifiers() { + let context = TestContext::new("3.12").with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + // Install `black` + context + .tool_install() + .arg("black<24.3.0") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .assert() + .success(); + + uv_snapshot!(context.filters(), context.tool_list().arg("--show-version-specifiers") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + black v24.2.0 [required: <24.3.0] + - black + - blackd + + ----- stderr ----- + "###); + + // with paths + uv_snapshot!(context.filters(), context.tool_list().arg("--show-version-specifiers").arg("--show-paths") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + black v24.2.0 [required: <24.3.0] ([TEMP_DIR]/tools/black) + - black ([TEMP_DIR]/bin/black) + - blackd ([TEMP_DIR]/bin/blackd) + + ----- stderr ----- + "###); +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 55dccf1dd89a..21861d59fc83 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -3141,6 +3141,8 @@ uv tool list [OPTIONS]
--show-paths

Whether to display the path to each tool environment and installed executable

+
--show-version-specifiers

Whether to display the version specifier(s) used to install each tool

+
--verbose, -v

Use verbose output.

You can configure fine-grained logging using the RUST_LOG environment variable. (<https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>)