Change the current working directory The most frequently typed command in a terminal β and the only one that cannot exist as a regular program.
cdis a shell builtin, always.
- What is cd?
- Why cd Must Be a Builtin
- How cd works internally
- Syntax
- Special Arguments
- All Options
- Environment Variables: HOME, CDPATH, OLDPWD
- Logical vs Physical Directory
- cd in Different Shells
- Modern Alternatives
- Related Commands
cd changes the current working directory of the shell process. Every process on Linux has a working directory β the directory used as the base when resolving relative paths.
cd is specified by POSIX and is present in every Unix shell: bash, zsh, dash, ksh, fish, and others. The behavior is mostly identical across shells, with small additions in bash and zsh.
This is the most fundamental fact about cd β and a classic interview question.
Every process on Linux has its own working directory, stored in the kernel. When a shell runs an external command, it forks a child process. The child can change its own working directory, but that change does not affect the parent (the shell).
shell (pid=1000, cwd=/home/alice)
βββ fork β child (pid=1001, cwd=/home/alice)
βββ chdir("/tmp") β only changes child's cwd
βββ exit
shell (pid=1000, cwd=/home/alice) β unchanged!
If cd were an external program, running it would:
- Fork a child process
- Child changes its own cwd to
/tmp - Child exits
- Shell's cwd is still
/home/aliceβ nothing changed
Solution: cd must run inside the shell process itself, using chdir() directly. That's what a builtin is β a command implemented inside the shell, not a separate executable.
type cd
# cd is a shell builtin
which cd # may return nothing, or a stub
/usr/bin/cd # this exists but is useless as a standalone programWhen you type cd /tmp:
- Shell parses the command, recognizes
cdas a builtin - Shell calls
chdir("/tmp")syscall directly (no fork) - On success: shell updates
$PWDand$OLDPWDenvironment variables - On failure: shell prints an error,
$PWDunchanged
chdir(path) β update $OLDPWD = $PWD β update $PWD = new path
The chdir() syscall:
- Takes an absolute or relative path
- Updates the process's working directory in the kernel
- Returns 0 on success, -1 on error (ENOENT, ENOTDIR, EACCES, etc.)
$PWD is maintained by the shell, not the kernel:
- The kernel tracks the real (physical) path
- The shell tracks the logical path (preserving symlink names)
pwd(builtin) shows$PWD(logical);pwd -Pshows the kernel's real path
cd [-L|-P] [-e] [-@] [directory]
- With no argument β goes to
$HOME - With
-β goes to previous directory ($OLDPWD) directorycan be absolute, relative, or a$CDPATHshortcut
cd # same as: cd $HOME or cd ~
cd ~ # explicit home
cd ~/docs # subdirectory of homecd /var/log
cd /etc
cd - # back to /var/log
cd - # back to /etc
# Toggles between two directories
# Also prints the directory it switched tocd ~root # /root
cd ~alice # /home/alice (if you have permission)
cd ~www-data # web server user's homecd .. # one level up
cd ../.. # two levels up
cd ../../etc # up two, then into etccd / # filesystem rootcd . # does nothing (stays in same dir)Follow the logical path. Symlink names are preserved in $PWD.
ln -s /var/log /tmp/logs
cd /tmp/logs # $PWD = /tmp/logs (logical)
cd -L /tmp/logs # sameResolve all symlinks. $PWD shows the real path.
ln -s /var/log /tmp/logs
cd -P /tmp/logs # $PWD = /var/log (physical/real)cd -Pe /tmp/logs # if physical path can't be determined, return errorcd -@ file.txt # treats file's extended attribute namespace as a directory
# macOS/zsh specific β rarely usedThe default destination for bare cd. Set at login by PAM/the system.
echo $HOME # /home/alice
cd # goes to $HOME
cd ~ # same
# Temporarily change home:
HOME=/tmp cd # doesn't work β builtin doesn't take env prefix
export HOME=/tmp && cd # changes home for sessionCurrent working directory β maintained by the shell (not the kernel).
echo $PWD # /home/alice/projects
pwd # same (reads $PWD)
pwd -P # physical path (resolves symlinks, asks kernel)The previous working directory β set by cd each time you change directories.
cd /var/log
cd /etc
echo $OLDPWD # /var/log
cd - # uses $OLDPWD to go backA colon-separated list of directories to search when cd is given a relative path that doesn't exist locally.
export CDPATH=".:$HOME:$HOME/projects:/var"
cd log # tries ./log, then ~/log, then ~/projects/log, then /var/log
# /var/log exists β goes there!
# Also prints the full path it resolved to$CDPATH is powerful but can cause surprising behavior β you type cd src expecting ./src but end up in ~/projects/src.
This distinction matters when symlinks are involved.
mkdir -p /real/path
ln -s /real/path /tmp/link
# Logical (default: -L)
cd /tmp/link
pwd # /tmp/link β shell's $PWD (preserves symlink name)
pwd -P # /real/path β kernel's real path
# Physical (-P)
cd -P /tmp/link
pwd # /real/path β $PWD is updated to real path
# Traversal difference:
cd /tmp/link
cd ..
pwd # /tmp β went up from the logical path
cd -P /tmp/link
cd ..
pwd # /real β went up from the real path- All standard options:
-L,-P,-e - Supports
$CDPATH cd -prints destination
- All bash options plus
-@(extended attributes on macOS) $CDPATHsupportedAUTO_CDoption: type a directory name withoutcdto navigatesetopt AUTO_CD /etc # same as: cd /etc .. # same as: cd ..
PUSHD_SILENT,AUTO_PUSHD: automatically push to directory stack
- No
-L/-Pflags (usesbuiltin cd) cdhshows recent directory history interactively- Abbreviation system can make
..work ascd ..
- Only
-Land-P(POSIX minimum) - No
$CDPATHrequired by POSIX (but usually supported)
- Same as bash options
cd old newβ replacesoldwithnewin current path:pwd # /home/alice/project/src cd src lib # goes to /home/alice/project/lib
Tools that enhance or replace cd with smarter navigation:
# After visiting /home/alice/projects/myapp a few times:
z myapp # jumps to /home/alice/projects/myapp
z proj # fuzzy matches most frequent dir with "proj"
# zoxide (Rust, faster z):
eval "$(zoxide init bash)" # add to ~/.bashrc
z myapp
zi myapp # interactive selection with fzfpushd /var/log # go to /var/log AND push current dir to stack
pushd /etc # go to /etc AND push /var/log
popd # return to /var/log (pop from stack)
popd # return to original dir
dirs # show directory stack
dirs -v # show with index numbers
cd ~2 # go to index 2 in stack (zsh)# Add to ~/.bashrc:
bind '"\C-f": "cd $(find . -type d | fzf)\n"'
# Ctrl+F β fuzzy search all subdirectoriesj myapp # jump to most used dir matching "myapp"
jc myapp # jump to child directory matching "myapp"
jo myapp # open directory in file managerbr # open interactive tree, navigate and cd| Command | Relation |
|---|---|
pwd |
Print current working directory ($PWD or physical) |
pushd |
Like cd but pushes to directory stack |
popd |
Return to previous directory from stack |
dirs |
Show directory stack |
ls |
List contents of a directory |
mkdir |
Create a directory to cd into |
realpath |
Resolve a path to its absolute physical form |
readlink -f |
Resolve symlinks in a path |
z / zoxide |
Smart frecency-based directory jumping |
See also:
examples.mdΒ·edge-cases.mdΒ·interview-questions.md