mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 18:38:21 +00:00
Return error when running uvx with a .py script (#11623)
If we see `uvx script.py`, we exit early, giving a hint to use `uv run script.py` if the script exists. If it does not exist, we suggest running `uv run` with a normalized package name. This PR includes a snapshot test for each of these scenarios. An alternative approach would be to wait until we encounter an error, and then add the hint. But if there happens to be a malicious package called `script-py`, this would be run unintentionally (a point raised by @zanieb). Closes #10784 --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
3b83b48fd2
commit
c072c9adca
2 changed files with 161 additions and 2 deletions
|
@ -1,5 +1,6 @@
|
|||
use std::fmt::Display;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
@ -18,6 +19,7 @@ use uv_configuration::{Concurrency, PreviewMode};
|
|||
use uv_distribution_types::{
|
||||
Name, NameRequirementSpecification, UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||
};
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::{SatisfiesResult, SitePackages};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
||||
|
@ -92,6 +94,12 @@ pub(crate) async fn run(
|
|||
printer: Printer,
|
||||
preview: PreviewMode,
|
||||
) -> anyhow::Result<ExitStatus> {
|
||||
/// Whether or not a path looks like a Python script based on the file extension.
|
||||
fn has_python_script_ext(path: &Path) -> bool {
|
||||
path.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("py") || ext.eq_ignore_ascii_case("pyw"))
|
||||
}
|
||||
|
||||
let Some(command) = command else {
|
||||
// When a command isn't provided, we'll show a brief help including available tools
|
||||
show_help(invocation_source, &cache, printer).await?;
|
||||
|
@ -105,9 +113,46 @@ pub(crate) async fn run(
|
|||
};
|
||||
|
||||
let Some(target) = target.to_str() else {
|
||||
return Err(anyhow::anyhow!("Tool command could not be parsed as UTF-8 string. Use `--from` to specify the package name."));
|
||||
return Err(anyhow::anyhow!("Tool command could not be parsed as UTF-8 string. Use `--from` to specify the package name"));
|
||||
};
|
||||
|
||||
if let Some(ref from) = from {
|
||||
if has_python_script_ext(Path::new(from)) {
|
||||
let package_name = PackageName::from_str(from)?;
|
||||
return Err(anyhow::anyhow!(
|
||||
"It looks you provided a Python script to `--from`, which is not supported\n\n{}{} If you meant to run a command from the `{}` package, use the normalized package name instead to disambiguate, e.g., `{}`",
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
package_name.cyan(),
|
||||
format!("{} --from {} {}", invocation_source, package_name.cyan(), target),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let target_path = Path::new(target);
|
||||
if has_python_script_ext(target_path) {
|
||||
return if target_path.try_exists()? {
|
||||
Err(anyhow::anyhow!(
|
||||
"It looks you tried to run a Python script at `{}`, which is not supported by `{}`\n\n{}{} Use `{}` instead",
|
||||
target_path.user_display(),
|
||||
invocation_source,
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
format!("uv run {}", target_path.user_display().cyan()),
|
||||
))
|
||||
} else {
|
||||
let package_name = PackageName::from_str(target)?;
|
||||
Err(anyhow::anyhow!(
|
||||
"It looks you provided a Python script to run, which is not supported supported by `{}`\n\n{}{} We did not find a script at the requested path. If you meant to run a command from the `{}` package, pass the normalized package name to `--from` to disambiguate, e.g., `{}`",
|
||||
invocation_source,
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
package_name.cyan(),
|
||||
format!("{} --from {} {}", invocation_source, package_name, target),
|
||||
))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let request = ToolRequest::parse(target, from.as_deref());
|
||||
|
||||
// If the user passed, e.g., `ruff@latest`, refresh the cache.
|
||||
|
|
|
@ -1760,7 +1760,7 @@ fn tool_run_python_at_version() {
|
|||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
|
||||
----- stderr -----
|
||||
error: Invalid version request: 3.300
|
||||
"###);
|
||||
|
@ -1962,3 +1962,117 @@ fn tool_run_verbatim_name() {
|
|||
Resolved [N] packages in [TIME]
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_run_with_existing_py_script() -> anyhow::Result<()> {
|
||||
let context = TestContext::new("3.12").with_filtered_counts();
|
||||
context.temp_dir.child("script.py").touch()?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_run().arg("script.py"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: It looks you tried to run a Python script at `script.py`, which is not supported by `uv tool run`
|
||||
|
||||
hint: Use `uv run script.py` instead
|
||||
");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_run_with_existing_pyw_script() -> anyhow::Result<()> {
|
||||
let context = TestContext::new("3.12").with_filtered_counts();
|
||||
context.temp_dir.child("script.pyw").touch()?;
|
||||
|
||||
// We treat arguments before the command as uv arguments
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("script.pyw"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: It looks you tried to run a Python script at `script.pyw`, which is not supported by `uv tool run`
|
||||
|
||||
hint: Use `uv run script.pyw` instead
|
||||
");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_run_with_nonexistent_py_script() {
|
||||
let context = TestContext::new("3.12").with_filtered_counts();
|
||||
|
||||
// We treat arguments before the command as uv arguments
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("script.py"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: It looks you provided a Python script to run, which is not supported supported by `uv tool run`
|
||||
|
||||
hint: We did not find a script at the requested path. If you meant to run a command from the `script-py` package, pass the normalized package name to `--from` to disambiguate, e.g., `uv tool run --from script-py script.py`
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_run_with_nonexistent_pyw_script() {
|
||||
let context = TestContext::new("3.12").with_filtered_counts();
|
||||
|
||||
// We treat arguments before the command as uv arguments
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("script.pyw"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: It looks you provided a Python script to run, which is not supported supported by `uv tool run`
|
||||
|
||||
hint: We did not find a script at the requested path. If you meant to run a command from the `script-pyw` package, pass the normalized package name to `--from` to disambiguate, e.g., `uv tool run --from script-pyw script.pyw`
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_run_with_from_script() {
|
||||
let context = TestContext::new("3.12").with_filtered_counts();
|
||||
|
||||
// We treat arguments before the command as uv arguments
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("--from")
|
||||
.arg("script.py")
|
||||
.arg("ruff"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: It looks you provided a Python script to `--from`, which is not supported
|
||||
|
||||
hint: If you meant to run a command from the `script-py` package, use the normalized package name instead to disambiguate, e.g., `uv tool run --from script-py ruff`
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_run_with_script_and_from_script() {
|
||||
let context = TestContext::new("3.12").with_filtered_counts();
|
||||
|
||||
// We treat arguments before the command as uv arguments
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("--from")
|
||||
.arg("script.py")
|
||||
.arg("other-script.py"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: It looks you provided a Python script to `--from`, which is not supported
|
||||
|
||||
hint: If you meant to run a command from the `script-py` package, use the normalized package name instead to disambiguate, e.g., `uv tool run --from script-py other-script.py`
|
||||
");
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue