diff --git a/doc_src/cmds/cd.rst b/doc_src/cmds/cd.rst index 52ae89561..2992d05b3 100644 --- a/doc_src/cmds/cd.rst +++ b/doc_src/cmds/cd.rst @@ -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 `. + 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 ` 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 -------- diff --git a/share/completions/cd.fish b/share/completions/cd.fish index 11cb16e56..026139ce8 100644 --- a/share/completions/cd.fish +++ b/share/completions/cd.fish @@ -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' diff --git a/share/functions/cd.fish b/share/functions/cd.fish index be9f00683..7ad6239ed 100644 --- a/share/functions/cd.fish +++ b/share/functions/cd.fish @@ -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 diff --git a/src/builtins/cd.rs b/src/builtins/cd.rs index d5468eca2..3d2c07c25 100644 --- a/src/builtins/cd.rs +++ b/src/builtins/cd.rs @@ -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); } diff --git a/tests/checks/cd.fish b/tests/checks/cd.fish index daf3a5462..e6cded924 100644 --- a/tests/checks/cd.fish +++ b/tests/checks/cd.fish @@ -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