1use super::Command;
20use super::Mode;
21use thiserror::Error;
22use yash_env::Env;
23use yash_env::path::Path;
24use yash_env::path::PathBuf;
25use yash_env::semantics::ExitStatus;
26use yash_env::source::Location;
27use yash_env::source::pretty::{Report, ReportType, Snippet};
28use yash_env::system::Fstat;
29use yash_env::variable::HOME;
30use yash_env::variable::OLDPWD;
31
32#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
34#[non_exhaustive]
35pub enum Origin {
36 Home,
38 Oldpwd,
40 Cdpath,
42 Literal,
44}
45
46#[derive(Debug, Clone, Eq, Error, PartialEq)]
48#[non_exhaustive]
49pub enum TargetError {
50 #[error("$HOME not set")]
52 UnsetHome {
53 location: Location,
55 },
56
57 #[error("$OLDPWD not set")]
59 UnsetOldpwd {
60 location: Location,
62 },
63
64 #[error("change target contains non-existing directory component")]
70 NonExistingDirectory {
71 missing: PathBuf,
73 target: PathBuf,
75 location: Location,
77 },
78}
79
80impl TargetError {
81 #[must_use]
83 pub fn exit_status(&self) -> ExitStatus {
84 match self {
85 TargetError::UnsetHome { .. } | TargetError::UnsetOldpwd { .. } => {
86 super::EXIT_STATUS_UNSET_VARIABLE
87 }
88 TargetError::NonExistingDirectory { .. } => super::EXIT_STATUS_CANNOT_CANONICALIZE,
89 }
90 }
91
92 #[must_use]
94 pub fn to_report(&self) -> Report<'_> {
95 use TargetError::*;
96 let (location, label) = match self {
97 UnsetHome { location } => (
98 location,
99 "cd built-in used without operand requires non-empty $HOME".into(),
100 ),
101
102 UnsetOldpwd { location } => (location, "'-' operand requires non-empty $OLDPWD".into()),
103
104 NonExistingDirectory {
105 missing,
106 target: _,
107 location,
108 } => (
109 location,
110 format!("intermediate directory '{}' not found", missing.display()).into(),
111 ),
112 };
113
114 let mut report = Report::new();
115 report.r#type = ReportType::Error;
116 report.title = self.to_string().into();
117 report.snippets = Snippet::with_primary_span(location, label);
118 report
119 }
120}
121
122impl<'a> From<&'a TargetError> for Report<'a> {
123 #[inline]
124 fn from(error: &'a TargetError) -> Self {
125 error.to_report()
126 }
127}
128
129fn get_scalar<'a, S>(env: &'a Env<S>, name: &str) -> Option<&'a str> {
131 env.variables
132 .get_scalar(name)
133 .filter(|value| !value.is_empty())
134}
135
136pub fn target<S>(
145 env: &Env<S>,
146 command: &Command,
147 pwd: &str,
148) -> Result<(PathBuf, Origin), TargetError>
149where
150 S: Fstat,
151{
152 let (mut curpath, mut origin) = match &command.operand {
154 None => {
155 let home = get_scalar(env, HOME).ok_or_else(|| {
156 let builtin = env.stack.current_builtin();
157 let location =
158 builtin.map_or_else(|| Location::dummy(""), |b| b.name.origin.clone());
159 TargetError::UnsetHome { location }
160 })?;
161 (PathBuf::from(home), Origin::Home)
162 }
163
164 Some(operand) if operand.value == "-" => {
165 let oldpwd = get_scalar(env, OLDPWD).ok_or_else(|| TargetError::UnsetOldpwd {
166 location: operand.origin.clone(),
167 })?;
168 (PathBuf::from(&oldpwd), Origin::Oldpwd)
169 }
170
171 Some(operand) => (PathBuf::from(&operand.value), Origin::Literal),
172 };
173
174 if let Some(path) = super::cdpath::search(env, &curpath) {
176 curpath = path;
177 origin = Origin::Cdpath;
178 }
179
180 if command.mode == Mode::Physical {
181 return Ok((curpath, origin));
183 }
184
185 curpath = Path::new(pwd).join(curpath);
187 curpath = super::canonicalize::canonicalize(&env.system, &curpath).map_err(|e| {
194 TargetError::NonExistingDirectory {
195 missing: e.missing,
196 target: curpath,
197 location: {
198 let field = command.operand.as_ref();
199 let field = field.or_else(|| env.stack.current_builtin().map(|b| &b.name));
200 field.map_or_else(|| Location::dummy(""), |f| f.origin.clone())
201 },
202 }
203 })?;
204
205 Ok((curpath, origin))
206 }
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use yash_env::semantics::Field;
249 use yash_env::stack::Builtin;
250 use yash_env::stack::Frame;
251 use yash_env::variable::Scope;
252
253 #[test]
254 fn default_home() {
255 let mut env = Env::new_virtual();
256 let command = Command {
257 mode: Mode::default(),
258 ensure_pwd: false,
259 operand: None,
260 };
261 env.get_or_create_variable(HOME, Scope::Global)
262 .assign("/home/user", None)
263 .unwrap();
264
265 let target = target(&env, &command, "").unwrap();
266 assert_eq!(target, (PathBuf::from("/home/user"), Origin::Home));
267 }
268
269 #[test]
270 fn home_unset() {
271 let mut env = Env::new_virtual();
272 let command = Command {
273 mode: Mode::default(),
274 ensure_pwd: false,
275 operand: None,
276 };
277 let arg0 = Field::dummy("cd");
278 let location = arg0.origin.clone();
279 let env = env.push_frame(Frame::Builtin(Builtin {
280 name: arg0,
281 is_special: false,
282 }));
283
284 let e = target(&env, &command, "").unwrap_err();
285 assert_eq!(e, TargetError::UnsetHome { location });
286 }
287
288 #[test]
289 fn home_empty() {
290 let mut env = Env::new_virtual();
291 let command = Command {
292 mode: Mode::default(),
293 ensure_pwd: false,
294 operand: None,
295 };
296 let arg0 = Field::dummy("cd");
297 let location = arg0.origin.clone();
298 let mut env = env.push_frame(Frame::Builtin(Builtin {
299 name: arg0,
300 is_special: false,
301 }));
302 env.get_or_create_variable(HOME, Scope::Global)
303 .assign("", None)
304 .unwrap();
305
306 let e = target(&env, &command, "/ignored").unwrap_err();
307 assert_eq!(e, TargetError::UnsetHome { location });
308 }
309
310 #[test]
311 fn oldpwd() {
312 let mut env = Env::new_virtual();
313 let command = Command {
314 mode: Mode::default(),
315 ensure_pwd: false,
316 operand: Some(Field::dummy("-")),
317 };
318 env.get_or_create_variable(OLDPWD, Scope::Global)
319 .assign("/old/dir", None)
320 .unwrap();
321
322 let target = target(&env, &command, "/ignored").unwrap();
323 assert_eq!(target, (PathBuf::from("/old/dir"), Origin::Oldpwd));
324 }
325
326 #[test]
327 fn oldpwd_unset() {
328 let env = Env::new_virtual();
329 let operand = Field::dummy("-");
330 let location = operand.origin.clone();
331 let command = Command {
332 mode: Mode::default(),
333 ensure_pwd: false,
334 operand: Some(operand),
335 };
336
337 let e = target(&env, &command, "/ignored").unwrap_err();
338 assert_eq!(e, TargetError::UnsetOldpwd { location });
339 }
340
341 #[test]
342 fn oldpwd_empty() {
343 let mut env = Env::new_virtual();
344 let operand = Field::dummy("-");
345 let location = operand.origin.clone();
346 let command = Command {
347 mode: Mode::default(),
348 ensure_pwd: false,
349 operand: Some(operand),
350 };
351 env.get_or_create_variable(OLDPWD, Scope::Global)
352 .assign("", None)
353 .unwrap();
354
355 let e = target(&env, &command, "/ignored").unwrap_err();
356 assert_eq!(e, TargetError::UnsetOldpwd { location });
357 }
358
359 #[test]
360 fn literal_physical() {
361 let env = Env::new_virtual();
362
363 let result = target(
364 &env,
365 &Command {
366 mode: Mode::Physical,
367 ensure_pwd: false,
368 operand: Some(Field::dummy("foo")),
369 },
370 "/ignored",
371 )
372 .unwrap();
373 assert_eq!(result, (PathBuf::from("foo"), Origin::Literal));
374
375 let result = target(
376 &env,
377 &Command {
378 mode: Mode::Physical,
379 ensure_pwd: false,
380 operand: Some(Field::dummy("foo/bar")),
381 },
382 "/ignored",
383 )
384 .unwrap();
385 assert_eq!(result, (PathBuf::from("foo/bar"), Origin::Literal));
386 }
387
388 #[test]
389 fn literal_logical_absolute() {
390 let env = Env::new_virtual();
391
392 let result = target(
393 &env,
394 &Command {
395 mode: Mode::Logical,
396 ensure_pwd: false,
397 operand: Some(Field::dummy("/foo")),
398 },
399 "/ignored",
400 )
401 .unwrap();
402 assert_eq!(result, (PathBuf::from("/foo"), Origin::Literal));
403
404 let result = target(
405 &env,
406 &Command {
407 mode: Mode::Logical,
408 ensure_pwd: false,
409 operand: Some(Field::dummy("/foo/bar")),
410 },
411 "/ignored",
412 )
413 .unwrap();
414 assert_eq!(result, (PathBuf::from("/foo/bar"), Origin::Literal));
415 }
416
417 #[test]
418 fn literal_logical_relative() {
419 let env = Env::new_virtual();
421 let command = Command {
422 mode: Mode::Logical,
423 ensure_pwd: false,
424 operand: Some(Field::dummy("foo/bar")),
425 };
426
427 assert_eq!(
428 target(&env, &command, "/current/pwd").unwrap(),
429 (PathBuf::from("/current/pwd/foo/bar"), Origin::Literal)
430 );
431 }
432}