diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index b57863797..c624c32dc 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3192,8 +3192,8 @@ pub struct RunArgs { /// /// Can be provided multiple times, with subsequent files overriding values defined in previous /// files. - #[arg(long, value_delimiter = ' ', env = EnvVars::UV_ENV_FILE)] - pub env_file: Vec, + #[arg(long, env = EnvVars::UV_ENV_FILE)] + pub env_file: Vec, /// Avoid reading environment variables from a `.env` file. #[arg(long, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_ENV_FILE)] diff --git a/crates/uv-configuration/src/env_file.rs b/crates/uv-configuration/src/env_file.rs new file mode 100644 index 000000000..8a7bf855d --- /dev/null +++ b/crates/uv-configuration/src/env_file.rs @@ -0,0 +1,135 @@ +use std::path::PathBuf; + +/// A collection of `.env` file paths. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct EnvFile(Vec); + +impl EnvFile { + /// Parse the env file paths from command-line arguments. + pub fn from_args(env_file: Vec, no_env_file: bool) -> Self { + if no_env_file { + return Self::default(); + } + + if env_file.is_empty() { + return Self::default(); + } + + let mut paths = Vec::new(); + + // Split on spaces, but respect backslashes. + for env_file in env_file { + let mut current = String::new(); + let mut escape = false; + for c in env_file.chars() { + if escape { + current.push(c); + escape = false; + } else if c == '\\' { + escape = true; + } else if c.is_whitespace() { + if !current.is_empty() { + paths.push(PathBuf::from(current)); + current = String::new(); + } + } else { + current.push(c); + } + } + if !current.is_empty() { + paths.push(PathBuf::from(current)); + } + } + + Self(paths) + } + + /// Iterate over the paths in the env file. + pub fn iter(&self) -> impl DoubleEndedIterator { + self.0.iter() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_args_default() { + let env_file = EnvFile::from_args(vec![], false); + assert_eq!(env_file, EnvFile::default()); + } + + #[test] + fn test_from_args_no_env_file() { + let env_file = EnvFile::from_args(vec!["path1 path2".to_string()], true); + assert_eq!(env_file, EnvFile::default()); + } + + #[test] + fn test_from_args_empty_string() { + let env_file = EnvFile::from_args(vec![String::new()], false); + assert_eq!(env_file, EnvFile::default()); + } + + #[test] + fn test_from_args_whitespace_only() { + let env_file = EnvFile::from_args(vec![" ".to_string()], false); + assert_eq!(env_file, EnvFile::default()); + } + + #[test] + fn test_from_args_single_path() { + let env_file = EnvFile::from_args(vec!["path1".to_string()], false); + assert_eq!(env_file.0, vec![PathBuf::from("path1")]); + } + + #[test] + fn test_from_args_multiple_paths() { + let env_file = EnvFile::from_args(vec!["path1 path2 path3".to_string()], false); + assert_eq!( + env_file.0, + vec![ + PathBuf::from("path1"), + PathBuf::from("path2"), + PathBuf::from("path3") + ] + ); + } + + #[test] + fn test_from_args_escaped_spaces() { + let env_file = EnvFile::from_args(vec![r"path\ with\ spaces".to_string()], false); + assert_eq!(env_file.0, vec![PathBuf::from("path with spaces")]); + } + + #[test] + fn test_from_args_mixed_escaped_and_normal() { + let env_file = + EnvFile::from_args(vec![r"path1 path\ with\ spaces path2".to_string()], false); + assert_eq!( + env_file.0, + vec![ + PathBuf::from("path1"), + PathBuf::from("path with spaces"), + PathBuf::from("path2") + ] + ); + } + + #[test] + fn test_from_args_escaped_backslash() { + let env_file = EnvFile::from_args(vec![r"path\\with\\backslashes".to_string()], false); + assert_eq!(env_file.0, vec![PathBuf::from(r"path\with\backslashes")]); + } + + #[test] + fn test_iter() { + let env_file = EnvFile(vec![PathBuf::from("path1"), PathBuf::from("path2")]); + let paths: Vec<_> = env_file.iter().collect(); + assert_eq!( + paths, + vec![&PathBuf::from("path1"), &PathBuf::from("path2")] + ); + } +} diff --git a/crates/uv-configuration/src/lib.rs b/crates/uv-configuration/src/lib.rs index ec5514bf0..500a7aa38 100644 --- a/crates/uv-configuration/src/lib.rs +++ b/crates/uv-configuration/src/lib.rs @@ -5,6 +5,7 @@ pub use constraints::*; pub use dependency_groups::*; pub use dry_run::*; pub use editable::*; +pub use env_file::*; pub use export_format::*; pub use extras::*; pub use hash::*; @@ -28,6 +29,7 @@ mod constraints; mod dependency_groups; mod dry_run; mod editable; +mod env_file; mod export_format; mod extras; mod hash; diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index e6cf2f50f..3101cee81 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -18,7 +18,7 @@ use uv_cache::Cache; use uv_cli::ExternalCommand; use uv_client::BaseClientBuilder; use uv_configuration::{ - Concurrency, Constraints, DependencyGroups, DryRun, EditableMode, ExtrasSpecification, + Concurrency, Constraints, DependencyGroups, DryRun, EditableMode, EnvFile, ExtrasSpecification, InstallOptions, TargetTriple, }; use uv_distribution::LoweredExtraBuildDependencies; @@ -106,8 +106,7 @@ pub(crate) async fn run( concurrency: Concurrency, cache: &Cache, printer: Printer, - env_file: Vec, - no_env_file: bool, + env_file: EnvFile, preview: Preview, max_recursion_depth: u32, ) -> anyhow::Result { @@ -164,39 +163,37 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl let workspace_cache = WorkspaceCache::default(); // Read from the `.env` file, if necessary. - if !no_env_file { - for env_file_path in env_file.iter().rev().map(PathBuf::as_path) { - match dotenvy::from_path(env_file_path) { - Err(dotenvy::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => { - bail!( - "No environment file found at: `{}`", - env_file_path.simplified_display() - ); - } - Err(dotenvy::Error::Io(err)) => { - bail!( - "Failed to read environment file `{}`: {err}", - env_file_path.simplified_display() - ); - } - Err(dotenvy::Error::LineParse(content, position)) => { - warn_user!( - "Failed to parse environment file `{}` at position {position}: {content}", - env_file_path.simplified_display(), - ); - } - Err(err) => { - warn_user!( - "Failed to parse environment file `{}`: {err}", - env_file_path.simplified_display(), - ); - } - Ok(()) => { - debug!( - "Read environment file at: `{}`", - env_file_path.simplified_display() - ); - } + for env_file_path in env_file.iter().rev().map(PathBuf::as_path) { + match dotenvy::from_path(env_file_path) { + Err(dotenvy::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => { + bail!( + "No environment file found at: `{}`", + env_file_path.simplified_display() + ); + } + Err(dotenvy::Error::Io(err)) => { + bail!( + "Failed to read environment file `{}`: {err}", + env_file_path.simplified_display() + ); + } + Err(dotenvy::Error::LineParse(content, position)) => { + warn_user!( + "Failed to parse environment file `{}` at position {position}: {content}", + env_file_path.simplified_display(), + ); + } + Err(err) => { + warn_user!( + "Failed to parse environment file `{}`: {err}", + env_file_path.simplified_display(), + ); + } + Ok(()) => { + debug!( + "Read environment file at: `{}`", + env_file_path.simplified_display() + ); } } } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 95d636732..345302946 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1865,7 +1865,6 @@ async fn run_project( &cache, printer, args.env_file, - args.no_env_file, globals.preview, args.max_recursion_depth, )) diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index e3c71649e..9c1ab8f07 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -23,7 +23,7 @@ use uv_cli::{ }; use uv_client::Connectivity; use uv_configuration::{ - BuildIsolation, BuildOptions, Concurrency, DependencyGroups, DryRun, EditableMode, + BuildIsolation, BuildOptions, Concurrency, DependencyGroups, DryRun, EditableMode, EnvFile, ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, KeyringProviderType, NoBinary, NoBuild, ProjectBuildBackend, Reinstall, RequiredVersion, SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, @@ -343,8 +343,7 @@ pub(crate) struct RunSettings { pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, - pub(crate) env_file: Vec, - pub(crate) no_env_file: bool, + pub(crate) env_file: EnvFile, pub(crate) max_recursion_depth: u32, } @@ -461,8 +460,7 @@ impl RunSettings { resolver_installer_options(installer, build), filesystem, ), - env_file, - no_env_file, + env_file: EnvFile::from_args(env_file, no_env_file), install_mirrors, max_recursion_depth: max_recursion_depth.unwrap_or(Self::DEFAULT_MAX_RECURSION_DEPTH), }