mirror of https://github.com/astral-sh/uv
Allow escaping spaces in --env-file handling (#15815)
## Summary We allow space-delimiting for `--env-file`, but Clap doesn't support any form of escaping, so as-is, there's no way to provide a `.env` file in a directory that contains a space. We now do the splitting ourselves and respect escapes. Closes https://github.com/astral-sh/uv/issues/15806.
This commit is contained in:
parent
6876716fd2
commit
f59d00b479
|
|
@ -3192,8 +3192,8 @@ pub struct RunArgs {
|
||||||
///
|
///
|
||||||
/// Can be provided multiple times, with subsequent files overriding values defined in previous
|
/// Can be provided multiple times, with subsequent files overriding values defined in previous
|
||||||
/// files.
|
/// files.
|
||||||
#[arg(long, value_delimiter = ' ', env = EnvVars::UV_ENV_FILE)]
|
#[arg(long, env = EnvVars::UV_ENV_FILE)]
|
||||||
pub env_file: Vec<PathBuf>,
|
pub env_file: Vec<String>,
|
||||||
|
|
||||||
/// Avoid reading environment variables from a `.env` file.
|
/// Avoid reading environment variables from a `.env` file.
|
||||||
#[arg(long, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_ENV_FILE)]
|
#[arg(long, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_ENV_FILE)]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// A collection of `.env` file paths.
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct EnvFile(Vec<PathBuf>);
|
||||||
|
|
||||||
|
impl EnvFile {
|
||||||
|
/// Parse the env file paths from command-line arguments.
|
||||||
|
pub fn from_args(env_file: Vec<String>, 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<Item = &PathBuf> {
|
||||||
|
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")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ pub use constraints::*;
|
||||||
pub use dependency_groups::*;
|
pub use dependency_groups::*;
|
||||||
pub use dry_run::*;
|
pub use dry_run::*;
|
||||||
pub use editable::*;
|
pub use editable::*;
|
||||||
|
pub use env_file::*;
|
||||||
pub use export_format::*;
|
pub use export_format::*;
|
||||||
pub use extras::*;
|
pub use extras::*;
|
||||||
pub use hash::*;
|
pub use hash::*;
|
||||||
|
|
@ -28,6 +29,7 @@ mod constraints;
|
||||||
mod dependency_groups;
|
mod dependency_groups;
|
||||||
mod dry_run;
|
mod dry_run;
|
||||||
mod editable;
|
mod editable;
|
||||||
|
mod env_file;
|
||||||
mod export_format;
|
mod export_format;
|
||||||
mod extras;
|
mod extras;
|
||||||
mod hash;
|
mod hash;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ use uv_cache::Cache;
|
||||||
use uv_cli::ExternalCommand;
|
use uv_cli::ExternalCommand;
|
||||||
use uv_client::BaseClientBuilder;
|
use uv_client::BaseClientBuilder;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
Concurrency, Constraints, DependencyGroups, DryRun, EditableMode, ExtrasSpecification,
|
Concurrency, Constraints, DependencyGroups, DryRun, EditableMode, EnvFile, ExtrasSpecification,
|
||||||
InstallOptions, TargetTriple,
|
InstallOptions, TargetTriple,
|
||||||
};
|
};
|
||||||
use uv_distribution::LoweredExtraBuildDependencies;
|
use uv_distribution::LoweredExtraBuildDependencies;
|
||||||
|
|
@ -106,8 +106,7 @@ pub(crate) async fn run(
|
||||||
concurrency: Concurrency,
|
concurrency: Concurrency,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
env_file: Vec<PathBuf>,
|
env_file: EnvFile,
|
||||||
no_env_file: bool,
|
|
||||||
preview: Preview,
|
preview: Preview,
|
||||||
max_recursion_depth: u32,
|
max_recursion_depth: u32,
|
||||||
) -> anyhow::Result<ExitStatus> {
|
) -> anyhow::Result<ExitStatus> {
|
||||||
|
|
@ -164,7 +163,6 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
||||||
let workspace_cache = WorkspaceCache::default();
|
let workspace_cache = WorkspaceCache::default();
|
||||||
|
|
||||||
// Read from the `.env` file, if necessary.
|
// Read from the `.env` file, if necessary.
|
||||||
if !no_env_file {
|
|
||||||
for env_file_path in env_file.iter().rev().map(PathBuf::as_path) {
|
for env_file_path in env_file.iter().rev().map(PathBuf::as_path) {
|
||||||
match dotenvy::from_path(env_file_path) {
|
match dotenvy::from_path(env_file_path) {
|
||||||
Err(dotenvy::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
|
Err(dotenvy::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
|
@ -199,7 +197,6 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize any output reporters.
|
// Initialize any output reporters.
|
||||||
let download_reporter = PythonDownloadReporter::single(printer);
|
let download_reporter = PythonDownloadReporter::single(printer);
|
||||||
|
|
|
||||||
|
|
@ -1865,7 +1865,6 @@ async fn run_project(
|
||||||
&cache,
|
&cache,
|
||||||
printer,
|
printer,
|
||||||
args.env_file,
|
args.env_file,
|
||||||
args.no_env_file,
|
|
||||||
globals.preview,
|
globals.preview,
|
||||||
args.max_recursion_depth,
|
args.max_recursion_depth,
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ use uv_cli::{
|
||||||
};
|
};
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
BuildIsolation, BuildOptions, Concurrency, DependencyGroups, DryRun, EditableMode,
|
BuildIsolation, BuildOptions, Concurrency, DependencyGroups, DryRun, EditableMode, EnvFile,
|
||||||
ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions,
|
ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions,
|
||||||
KeyringProviderType, NoBinary, NoBuild, ProjectBuildBackend, Reinstall, RequiredVersion,
|
KeyringProviderType, NoBinary, NoBuild, ProjectBuildBackend, Reinstall, RequiredVersion,
|
||||||
SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem,
|
SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem,
|
||||||
|
|
@ -343,8 +343,7 @@ pub(crate) struct RunSettings {
|
||||||
pub(crate) install_mirrors: PythonInstallMirrors,
|
pub(crate) install_mirrors: PythonInstallMirrors,
|
||||||
pub(crate) refresh: Refresh,
|
pub(crate) refresh: Refresh,
|
||||||
pub(crate) settings: ResolverInstallerSettings,
|
pub(crate) settings: ResolverInstallerSettings,
|
||||||
pub(crate) env_file: Vec<PathBuf>,
|
pub(crate) env_file: EnvFile,
|
||||||
pub(crate) no_env_file: bool,
|
|
||||||
pub(crate) max_recursion_depth: u32,
|
pub(crate) max_recursion_depth: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -461,8 +460,7 @@ impl RunSettings {
|
||||||
resolver_installer_options(installer, build),
|
resolver_installer_options(installer, build),
|
||||||
filesystem,
|
filesystem,
|
||||||
),
|
),
|
||||||
env_file,
|
env_file: EnvFile::from_args(env_file, no_env_file),
|
||||||
no_env_file,
|
|
||||||
install_mirrors,
|
install_mirrors,
|
||||||
max_recursion_depth: max_recursion_depth.unwrap_or(Self::DEFAULT_MAX_RECURSION_DEPTH),
|
max_recursion_depth: max_recursion_depth.unwrap_or(Self::DEFAULT_MAX_RECURSION_DEPTH),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue