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/issue
and 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/null
cd
to the containing directory of the target filebuiltin cd
calls thecd
provided by the shell instead of recursively calling thiscd
function
|| builtin cd "$@"
- If the group command above fails, then the
cd
target is not a file, so fall back to the regularcd
builtin.
- 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
cd
to 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
cd
command is to go to the home directory, so I madecd
without 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 cd
bypasses thecd
function 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 (
mv
requires 2 arguments)
- If there is more than 1 argument (
[[ -d "${@: -1}" ]]
- If the last argument is a directory
mv "$@"
- Call
mv
with all the arguments
- Call
builtin cd "${@: -1}"
cd
to the last argument
- Note: shellcheck complains about
"${@: -1}"
, but it is portable between both bash and zsh.