Skip to content

Commit a108269

Browse files
committed
Add json output to uv tool list
1 parent 66142e9 commit a108269

4 files changed

Lines changed: 184 additions & 3 deletions

File tree

crates/uv-cli/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ pub enum PythonListFormat {
5959
Json,
6060
}
6161

62+
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
63+
pub enum ToolListFormat {
64+
/// Plain text (for humans).
65+
#[default]
66+
Text,
67+
/// JSON (for computers).
68+
Json,
69+
}
70+
6271
#[derive(Debug, Default, Clone, Copy, clap::ValueEnum)]
6372
pub enum SyncFormat {
6473
/// Display the result in a human-readable format.
@@ -6149,6 +6158,10 @@ pub struct ToolListArgs {
61496158

61506159
#[arg(long, hide = true)]
61516160
pub no_python_downloads: bool,
6161+
6162+
/// Select the output format.
6163+
#[arg(long, value_enum, default_value_t = ToolListFormat::default())]
6164+
pub output_format: ToolListFormat,
61526165
}
61536166

61546167
#[derive(Args)]

crates/uv/src/commands/tool/list.rs

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
use std::fmt::Write;
2+
use std::path::PathBuf;
23

34
use anyhow::Result;
45
use futures::StreamExt;
56
use itertools::Itertools;
67
use owo_colors::OwoColorize;
78
use rustc_hash::FxHashMap;
89

10+
use serde::Serialize;
911
use uv_cache::{Cache, Refresh};
1012
use uv_cache_info::Timestamp;
13+
use uv_cli::ToolListFormat;
1114
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
1215
use uv_configuration::Concurrency;
1316
use uv_distribution_filename::DistFilename;
1417
use uv_distribution_types::{IndexCapabilities, RequiresPython};
1518
use uv_fs::Simplified;
1619
use uv_normalize::PackageName;
20+
use uv_pep440::Version;
21+
use uv_pep508::StringVersion;
1722
use uv_python::LenientImplementationName;
1823
use uv_settings::{Combine, ResolverInstallerOptions};
19-
use uv_tool::InstalledTools;
24+
use uv_tool::{InstalledTools, Tool, ToolEnvironment};
2025
use uv_warnings::warn_user;
2126

2227
use crate::commands::ExitStatus;
@@ -25,6 +30,49 @@ use crate::commands::reporters::LatestVersionReporter;
2530
use crate::printer::Printer;
2631
use crate::settings::ResolverInstallerSettings;
2732

33+
#[derive(Debug, Serialize, Default)]
34+
struct Schema {
35+
version: SchemaVersion,
36+
}
37+
38+
#[derive(Debug, Serialize, Default)]
39+
#[serde(rename_all = "snake_case")]
40+
enum SchemaVersion {
41+
#[default]
42+
Preview,
43+
}
44+
45+
#[derive(Debug, Serialize)]
46+
struct CommandPrintData {
47+
name: String,
48+
path: PathBuf,
49+
}
50+
51+
#[derive(Debug, Serialize)]
52+
struct PythonPrintData {
53+
implementation: String,
54+
version: StringVersion,
55+
}
56+
57+
#[derive(Debug, Serialize)]
58+
struct ToolPrintData {
59+
name: String,
60+
version: Version,
61+
latest_version: Option<Version>,
62+
path: PathBuf,
63+
commands: Vec<CommandPrintData>,
64+
extras: Vec<String>,
65+
version_specifiers: String,
66+
with: Vec<String>,
67+
python: PythonPrintData,
68+
}
69+
70+
#[derive(Debug, Serialize)]
71+
struct PrintData {
72+
schema: Schema,
73+
tools: Vec<ToolPrintData>,
74+
}
75+
2876
/// List installed tools.
2977
#[expect(clippy::fn_params_excessive_bools)]
3078
pub(crate) async fn list(
@@ -34,6 +82,7 @@ pub(crate) async fn list(
3482
show_extras: bool,
3583
show_python: bool,
3684
outdated: bool,
85+
output_format: ToolListFormat,
3786
args: ResolverInstallerOptions,
3887
filesystem: ResolverInstallerOptions,
3988
client_builder: BaseClientBuilder<'_>,
@@ -183,6 +232,121 @@ pub(crate) async fn list(
183232
FxHashMap::default()
184233
};
185234

235+
match output_format {
236+
ToolListFormat::Text => render_list_text(
237+
outdated,
238+
show_version_specifiers,
239+
show_extras,
240+
show_python,
241+
show_with,
242+
show_paths,
243+
latest,
244+
printer,
245+
valid_tools,
246+
installed_tools,
247+
)?,
248+
ToolListFormat::Json => {
249+
render_list_json(outdated, latest, printer, valid_tools, installed_tools)?
250+
}
251+
}
252+
253+
Ok(ExitStatus::Success)
254+
}
255+
256+
fn render_list_json(
257+
outdated: bool,
258+
latest: FxHashMap<PackageName, Option<DistFilename>>,
259+
printer: Printer,
260+
valid_tools: Vec<(PackageName, Tool, ToolEnvironment, Version)>,
261+
installed_tools: InstalledTools,
262+
) -> Result<()> {
263+
let tools = valid_tools
264+
.iter()
265+
.filter(|(name, _, _, version)| {
266+
// skip up-to-date entries if we want outdated tools
267+
!outdated
268+
|| latest
269+
.get(&name)
270+
.and_then(Option::as_ref)
271+
.is_some_and(|filename| filename.version() > &version)
272+
})
273+
.map(|(name, tool, tool_env, version)| ToolPrintData {
274+
name: name.to_string(),
275+
version: version.clone(),
276+
latest_version: if outdated {
277+
latest
278+
.get(&name)
279+
.and_then(Option::as_ref)
280+
.map(|filename| filename.version().clone())
281+
} else {
282+
// when we don't list outdated tools, we do not know the latest version
283+
None
284+
},
285+
path: installed_tools.tool_dir(&name),
286+
commands: tool
287+
.entrypoints()
288+
.iter()
289+
.map(|ep| CommandPrintData {
290+
name: ep.name.clone(),
291+
path: ep.install_path.clone(),
292+
})
293+
.collect::<Vec<_>>(),
294+
extras: tool
295+
.requirements()
296+
.iter()
297+
.filter(|req| req.name == *name)
298+
.flat_map(|req| req.extras.iter()) // Flatten the extras from all matching requirements
299+
.map(ToString::to_string)
300+
.collect::<Vec<_>>(),
301+
version_specifiers: tool
302+
.requirements()
303+
.iter()
304+
.filter(|req| req.name == *name)
305+
.map(|req| req.source.to_string())
306+
.filter(|s| !s.is_empty())
307+
// XXX I don't think we'll ever have more than 1 element here, since multiple
308+
// specifiers like in 'twine[keyring]>=6.2.0,!=69' are all in the same string
309+
.join(", "),
310+
with: tool
311+
.requirements()
312+
.iter()
313+
.filter(|req| req.name != *name)
314+
.map(|req| format!("{}{}", req.name, req.source))
315+
.collect::<Vec<_>>(),
316+
python: {
317+
let interpreter = tool_env.environment().interpreter();
318+
let implementation =
319+
LenientImplementationName::from(interpreter.implementation_name());
320+
PythonPrintData {
321+
implementation: implementation.to_string(),
322+
version: interpreter.python_full_version().clone(),
323+
}
324+
},
325+
})
326+
.collect::<Vec<_>>();
327+
328+
let data = PrintData {
329+
schema: Schema {
330+
version: SchemaVersion::Preview,
331+
},
332+
tools,
333+
};
334+
writeln!(printer.stdout(), "{}", serde_json::to_string(&data)?)?;
335+
Ok(())
336+
}
337+
338+
fn render_list_text(
339+
outdated: bool,
340+
show_version_specifiers: bool,
341+
show_extras: bool,
342+
show_python: bool,
343+
show_with: bool,
344+
show_paths: bool,
345+
latest: FxHashMap<PackageName, Option<DistFilename>>,
346+
printer: Printer,
347+
valid_tools: Vec<(PackageName, Tool, ToolEnvironment, Version)>,
348+
installed_tools: InstalledTools,
349+
) -> Result<()> {
186350
for (name, tool, tool_env, version) in valid_tools {
187351
// If `--outdated` is set, skip tools that are up-to-date.
188352
if outdated {
@@ -295,5 +459,5 @@ pub(crate) async fn list(
295459
}
296460
}
297461

298-
Ok(ExitStatus::Success)
462+
Ok(())
299463
}

crates/uv/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1728,6 +1728,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul
17281728
args.show_extras,
17291729
args.show_python,
17301730
args.outdated,
1731+
args.output_format,
17311732
args.args,
17321733
args.filesystem,
17331734
client_builder.subcommand(vec!["tool".to_owned(), "list".to_owned()]),

crates/uv/src/settings.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use uv_cli::{
1919
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
2020
PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs,
2121
PythonListFormat, PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs,
22-
SyncArgs, SyncFormat, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs,
22+
SyncArgs, SyncFormat, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolListFormat, ToolRunArgs,
2323
ToolUninstallArgs, TreeArgs, TreeFormat, UpgradeArgs, VenvArgs, VersionArgs, VersionBumpSpec,
2424
VersionFormat,
2525
};
@@ -1270,6 +1270,7 @@ pub(crate) struct ToolListSettings {
12701270
pub(crate) show_extras: bool,
12711271
pub(crate) show_python: bool,
12721272
pub(crate) outdated: bool,
1273+
pub(crate) output_format: ToolListFormat,
12731274
pub(crate) args: ResolverInstallerOptions,
12741275
pub(crate) filesystem: ResolverInstallerOptions,
12751276
}
@@ -1285,6 +1286,7 @@ impl ToolListSettings {
12851286
show_python,
12861287
outdated,
12871288
no_outdated,
1289+
output_format,
12881290
exclude_newer,
12891291
python_preference: _,
12901292
no_python_downloads: _,
@@ -1303,6 +1305,7 @@ impl ToolListSettings {
13031305
show_extras,
13041306
show_python,
13051307
outdated: flag(outdated, no_outdated, "outdated").unwrap_or(false),
1308+
output_format,
13061309
args: ResolverInstallerOptions {
13071310
exclude_newer,
13081311
..ResolverInstallerOptions::default()

0 commit comments

Comments
 (0)