Implement cd -L and cd -P

Closes #7206
This commit is contained in:
Asuka Minato
2025-10-08 04:26:16 +09:00
committed by David Adam
parent e4123df7be
commit 0eb17fa58f
5 changed files with 87 additions and 18 deletions

View File

@@ -6,7 +6,7 @@ Synopsis
.. synopsis::
cd [DIRECTORY]
cd [( -L | --no-dereference ) | ( -P | --dereference )] [DIRECTORY]
Description
-----------
@@ -18,11 +18,22 @@ Description
``cd`` changes the current working directory.
The :envvar:`PWD` environment variable is updated with the new working directory, and the previous directory
is added to the :ref:`directory history <directory-history>`.
If *DIRECTORY* is given, it will become the new directory. If no parameter is given, the :envvar:`HOME` environment variable will be used.
If *DIRECTORY* is a relative path, all the paths in the :envvar:`CDPATH` will be tried as prefixes for it, in addition to :envvar:`PWD`.
It is recommended to keep **.** as the first element of :envvar:`CDPATH`, or :envvar:`PWD` will be tried last.
The new directory name is partially resolved to remove redundant segments (``.`` or ``..``).
``cd`` defaults to treating symbolic links as real directories, and not resolving them to their underlying
targets. The ``$PWD`` :ref:`special variable <variables-special>` variable will contain the path that was
supplied. This default behaviour can be enforced with the ``-L`` or ``--no-dereference`` option.
The ``-P`` or ``--dereference`` option resolves all symbolic links first. This was the default in fish versions before 3.0.0.
Fish will also try to change directory if given a command that looks like a directory (starting with **.**, **/** or **~**, or ending with **/**), without explicitly requiring **cd**.
Fish also ships a wrapper function around the builtin **cd** that understands ``cd -`` as changing to the previous directory.
@@ -45,6 +56,9 @@ Examples
cd /usr/src/fish-shell
# changes the working directory to /usr/src/fish-shell
cd -P /tmp/link
# resolves /tmp/link to its target before recording the directory
See Also
--------

View File

@@ -1,2 +1,4 @@
complete -c cd -a "(__fish_complete_cd)"
complete -c cd -s h -l help -d 'Display help and exit'
complete -c cd -s L -l no-dereference -d 'Change directory without resolving symbolic links'
complete -c cd -s P -l dereference -d 'Resolve symbolic links before changing directory'

View File

@@ -5,14 +5,6 @@
function cd --description "Change directory"
set -l MAX_DIR_HIST 25
if set -q argv[2]; and begin
set -q argv[3]
or not test "$argv[1]" = --
end
printf "%s\n" (_ "Too many args for cd command") >&2
return 1
end
# Skip history in subshells.
if status is-command-substitution
builtin cd $argv

View File

@@ -7,13 +7,20 @@
fds::{BEST_O_SEARCH, wopen_dir},
parser::ParserEnvSetMode,
path::path_apply_cdpath,
wutil::{normalize_path, wreadlink},
wutil::{normalize_path, wreadlink, wrealpath},
};
use errno::Errno;
use libc::{EACCES, ELOOP, ENOENT, ENOTDIR, EPERM};
use nix::unistd::fchdir;
use std::sync::Arc;
const SHORT_OPTIONS: &wstr = L!("hLP");
const LONG_OPTIONS: &[WOption] = &[
wopt(L!("help"), ArgType::NoArgument, 'h'),
wopt(L!("no-dereference"), ArgType::NoArgument, 'L'),
wopt(L!("dereference"), ArgType::NoArgument, 'P'),
];
// 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: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> BuiltinResult {
@@ -26,18 +33,43 @@ pub fn cd(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
return Err(STATUS_INVALID_ARGS);
};
let opts = HelpOnlyCmdOpts::parse(args, parser, streams)?;
let argc = args.len();
let mut deref_symlink = false;
let mut w = WGetopter::new(SHORT_OPTIONS, LONG_OPTIONS, args);
while let Some(opt) = w.next_opt() {
match opt {
'L' => deref_symlink = false,
'P' => deref_symlink = true,
'h' => {
builtin_print_help(parser, streams, cmd);
return Ok(SUCCESS);
}
';' => {
builtin_unexpected_argument(parser, streams, cmd, args[w.wopt_index - 1], false);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
builtin_unknown_option(parser, streams, cmd, args[w.wopt_index - 1], false);
return Err(STATUS_INVALID_ARGS);
}
_ => panic!("unexpected option {}", opt),
}
}
if opts.print_help {
builtin_print_help(parser, streams, cmd);
return Ok(SUCCESS);
let optind = w.wopt_index;
let non_option_argc = argc - optind;
if non_option_argc > 1 {
err_fmt!(Error::UNEXP_ARG_COUNT, 1, non_option_argc)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let vars = parser.vars();
let tmpstr;
let dir_in: &wstr = if args.len() > opts.optind {
args[opts.optind]
let dir_in: &wstr = if non_option_argc == 1 {
args[optind]
} else {
match vars.get_unless_empty(L!("HOME")) {
Some(v) => {
@@ -63,7 +95,15 @@ pub fn cd(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
return Err(STATUS_CMD_ERROR);
}
let pwd = vars.get_pwd_slash();
let mut pwd = vars.get_pwd_slash();
if deref_symlink {
if let Some(mut real_pwd) = wrealpath(&pwd) {
if !real_pwd.ends_with('/') {
real_pwd.push('/');
}
pwd = real_pwd;
}
}
let dirs = path_apply_cdpath(dir_in, &pwd, vars);
assert!(
@@ -120,10 +160,17 @@ pub fn cd(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
// Eventually fish will support distinct CWDs for different parsers in different threads.
parser.libdata_mut().cwd_fd = Some(Arc::new(fd));
let mut new_pwd = norm_dir;
if deref_symlink {
if let Some(real_dir) = wrealpath(&new_pwd) {
new_pwd = real_dir;
}
}
parser.set_var_and_fire(
L!("PWD"),
ParserEnvSetMode::new(EnvMode::EXPORT | EnvMode::GLOBAL),
vec![norm_dir],
vec![new_pwd],
);
return Ok(SUCCESS);
}

View File

@@ -55,6 +55,20 @@ else
end
# CHECKERR: pwd: realpath failed: {{.+}}
mkdir $real/subdir
cd $link
test "$PWD" = "$link" || echo "Default cd should keep symlink:"\n "\$PWD: $PWD"\n "\$link: $link"\n
cd -P $link
test "$PWD" = "$real" || echo "cd -P should resolve symlink:"\n "\$PWD: $PWD"\n "\$real: $real"\n
cd $link/subdir
test "$PWD" = "$link/subdir" || echo "Logical cd should keep subdir symlink:"\n "\$PWD: $PWD"\n "\$link/subdir: $link/subdir"\n
cd -P ..
test "$PWD" = "$real" || echo "cd -P .. should use physical parent:"\n "\$PWD: $PWD"\n "\$real: $real"\n
cd $link/subdir
cd -L ..
test "$PWD" = "$link" || echo "cd -L .. should use logical parent:"\n "\$PWD: $PWD"\n "\$link: $link"\n
cd $base
# Create a symlink and verify logical completion.
# create directory $base/through/the/looking/glass
# symlink $base/somewhere/rabbithole -> $base/through/the/looking/glass