cd Command in Linux: Complete Guide to Changing Directories and Navigation Parameters

cd command in Linux and it perimeters

cd is the command I use before almost every other command in a terminal session, and yet for years I only knew cd path and cd ... It wasn’t until I actually read help cd in bash that I realized how much is packed into this tiny builtin — a directory stack, environment variables tracking your history, symbolic-link-aware navigation toggles, and a scripting-friendly return-to-previous shortcut. This guide covers all of it.

What Is the cd Command?

cd stands for “change directory.” Unlike rm, mv, ls, cp, chown, and chmod, cd is not a standalone executable — it’s a shell builtin, implemented directly inside bash, zsh, dash, and every other shell. I confirmed this directly:

$ type cd
cd is a shell builtin

This is not incidental — it’s a necessity. A process cannot change the working directory of its parent process; a working directory is per-process state, so if cd were an external program, running it would change the working directory of that separate child process and then immediately exit, having no effect at all on your interactive shell. Building cd into the shell itself is the only way cd somewhere can actually affect the shell you’re typing into.

Basic Syntax (Bash)

cd [-L|-P] [dir]
cd -

How cd Works Internally

Every process on Linux has a current working directory tracked in the kernel as part of its process state (visible under /proc/<pid>/cwd as a symlink). When you run cd, the shell issues the chdir() system call (or fchdir() if operating on a file descriptor) directly on itself, updating its own working directory. That’s the entire mechanism — no external binary is involved.

Bash also maintains a small amount of extra state around cd:

  • $PWD — updated to the new absolute path after every successful cd.
  • $OLDPWD — set to the previous working directory just before the change, which is what makes cd - possible.
  • $CDPATH — an optional colon-separated list of directories bash searches when you cd to a relative name that isn’t found directly under the current directory (similar in spirit to how $PATH works for commands).

I tested the basic navigation and the cd - shortcut directly:

$ pwd
/tmp
$ cd /home/claude
$ pwd
/home/claude
$ cd -
/tmp
$ pwd
/tmp

cd - printed the directory it switched to (/tmp) and jumped straight back to it — a shortcut for cd "$OLDPWD" that I use constantly when bouncing between two directories.

Parameters and Special Arguments

FormDescription
cd (no argument)Changes to $HOME
cd dirChanges to dir (relative or absolute)
cd -Changes to $OLDPWD (the previous directory) and prints its path
cd ..Changes to the parent directory
cd ~Changes to $HOME (tilde expansion, handled by the shell before cd even runs)
cd ~userChanges to user‘s home directory
-LFollow symbolic links logically (default) — .. after a symlinked path moves up the logical path, not the physical one
-PUse the physical directory structure, resolving symlinks before processing ..
-eWith -P, exit with non-zero status if the current directory could not be determined successfully
-@On systems that support it, present a file with extended attributes as a directory containing those attributes (rarely used)

The -L vs -P Distinction, Demonstrated

This is the single most misunderstood part of cd, so it’s worth being concrete about it. Say /srv/current is a symlink to /srv/releases/v2, and /srv/releases/v2 has a subdirectory logs.

$ cd /srv/current/logs   # logical path: /srv/current/logs
$ cd ..

With the default -L (logical) behavior, that cd .. takes you back to /srv/current — it works on the path as you typed it, symlink and all, simply removing the last component. With -P (physical), bash would first resolve /srv/current to its real target /srv/releases/v2, so cd .. from logs would land you in /srv/releases, a completely different directory. This distinction has genuinely bitten me once, in a deploy script that assumed logical navigation while running under a shell configured for physical (set -P) — the .. didn’t go where I expected.

Common Use Cases

  • Everyday interactive navigation between project, log, and config directories
  • Toggling quickly between two working locations using cd -
  • Navigating into a user’s home directory with cd ~username for administrative tasks
  • Setting up $CDPATH for faster navigation across a fixed set of frequently used base directories

Shell Scripting and Automation

A crucial habit for scripts: always check that cd succeeded before running anything else, since a failed cd that gets silently ignored can cause a script to run destructive commands in the wrong directory:

#!/usr/bin/env bash
set -euo pipefail

cd /opt/myapp/release || { echo "Failed to cd into release dir" >&2; exit 1; }
rm -rf ./build

With set -e active, a failed cd already aborts the script — but I still write the explicit || check in critical scripts for a clearer error message, since a bare failed cd under set -e can produce a confusing generic exit.

Returning to the original directory after a temporary detour — a very common pattern in build scripts:

pushd /tmp/build_workspace > /dev/null
# ... do work ...
popd > /dev/null

pushd/popd are bash builtins that maintain a directory stack, layered on top of the same cd mechanism, letting you nest multiple “go here temporarily, then come back” operations reliably, which plain cd - can’t do beyond a single level of history.

Using $CDPATH for faster navigation (interactive convenience, set in ~/.bashrc):

export CDPATH=".:$HOME/projects:/srv"

With that set, typing cd myproject from anywhere will find $HOME/projects/myproject automatically if it’s not in the current directory, without needing the full path.

Real-World System Administration Workflows

  • Deployment scripts: cd into a release directory before running build or install steps, always checked for success first.
  • Log investigation: quickly hopping with cd - between an application’s working directory and its log directory while debugging an incident.
  • Backup and restore scripts: using pushd/popd to temporarily move into a backup staging directory and reliably return afterward, even if an error occurs partway (combined with trap to guarantee cleanup).
  • Multi-step batch jobs: scripts that iterate over multiple project directories, cd-ing into each one to run a build or test step, then returning to a known base directory before moving to the next.

Comparing cd to Related Concepts

  • cd vs pushd/popd: cd only remembers one previous directory (via $OLDPWD, accessible with cd -); pushd/popd maintain an actual stack, letting you nest several levels of “go here, then come back” reliably.
  • cd vs chdir() in other languages: the underlying system call is the same one bash uses internally; in scripting or programming languages, calling chdir() only affects that process’s own working directory, exactly like the “can’t be an external binary” reasoning above.
  • cd vs find -exec for touching files “in another directory”: you often don’t need to cd at all — many commands accept a target path directly, and reaching for cd when a simple path argument would do adds unnecessary state to a script.

Troubleshooting Common cd Issues

“bash: cd: dir: No such file or directory”: typo, or a relative path that doesn’t exist from the current location — pwd first to confirm where you actually are.

“bash: cd: dir: Permission denied”: you need execute (x) permission on the target directory to enter it, not just read permission — see the chmod article for the directory-specific meaning of each bit.

cd “succeeds” in a script but subsequent commands act on the wrong directory: check whether the cd was run inside a subshell (e.g., inside $(...) or a pipeline segment) — subshells have their own working directory that doesn’t propagate back to the parent shell.

cd - doesn’t go where expected: $OLDPWD only tracks the single most recent previous directory; if you’ve cded multiple times since the one you wanted, cd - won’t reach further back — use pushd/popd or dirs -v for deeper history.

Performance Considerations

cd itself is essentially free — a single chdir() syscall with negligible overhead. The performance consideration that actually matters is $CDPATH: if misconfigured with many entries or slow network-mounted directories, bash has to check each one in sequence when resolving a relative target, which can introduce a noticeable pause on directories backed by slow or unreliable network filesystems (NFS especially).

Security Implications

Because cd resolves relative paths against the current working directory, scripts that cd into a directory based on unsanitized user input are a real risk — path traversal via ../../ sequences can move a script far outside its intended working area before running subsequent destructive commands. Always validate or canonicalize (realpath) any user-influenced path before cd-ing into it in a privileged script. Also, CDPATH including . (the current directory) as an early or default entry can occasionally cause a relative cd to resolve somewhere unexpected in edge cases — worth being deliberate about $CDPATH ordering if you use it.

Best Practices

  • Always check (or rely on set -e) that a cd in a script actually succeeded before proceeding.
  • Prefer pushd/popd over manual cd bookkeeping when a script needs to return to its starting directory reliably, especially with multiple nested detours.
  • Use cd -P deliberately (or set -P) when a script’s correctness depends on the physical filesystem structure rather than symlinked logical paths.
  • Canonicalize any user-influenced path with realpath before cd-ing into it in privileged contexts.
  • Use $CDPATH for personal interactive convenience, but avoid relying on it inside portable scripts, since it changes relative-path resolution in ways that can surprise someone reading the script later.

Compatibility Across Shells

cd as described here reflects bash’s implementation; POSIX sh/dash implement the same core behavior (cd, cd -, $OLDPWD, $PWD) since it’s specified by POSIX, but lack bash-specific extras like pushd/popd and some -L/-P nuances. zsh extends cd further with features like directory stack auto-tracking and spelling correction. Since cd is a shell builtin rather than a coreutils binary, its exact behavior and available flags depend on which shell you’re using — always check help cd (bash) or your shell’s manual rather than assuming coreutils-style --long-options, which builtins generally don’t support.

Advanced Scenarios I’ve Run Into

Verifying where you actually are after a symlinked cd: because logical navigation (-L, the default) preserves the symlinked path you typed rather than the resolved physical location, pwd alone can be ambiguous about which one you’re seeing. pwd -P always prints the physical path regardless of how you got there, while plain pwd (or pwd -L) prints the logical path bash has been tracking in $PWD:

$ cd /srv/current
$ pwd
/srv/current
$ pwd -P
/srv/releases/v2

Using dirs to inspect the full directory stack built up by pushd, which goes beyond what a single $OLDPWD/cd - can express:

$ pushd /var/log > /dev/null
$ pushd /etc > /dev/null
$ dirs -v
0  /etc
1  /var/log
2  /home/claude

Each pushd added a new entry at the top of the stack; dirs -v numbers them, and cd ~2 (in bash, using the ~N directory-stack shorthand) would jump directly to entry 2 without needing repeated popd calls.

Handling cd failures gracefully in interactive functions, a pattern I use in custom shell functions that need to guarantee they return to a known state even on error:

safe_cd_and_run() {
    local target="$1"
    local original
    original="$(pwd)"
    if ! cd "$target"; then
        echo "Could not enter $target" >&2
        return 1
    fi
    # ... do work ...
    cd "$original" || echo "Warning: failed to return to $original" >&2
}

Autocompletion and cd in Practice

Modern interactive shells (bash with programmable completion enabled, zsh by default) tab-complete directory names after cd, and many people rely on this so heavily that they rarely type a full path by hand. It’s worth knowing that completion is a separate layer built on top of the same chdir()-based cd builtin described throughout this guide — the completion system just helps assemble the argument bash then hands to cd, and understanding the underlying mechanics still matters the moment completion can’t help, such as constructing a path dynamically inside a script.

Summary

cd is deceptively simple on the surface and genuinely foundational underneath: it exists as a shell builtin specifically because changing a process’s working directory can only ever affect that same process, and bash layers useful state on top of the basic chdir() syscall — $PWD, $OLDPWD, cd -, and the logical/physical (-L/-P) navigation distinction. Understanding those mechanics explains both the everyday convenience of cd - and the occasional confusing edge case when symlinks, subshells, or unchecked failures are involved.

References

  • GNU Bash Reference Manual — Bourne Shell Builtins (cd): https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html
  • POSIX specification for cd: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/cd.html
  • chdir(2) system call documentation: https://man7.org/linux/man-pages/man2/chdir.2.html
  • Bash Directory Stack Builtins (pushd/popd/dirs): https://www.gnu.org/software/bash/manual/html_node/The-Directory-Stack.html
Total
0
Shares

Leave a Reply

Previous Post
unalias command in Linux and it perimeters

unalias Command in Linux: Complete Guide to Removing Shell Aliases and Parameters

Next Post
chmod command in Linux and it perimeters

chmod Command in Linux: Complete Guide to Changing File Permissions and Parameters

Related Posts