Skip to content

Commit 18bd270

Browse files
authored
Normalize and expand relative-paths to absolute paths (#2933)
1 parent 63ca7ef commit 18bd270

File tree

2 files changed

+88
-7
lines changed

2 files changed

+88
-7
lines changed

crates/chat-cli/src/cli/chat/cli/clear.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ impl ClearArgs {
5757
session.tool_uses.clear();
5858
session.pending_tool_index = None;
5959
session.tool_turn_start_time = None;
60-
60+
6161
execute!(
6262
session.stderr,
6363
style::SetForegroundColor(Color::Green),

crates/chat-cli/src/util/directories.rs

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,45 @@ pub fn canonicalizes_path(os: &Os, path_as_str: &str) -> Result<String> {
185185
let context = |input: &str| Ok(os.env.get(input).ok());
186186
let home_dir = || os.env.home().map(|p| p.to_string_lossy().to_string());
187187

188-
Ok(shellexpand::full_with_context(path_as_str, home_dir, context)?.to_string())
188+
let expanded = shellexpand::full_with_context(path_as_str, home_dir, context)?;
189+
let path_buf = if !expanded.starts_with("/") {
190+
// Convert relative paths to absolute paths
191+
let current_dir = os.env.current_dir()?;
192+
current_dir.join(expanded.as_ref() as &str)
193+
} else {
194+
// Already absolute path
195+
PathBuf::from(expanded.as_ref() as &str)
196+
};
197+
198+
// Try canonicalize first, fallback to manual normalization if it fails
199+
match path_buf.canonicalize() {
200+
Ok(normalized) => Ok(normalized.as_path().to_string_lossy().to_string()),
201+
Err(_) => {
202+
// If canonicalize fails (e.g., path doesn't exist), do manual normalization
203+
let normalized = normalize_path(&path_buf);
204+
Ok(normalized.to_string_lossy().to_string())
205+
},
206+
}
207+
}
208+
209+
/// Manually normalize a path by resolving . and .. components
210+
fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
211+
let mut components = Vec::new();
212+
for component in path.components() {
213+
match component {
214+
std::path::Component::CurDir => {
215+
// Skip current directory components
216+
},
217+
std::path::Component::ParentDir => {
218+
// Pop the last component for parent directory
219+
components.pop();
220+
},
221+
_ => {
222+
components.push(component);
223+
},
224+
}
225+
}
226+
components.iter().collect()
189227
}
190228

191229
/// Given a globset builder and a path, build globs for both the file and directory patterns
@@ -445,28 +483,71 @@ mod tests {
445483

446484
// Test home directory expansion
447485
let result = canonicalizes_path(&test_os, "~/test").unwrap();
486+
#[cfg(windows)]
487+
assert_eq!(result, "\\home\\testuser\\test");
488+
#[cfg(unix)]
448489
assert_eq!(result, "/home/testuser/test");
449490

450491
// Test environment variable expansion
451492
let result = canonicalizes_path(&test_os, "$TEST_VAR/path").unwrap();
452-
assert_eq!(result, "test_value/path");
493+
#[cfg(windows)]
494+
assert_eq!(result, "\\test_value\\path");
495+
#[cfg(unix)]
496+
assert_eq!(result, "/test_value/path");
453497

454498
// Test combined expansion
455499
let result = canonicalizes_path(&test_os, "~/$TEST_VAR").unwrap();
500+
#[cfg(windows)]
501+
assert_eq!(result, "\\home\\testuser\\test_value");
502+
#[cfg(unix)]
456503
assert_eq!(result, "/home/testuser/test_value");
457504

505+
// Test ~, . and .. expansion
506+
let result = canonicalizes_path(&test_os, "~/./.././testuser").unwrap();
507+
#[cfg(windows)]
508+
assert_eq!(result, "\\home\\testuser");
509+
#[cfg(unix)]
510+
assert_eq!(result, "/home/testuser");
511+
458512
// Test absolute path (no expansion needed)
459513
let result = canonicalizes_path(&test_os, "/absolute/path").unwrap();
514+
#[cfg(windows)]
515+
assert_eq!(result, "\\absolute\\path");
516+
#[cfg(unix)]
460517
assert_eq!(result, "/absolute/path");
461518

462-
// Test relative path (no expansion needed)
519+
// Test ~, . and .. expansion for a path that does not exist
520+
let result = canonicalizes_path(&test_os, "~/./.././testuser/new/path/../../new").unwrap();
521+
#[cfg(windows)]
522+
assert_eq!(result, "\\home\\testuser\\new");
523+
#[cfg(unix)]
524+
assert_eq!(result, "/home/testuser/new");
525+
526+
// Test path with . and ..
527+
let result = canonicalizes_path(&test_os, "/absolute/./../path").unwrap();
528+
#[cfg(windows)]
529+
assert_eq!(result, "\\path");
530+
#[cfg(unix)]
531+
assert_eq!(result, "/path");
532+
533+
// Test relative path (which should be expanded because now all inputs are converted to
534+
// absolute)
463535
let result = canonicalizes_path(&test_os, "relative/path").unwrap();
464-
assert_eq!(result, "relative/path");
536+
#[cfg(windows)]
537+
assert_eq!(result, "\\relative\\path");
538+
#[cfg(unix)]
539+
assert_eq!(result, "/relative/path");
465540

466541
// Test glob prefixed paths
467542
let result = canonicalizes_path(&test_os, "**/path").unwrap();
468-
assert_eq!(result, "**/path");
543+
#[cfg(windows)]
544+
assert_eq!(result, "\\**\\path");
545+
#[cfg(unix)]
546+
assert_eq!(result, "/**/path");
469547
let result = canonicalizes_path(&test_os, "**/middle/**/path").unwrap();
470-
assert_eq!(result, "**/middle/**/path");
548+
#[cfg(windows)]
549+
assert_eq!(result, "\\**\\middle\\**\\path");
550+
#[cfg(unix)]
551+
assert_eq!(result, "/**/middle/**/path");
471552
}
472553
}

0 commit comments

Comments
 (0)