Skip to content

Commit

Permalink
uv run: List available scripts when a script is not specified
Browse files Browse the repository at this point in the history
Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com>
  • Loading branch information
kakkoyun committed Sep 25, 2024
1 parent 82e33c2 commit 3c1be57
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 5 deletions.
2 changes: 1 addition & 1 deletion crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExternalCommand>,

/// Run with the given packages installed.
///
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::path::PathBuf, std::io::Error> {
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-python/src/which.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
50 changes: 50 additions & 0 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -717,6 +718,55 @@ 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 <command>` or `uv run script.py`.\n"
)?;

let mut 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();
scripts.sort();

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);

Expand Down
5 changes: 4 additions & 1 deletion crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,10 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
// 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
}
Expand Down
45 changes: 45 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <command>` or `uv run script.py`.
The following scripts are available:
python
python3
python3.12
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]
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Arguments following the command (or script) are not interpreted as arguments to
<h3 class="cli-reference">Usage</h3>

```
uv run [OPTIONS] <COMMAND>
uv run [OPTIONS] [COMMAND]
```

<h3 class="cli-reference">Options</h3>
Expand Down

0 comments on commit 3c1be57

Please sign in to comment.