agent_shell_parser/
path.rs1use std::path::PathBuf;
2
3use crate::parse::{base_command, tokenize, Operator, ParsedPipeline};
4
5pub fn resolve_path(target: &str, base: &str) -> String {
10 if target.starts_with('/') {
11 target.to_string()
12 } else if target == "~" || target.starts_with("~/") {
13 if let Some(home) = std::env::var_os("HOME") {
14 let rest = target.strip_prefix("~/").unwrap_or("");
15 if rest.is_empty() {
16 home.to_string_lossy().to_string()
17 } else {
18 PathBuf::from(home).join(rest).to_string_lossy().to_string()
19 }
20 } else {
21 target.to_string()
22 }
23 } else {
24 PathBuf::from(base)
25 .join(target)
26 .to_string_lossy()
27 .to_string()
28 }
29}
30
31pub fn extract_cd_target(words: &[String]) -> Option<&str> {
33 words
34 .iter()
35 .find(|w| !w.starts_with('-') && *w != "cd")
36 .map(String::as_str)
37}
38
39pub fn extract_git_c_path(words: &[String]) -> Option<String> {
41 let git_idx = words.iter().position(|w| w == "git")?;
42 let mut i = git_idx + 1;
43 while i < words.len() {
44 if words[i] == "-C" {
45 return words.get(i + 1).cloned();
46 }
47 i += 1;
48 }
49 None
50}
51
52pub fn effective_cwd(pipeline: &ParsedPipeline, session_cwd: &str) -> Vec<String> {
64 let mut cwd = session_cwd.to_string();
65 let mut git_cwds: Vec<String> = Vec::new();
66
67 for (i, seg) in pipeline.segments.iter().enumerate() {
68 let words = tokenize(&seg.command);
69 if words.is_empty() {
70 if i < pipeline.operators.len() {
72 match pipeline.operators[i] {
73 Operator::And | Operator::Semi => {}
74 _ => cwd = session_cwd.to_string(),
75 }
76 }
77 continue;
78 }
79
80 let base = base_command(&seg.command);
81
82 if base == "cd" {
83 if let Some(target) = extract_cd_target(&words) {
84 cwd = resolve_path(target, &cwd);
85 }
86 }
87
88 if base == "git" {
89 let git_cwd = extract_git_c_path(&words);
90 let resolved = if let Some(path) = git_cwd {
91 if path.starts_with('/') {
92 path
93 } else {
94 PathBuf::from(&cwd)
95 .join(&path)
96 .to_string_lossy()
97 .to_string()
98 }
99 } else {
100 cwd.clone()
101 };
102 git_cwds.push(resolved);
103 }
104
105 if i < pipeline.operators.len() {
107 match pipeline.operators[i] {
108 Operator::And | Operator::Semi => {}
109 _ => cwd = session_cwd.to_string(),
110 }
111 }
112 }
113
114 if git_cwds.is_empty() {
115 vec![cwd]
116 } else {
117 git_cwds.dedup();
118 git_cwds
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
129 fn resolve_absolute() {
130 assert_eq!(resolve_path("/abs/path", "/base"), "/abs/path");
131 }
132
133 #[test]
134 fn resolve_relative() {
135 assert_eq!(resolve_path("subdir", "/base"), "/base/subdir");
136 }
137
138 #[test]
139 fn resolve_tilde_alone() {
140 let home = std::env::var("HOME").unwrap_or_default();
141 assert_eq!(resolve_path("~", "/base"), home);
142 }
143
144 #[test]
145 fn resolve_tilde_subdir() {
146 let home = std::env::var("HOME").unwrap_or_default();
147 let result = resolve_path("~/docs", "/base");
148 assert_eq!(result, format!("{home}/docs"));
149 }
150
151 #[test]
154 fn cd_target_normal() {
155 let words: Vec<String> = ["cd", "/foo"].iter().map(|s| s.to_string()).collect();
156 assert_eq!(extract_cd_target(&words), Some("/foo"));
157 }
158
159 #[test]
160 fn cd_target_with_flags() {
161 let words: Vec<String> = ["cd", "-L", "/foo"].iter().map(|s| s.to_string()).collect();
162 assert_eq!(extract_cd_target(&words), Some("/foo"));
163 }
164
165 #[test]
166 fn cd_target_no_args() {
167 let words: Vec<String> = ["cd"].iter().map(|s| s.to_string()).collect();
168 assert_eq!(extract_cd_target(&words), None);
169 }
170
171 #[test]
174 fn git_c_path_present() {
175 let words: Vec<String> = ["git", "-C", "/repo", "status"]
176 .iter()
177 .map(|s| s.to_string())
178 .collect();
179 assert_eq!(extract_git_c_path(&words), Some("/repo".to_string()));
180 }
181
182 #[test]
183 fn git_c_path_absent() {
184 let words: Vec<String> = ["git", "status"].iter().map(|s| s.to_string()).collect();
185 assert_eq!(extract_git_c_path(&words), None);
186 }
187
188 #[test]
189 fn git_c_path_multiple_flags() {
190 let words: Vec<String> = ["git", "--no-pager", "-C", "/repo", "log"]
191 .iter()
192 .map(|s| s.to_string())
193 .collect();
194 assert_eq!(extract_git_c_path(&words), Some("/repo".to_string()));
195 }
196}