#[derive(Debug, Clone, PartialEq)]
pub struct ParsedPathFile {
pub path: String,
pub env_vars: Vec<(String, String)>,
}
#[must_use]
pub fn format_path_file(path: &str) -> String {
format_path_file_with_env(path, &[])
}
#[must_use]
pub fn format_path_file_with_env(path: &str, env_vars: &[(String, String)]) -> String {
let mut output = String::from("PATH!\n");
for entry in path.split(':').filter(|s| !s.is_empty()) {
output.push_str(entry);
output.push('\n');
}
output.push_str("\nENV!\n");
for (key, value) in env_vars {
output.push_str(key);
output.push(' ');
output.push_str(value);
output.push('\n');
}
output
}
pub fn parse_path_file(content: &str) -> Result<ParsedPathFile, String> {
let trimmed = content.trim();
if trimmed.is_empty() {
return Err("No PATH entries found in file".to_string());
}
if trimmed.starts_with("PATH!") {
parse_new_format(trimmed)
} else {
parse_legacy_format(trimmed)
}
}
fn parse_new_format(content: &str) -> Result<ParsedPathFile, String> {
let mut path_entries = Vec::new();
let mut env_vars = Vec::new();
let mut in_path_section = false;
let mut in_env_section = false;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line == "PATH!" {
in_path_section = true;
in_env_section = false;
continue;
} else if line == "ENV!" {
in_path_section = false;
in_env_section = true;
continue;
}
if in_path_section {
path_entries.push(line.to_string());
} else if in_env_section {
if let Some(space_idx) = line.find(char::is_whitespace) {
let key = line[..space_idx].to_string();
let value = line[space_idx..].trim().to_string();
env_vars.push((key, value));
} else {
env_vars.push((line.to_string(), String::new()));
}
}
}
if path_entries.is_empty() {
return Err("No PATH entries found in file".to_string());
}
Ok(ParsedPathFile {
path: path_entries.join(":"),
env_vars,
})
}
fn parse_legacy_format(content: &str) -> Result<ParsedPathFile, String> {
let all_lines = content.lines().map(str::trim).collect::<Vec<_>>().join("");
let entries: Vec<&str> = all_lines
.split(':')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
if entries.is_empty() {
return Err("No PATH entries found in file".to_string());
}
Ok(ParsedPathFile {
path: entries.join(":"),
env_vars: Vec::new(), })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_path_file() {
let path = "/usr/bin:/bin:/usr/local/bin";
let formatted = format_path_file(path);
assert!(formatted.contains("PATH!"));
assert!(formatted.contains("/usr/bin\n"));
assert!(formatted.contains("/bin\n"));
assert!(formatted.contains("/usr/local/bin\n"));
assert!(formatted.contains("ENV!"));
}
#[test]
fn test_parse_path_file() {
let content = r#"PATH!
/usr/bin
/bin
/usr/local/bin
ENV!
"#;
let parsed = parse_path_file(content).unwrap();
assert_eq!(parsed.path, "/usr/bin:/bin:/usr/local/bin");
assert!(parsed.env_vars.is_empty());
}
#[test]
fn test_roundtrip() {
let original = "/usr/bin:/bin:/usr/local/bin:/opt/bin";
let formatted = format_path_file(original);
let parsed = parse_path_file(&formatted).unwrap();
assert_eq!(parsed.path, original);
assert!(parsed.env_vars.is_empty());
}
#[test]
fn test_parse_with_extra_whitespace() {
let content = r#"
PATH!
/usr/bin
/bin
/usr/local/bin
ENV!
"#;
let parsed = parse_path_file(content).unwrap();
assert_eq!(parsed.path, "/usr/bin:/bin:/usr/local/bin");
}
#[test]
fn test_parse_empty_file() {
let content = "";
let result = parse_path_file(content);
assert!(result.is_err());
assert!(result.unwrap_err().contains("No PATH entries"));
}
#[test]
fn test_parse_no_path_section() {
let content = "ENV!\nVAR=value\n";
let result = parse_path_file(content);
assert!(result.is_ok());
}
#[test]
fn test_parse_legacy_format_single_line() {
let content = "/usr/bin:/bin:/usr/local/bin";
let parsed = parse_path_file(content).unwrap();
assert_eq!(parsed.path, "/usr/bin:/bin:/usr/local/bin");
assert!(parsed.env_vars.is_empty());
}
#[test]
fn test_parse_legacy_format_with_whitespace() {
let content = " /usr/bin:/bin:/usr/local/bin \n";
let parsed = parse_path_file(content).unwrap();
assert_eq!(parsed.path, "/usr/bin:/bin:/usr/local/bin");
}
#[test]
fn test_parse_legacy_format_multiline() {
let content = "/usr/bin:/bin:\n/usr/local/bin:/opt/bin";
let parsed = parse_path_file(content).unwrap();
assert_eq!(parsed.path, "/usr/bin:/bin:/usr/local/bin:/opt/bin");
}
#[test]
fn test_parse_legacy_format_with_empty_entries() {
let content = "/usr/bin::/bin::::/usr/local/bin";
let parsed = parse_path_file(content).unwrap();
assert_eq!(parsed.path, "/usr/bin:/bin:/usr/local/bin");
}
#[test]
fn test_backward_compatibility_upgrade() {
let legacy_content = "/usr/bin:/bin:/usr/local/bin:/opt/homebrew/bin";
let parsed = parse_path_file(legacy_content).unwrap();
assert_eq!(parsed.path, legacy_content);
let new_content = "PATH!\n/usr/bin\n/bin\n/usr/local/bin\n/opt/homebrew/bin\n\nENV!\n";
let parsed_new = parse_path_file(new_content).unwrap();
assert_eq!(
parsed_new.path,
"/usr/bin:/bin:/usr/local/bin:/opt/homebrew/bin"
);
assert_eq!(parsed.path, parsed_new.path);
}
#[test]
fn test_parse_env_vars() {
let content = r#"PATH!
/usr/bin
/bin
ENV!
RUST_LOG debug
MY_VAR hello world
EMPTY_VAR
"#;
let parsed = parse_path_file(content).unwrap();
assert_eq!(parsed.path, "/usr/bin:/bin");
assert_eq!(parsed.env_vars.len(), 3);
assert_eq!(
parsed.env_vars[0],
("RUST_LOG".to_string(), "debug".to_string())
);
assert_eq!(
parsed.env_vars[1],
("MY_VAR".to_string(), "hello world".to_string())
);
assert_eq!(parsed.env_vars[2], ("EMPTY_VAR".to_string(), String::new()));
}
#[test]
fn test_parse_env_vars_with_comments() {
let content = r#"PATH!
/usr/bin
/bin
ENV!
# This is a comment
RUST_LOG debug
# Another comment
MY_VAR value
"#;
let parsed = parse_path_file(content).unwrap();
assert_eq!(parsed.env_vars.len(), 2);
assert_eq!(
parsed.env_vars[0],
("RUST_LOG".to_string(), "debug".to_string())
);
assert_eq!(
parsed.env_vars[1],
("MY_VAR".to_string(), "value".to_string())
);
}
#[test]
fn test_format_with_env_vars() {
let env_vars = vec![
("RUST_LOG".to_string(), "debug".to_string()),
("MY_VAR".to_string(), "hello world".to_string()),
];
let formatted = format_path_file_with_env("/usr/bin:/bin", &env_vars);
assert!(formatted.contains("PATH!"));
assert!(formatted.contains("/usr/bin\n"));
assert!(formatted.contains("/bin\n"));
assert!(formatted.contains("ENV!"));
assert!(formatted.contains("RUST_LOG debug\n"));
assert!(formatted.contains("MY_VAR hello world\n"));
}
#[test]
fn test_roundtrip_with_env_vars() {
let env_vars = vec![
("RUST_LOG".to_string(), "trace".to_string()),
("PATH_VAR".to_string(), "/some/path".to_string()),
];
let formatted = format_path_file_with_env("/usr/bin:/bin:/usr/local/bin", &env_vars);
let parsed = parse_path_file(&formatted).unwrap();
assert_eq!(parsed.path, "/usr/bin:/bin:/usr/local/bin");
assert_eq!(parsed.env_vars, env_vars);
}
#[test]
fn test_legacy_format_no_env_vars() {
let legacy_content = "/usr/bin:/bin:/usr/local/bin";
let parsed = parse_path_file(legacy_content).unwrap();
assert_eq!(parsed.path, "/usr/bin:/bin:/usr/local/bin");
assert!(parsed.env_vars.is_empty());
}
#[test]
fn test_env_var_with_special_chars() {
let content = r#"PATH!
/usr/bin
ENV!
VAR1 /path/to/file
VAR2 value=with=equals
VAR3 value:with:colons
"#;
let parsed = parse_path_file(content).unwrap();
assert_eq!(parsed.env_vars.len(), 3);
assert_eq!(
parsed.env_vars[0],
("VAR1".to_string(), "/path/to/file".to_string())
);
assert_eq!(
parsed.env_vars[1],
("VAR2".to_string(), "value=with=equals".to_string())
);
assert_eq!(
parsed.env_vars[2],
("VAR3".to_string(), "value:with:colons".to_string())
);
}
}