fish-shell/src/builtins/cd.rs

183 lines
5.5 KiB
Rust

// Implementation of the cd builtin.
use super::prelude::*;
use crate::{
env::{EnvMode, Environment},
fds::{wopen_dir, BEST_O_SEARCH},
path::path_apply_cdpath,
wutil::{normalize_path, wperror, wreadlink},
};
use errno::Errno;
use libc::{fchdir, EACCES, ELOOP, ENOENT, ENOTDIR, EPERM};
use std::{os::fd::AsRawFd, sync::Arc};
// The cd builtin. Changes the current directory to the one specified or to $HOME if none is
// specified. The directory can be relative to any directory in the CDPATH variable.
pub fn cd(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
let cmd = args[0];
let opts = match HelpOnlyCmdOpts::parse(args, parser, streams) {
Ok(opts) => opts,
Err(err @ Some(_)) if err != STATUS_CMD_OK => return err,
Err(err) => panic!("Illogical exit code from parse_options(): {err:?}"),
};
if opts.print_help {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
let vars = parser.vars();
let tmpstr;
let dir_in: &wstr = if args.len() > opts.optind {
args[opts.optind]
} else {
match vars.get_unless_empty(L!("HOME")) {
Some(v) => {
tmpstr = v.as_string();
&tmpstr
}
None => {
streams
.err
.append(wgettext_fmt!("%ls: Could not find home directory\n", cmd));
return STATUS_CMD_ERROR;
}
}
};
// Stop `cd ""` from crashing
if dir_in.is_empty() {
streams.err.append(wgettext_fmt!(
"%ls: Empty directory '%ls' does not exist\n",
cmd,
dir_in
));
if !parser.is_interactive() {
streams.err.append(parser.current_line());
};
return STATUS_CMD_ERROR;
}
let pwd = vars.get_pwd_slash();
let dirs = path_apply_cdpath(dir_in, &pwd, vars);
if dirs.is_empty() {
streams.err.append(wgettext_fmt!(
"%ls: The directory '%ls' does not exist\n",
cmd,
dir_in
));
if !parser.is_interactive() {
streams.err.append(parser.current_line());
}
return STATUS_CMD_ERROR;
}
let mut best_errno = 0;
let mut broken_symlink = WString::new();
let mut broken_symlink_target = WString::new();
for dir in dirs {
let norm_dir = normalize_path(&dir, true);
errno::set_errno(Errno(0));
let res = wopen_dir(&norm_dir, BEST_O_SEARCH).map_err(|err| err as i32);
let res = res.and_then(|fd| {
if unsafe { fchdir(fd.as_raw_fd()) } == 0 {
Ok(fd)
} else {
Err(errno::errno().0)
}
});
let fd = match res {
Ok(fd) => fd,
Err(err) => {
// Some errors we skip and only report if nothing worked.
// ENOENT in particular is very low priority
// - if in another directory there was a *file* by the correct name
// we prefer *that* error because it's more specific
if err == ENOENT {
let tmp = wreadlink(&norm_dir);
// clippy doesn't like this is_some/unwrap pair, but using if let is harder to read IMO
#[allow(clippy::unnecessary_unwrap)]
if broken_symlink.is_empty() && tmp.is_some() {
broken_symlink = norm_dir;
broken_symlink_target = tmp.unwrap();
} else if best_errno == 0 {
best_errno = errno::errno().0;
}
continue;
} else if err == ENOTDIR {
best_errno = err;
continue;
}
best_errno = err;
break;
}
};
// We need to keep around the fd for this directory, in the parser.
let dir_fd = Arc::new(fd);
// Stash the fd for the cwd in the parser.
parser.libdata_mut().cwd_fd = Some(dir_fd);
parser.set_var_and_fire(L!("PWD"), EnvMode::EXPORT | EnvMode::GLOBAL, vec![norm_dir]);
return STATUS_CMD_OK;
}
if best_errno == ENOTDIR {
streams.err.append(wgettext_fmt!(
"%ls: '%ls' is not a directory\n",
cmd,
dir_in
));
} else if !broken_symlink.is_empty() {
streams.err.append(wgettext_fmt!(
"%ls: '%ls' is a broken symbolic link to '%ls'\n",
cmd,
broken_symlink,
broken_symlink_target
));
} else if best_errno == ELOOP {
streams.err.append(wgettext_fmt!(
"%ls: Too many levels of symbolic links: '%ls'\n",
cmd,
dir_in
));
} else if best_errno == ENOENT {
streams.err.append(wgettext_fmt!(
"%ls: The directory '%ls' does not exist\n",
cmd,
dir_in
));
} else if best_errno == EACCES || best_errno == EPERM {
streams.err.append(wgettext_fmt!(
"%ls: Permission denied: '%ls'\n",
cmd,
dir_in
));
} else {
errno::set_errno(Errno(best_errno));
wperror(L!("cd"));
streams.err.append(wgettext_fmt!(
"%ls: Unknown error trying to locate directory '%ls'\n",
cmd,
dir_in
));
}
if !parser.is_interactive() {
streams.err.append(parser.current_line());
}
return STATUS_CMD_ERROR;
}