From 26a355220d88292110f07b71269c682b33503c54 Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Wed, 25 Sep 2024 17:38:31 +0200 Subject: [PATCH] uv run: List available scripts when a script is not specified Signed-off-by: Kemal Akkoyun --- crates/uv-cli/src/lib.rs | 2 +- crates/uv-python/src/lib.rs | 2 +- crates/uv-python/src/which.rs | 2 +- crates/uv/src/commands/project/run.rs | 49 +++++++++++++++++++++++++++ crates/uv/src/lib.rs | 5 ++- crates/uv/tests/run.rs | 45 ++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 4 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 9d143d521480a..f38fe50eaa1eb 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2449,7 +2449,7 @@ pub struct RunArgs { /// If the path to a Python script (i.e., ending in `.py`), it will be /// executed with the Python interpreter. #[command(subcommand)] - pub command: ExternalCommand, + pub command: Option, /// Run with the given packages installed. /// diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index cb8e99f67f62b..b13ba983c809d 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -37,7 +37,7 @@ mod python_version; mod target; mod version_files; mod virtualenv; -mod which; +pub mod which; #[cfg(not(test))] pub(crate) fn current_dir() -> Result { diff --git a/crates/uv-python/src/which.rs b/crates/uv-python/src/which.rs index 352bc14f3b93d..26a68203dbf2c 100644 --- a/crates/uv-python/src/which.rs +++ b/crates/uv-python/src/which.rs @@ -3,7 +3,7 @@ use std::path::Path; /// Check whether a path in PATH is a valid executable. /// /// Derived from `which`'s `Checker`. -pub(crate) fn is_executable(path: &Path) -> bool { +pub fn is_executable(path: &Path) -> bool { #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] { if rustix::fs::access(path, rustix::fs::Access::EXEC_OK).is_err() { diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 2dc660e34c9cd..d4bd890bdd26c 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -21,6 +21,7 @@ use uv_distribution::LoweredRequirement; use uv_fs::{PythonExt, Simplified}; use uv_installer::{SatisfiesResult, SitePackages}; use uv_normalize::PackageName; +use uv_python::which::is_executable; use uv_python::{ EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile, VersionRequest, @@ -717,6 +718,54 @@ pub(crate) async fn run( .as_ref() .map_or_else(|| &base_interpreter, |env| env.interpreter()); + // Check if any run command is given. + // If not, print the available scripts for the current interpreter. + if let RunCommand::Empty = command { + writeln!( + printer.stdout(), + "Provide a command or script to invoke with `uv run ` or `uv run script.py`.\n" + )?; + + let scripts = interpreter + .scripts() + .read_dir() + .ok() + .into_iter() + .flatten() + .filter_map(|entry| match entry { + Ok(entry) => Some(entry), + Err(err) => { + debug!("Failed to read entry: {}", err); + None + } + }) + .filter(|entry| { + entry + .file_type() + .is_ok_and(|file_type| file_type.is_file() || file_type.is_symlink()) + }) + .map(|entry| entry.path()) + .filter(|path| is_executable(path)) + .map(|path| { + path.file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string() + }) + .collect_vec(); + + if !scripts.is_empty() { + writeln!(printer.stdout(), "The following scripts are available:\n")?; + writeln!(printer.stdout(), "{}", scripts.join("\n"))?; + } + writeln!( + printer.stdout(), + "\nSee `uv run --help` for more information." + )?; + + return Ok(ExitStatus::Success); + }; + debug!("Running `{command}`"); let mut process = command.as_command(interpreter); diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index ada7dc88532dd..ad962e6946522 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -148,7 +148,10 @@ async fn run(cli: Cli) -> Result { // Parse the external command, if necessary. let run_command = if let Commands::Project(command) = &*cli.command { if let ProjectCommand::Run(uv_cli::RunArgs { command, .. }) = &**command { - Some(RunCommand::try_from(command)?) + match command { + Some(command) => Some(RunCommand::try_from(command)?), + None => Some(RunCommand::Empty), + } } else { None } diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index ed6514913689e..3e5e558b9fb5e 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -197,6 +197,51 @@ fn run_args() -> Result<()> { Ok(()) } +/// Run without specifying any argunments. +/// This should list the available scripts. +#[test] +fn run_no_args() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.8" + dependencies = [] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "# + })?; + + // Run without specifying any argunments. + uv_snapshot!(context.filters(), context.run(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Provide a command or script to invoke with `uv run ` or `uv run script.py`. + + The following scripts are available: + + python3 + python3.12 + python + + See `uv run --help` for more information. + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + foo==1.0.0 (from file://[TEMP_DIR]/) + "###); + + Ok(()) +} + /// Run a PEP 723-compatible script. The script should take precedence over the workspace /// dependencies. #[test]