Improving the cd command
Originally published
Last modified
cd to a directory containing a file
Copying a path to a file and accidentally cd-ing to it is annoying, so I wanted to automatically cd to the directory containing the file.
This is possible with the Function Command Extension Trick:
# enable cd to directory containing file
cd() {
local parent
{
[[ ! -d "$1" ]] && [[ -e "$1" ]] &&
parent="$(dirname "$1")" &&
[[ "$parent" != . ]] && [[ -d "$parent" ]] &&
builtin cd "$parent" 2>/dev/null;
} ||
builtin cd "$@"
}Let's break the command down:
{ ... }- This creates a group command. The exit status of the group is the exit status of the last command inside.
- All the commands in this group are joined with
&&, so if a check fails, the rest of the commands are skipped.- Because all the commands in the group are joined with
&&, the grouping the commands with{ ... }is unnecessary, but I used it for readability.
- Because all the commands in the group are joined with
[[ ! -d "$1" ]] && [[ -e "$1" ]]- If the target is not a directory and if the target exists
- This matches both regular files like
/etc/issueand special files like/dev/sda
- This matches both regular files like
- If the target is not a directory and if the target exists
parent="$(dirname "$1")"- Get the path of the directory that contains the target file
[[ "$parent" != . ]]- If the containing directory of the target file is not the current directory
- This avoids accidentally
cd-ing to a file in the current directory when you expect it to be a folder
- This avoids accidentally
- If the containing directory of the target file is not the current directory
[[ -d "$parent" ]]- If the containing directory of the target file is an actual directory
- This prevents
cd-ing to a path that does not exist
- This prevents
- If the containing directory of the target file is an actual directory
builtin cd "$parent" 2>/dev/nullcdto the containing directory of the target filebuiltin cdcalls thecdprovided by the shell instead of recursively calling thiscdfunction
|| builtin cd "$@"- If the group command above fails, then the
cdtarget is not a file, so fall back to the regularcdbuiltin.
- If the group command above fails, then the
After implementing the first version of this function, I found myself frequently taking advantage of the ability to lazily copy full file paths with a double-click from command line output instead of having to meticulously create a selection from one precise character to another.
Extra features
With cd now being a function, the marginal cost of adding more features decreased, so I added some:
# enable cd to directory containing file; cd :/ to visit git root
# cd ..../ becomes ../../../ - every . after the first 2 goes up another directory
cd() {
local top parent
{ [[ "$1" = ":/" ]] && top="$(command git rev-parse --show-cdup)." && builtin cd "$top"; } || \
{ [[ ! -d "$1" ]] && [[ -e "$1" ]] && parent="$(dirname "$1")" && [[ "$parent" != . ]] && [[ -d "$parent" ]] && builtin cd "$parent" 2>/dev/null; } || \
{ [[ "$1" = '...'* ]] && command -v perl &>/dev/null && builtin cd "$(printf %s "$1" | perl -pe 's/\/(.*$)|(\.)(?=\.\.)/$2$2\/$1/g')"; } || \
builtin cd "$@"
}- I used to frequently
cdto the root of a git repository with permutations ofcd ../../..- Now, I just type
cd :/
- Now, I just type
- I have some local aliases for preparing a specific shell environment while also setting an iTerm tab color.
- The default behavior of a bare
cdcommand is to go to the home directory, so I madecdwithout arguments also strip the tab color
- The default behavior of a bare
mkcd
It's just what it sounds like: mkdir and cd
mkcd() {
mkdir -p "$1" && builtin cd "$1"
}- Calling
builtin cdbypasses thecdfunction above
mvcd
It's also just what it sounds like: mv a file and cd to the destination
mvcd() {
(( $# > 1 )) && [[ -d "${@: -1}" ]] && mv "$@" && builtin cd "${@: -1}"
}Breakdown:
(( $# > 1 ))- If there is more than 1 argument (
mvrequires 2 arguments)
- If there is more than 1 argument (
[[ -d "${@: -1}" ]]- If the last argument is a directory
mv "$@"- Call
mvwith all the arguments
- Call
builtin cd "${@: -1}"cdto the last argument
- Note: shellcheck complains about
"${@: -1}", but it is portable between both bash and zsh.