pwd, ls, and cd

Every terminal session starts somewhere, and that “somewhere” is real, trackable state — not just a visual illusion. pwd, ls, and cd are the three commands you will type more than any others in Linux: they tell you where you are, show you what is there, and let you move around. Master this trio and the rest of the command line — editing files, running scripts, managing permissions — becomes far easier, because almost everything else assumes you already know where you stand.

Overview / How It Works

Every running process on Linux — including your shell — has a piece of kernel-tracked state called its current working directory (cwd). When a process opens a file using a relative path like report.txt instead of /home/ada/report.txt, the kernel resolves that relative path against the process’s cwd. pwd (“print working directory”) simply prints that value. ls (“list”) reads the contents of a directory — by default, the cwd — and prints its entries. cd (“change directory”) updates the cwd for your shell.

That last point hides an important detail: cd is not an external program sitting in /bin or /usr/bin the way ls is. It is a shell builtin, implemented inside Bash itself. This has to be true, because of how processes work: when your shell runs an external command, it fork()s a brand-new child process and the child exec()s the program. A child process gets its own, independent cwd copied from the parent at the moment of the fork. If cd were an external program, it would call the chdir() system call on itself — the temporary child process — and then immediately exit, having changed nothing in your actual interactive shell. Because cd instead runs inside the shell’s own process and calls chdir() directly, the shell’s own cwd changes, and that change persists for every command you type afterward. This is also why cd cannot be meaningfully backgrounded or piped — there is no separate process to manage.

Paths come in two flavors. An absolute path starts with /, the filesystem root, and always means the same location no matter your cwd — /var/log/syslog is always the same file. A relative path is resolved against your current directory — logs/syslog means different things depending on where you are standing. Two special relative names exist in every directory: . means “this directory” and .. means “the parent directory.” The shell also expands ~ to your home directory (the value of $HOME) before any command ever sees it — that expansion is a shell feature, not something cd or ls does themselves.

pwd actually has two possible answers when a symlink is involved. If you cd into a directory through a symbolic link, the shell’s own bookkeeping (the $PWD variable) remembers the path as you typed it, symlink and all — that is the “logical” path, shown by pwd or pwd -L (the default). The “physical” path, shown by pwd -P, asks the kernel to resolve every symlink and prints the real location on disk. The two can differ, and this trips people up more often than you’d expect when working with symlinked directories.

ls does more work than it looks like. For each entry in a directory it reads (via the getdents system call under the hood), it can additionally call stat() to fetch metadata — permissions, owner, size, modification time — which is what powers the long listing format (-l). By default, ls hides any entry whose name starts with a dot (a “dotfile”), a long-standing Unix convention that programs rely on to keep configuration files like .bashrc or .gitignore out of normal directory listings.

Syntax

All three commands are simple to invoke, but each has options worth knowing well.

Command Option Meaning
pwd -L Print the logical path from $PWD (default) — keeps symlinks as typed
pwd -P Print the physical path, resolving all symbolic links
ls -l Long format: permissions, link count, owner, group, size, modification time, name
ls -a Show all entries, including hidden dotfiles, plus . and ..
ls -A Like -a, but omits . and ..
ls -h With -l, print sizes as human-readable (4.0K, 842K) instead of raw bytes
ls -t Sort by modification time, newest first
ls -S Sort by file size, largest first
ls -R List subdirectories recursively
ls -d List a directory entry itself, not its contents
ls --color=auto Colorize output by file type (often on by default via an alias)
cd directory Change to an absolute or relative path
cd (no argument) Change to $HOME
cd - Change to $OLDPWD, the previous directory, and print it

General forms:

pwd [-L | -P]
ls [OPTION]... [FILE]...
cd [-L | -P] [dir]

Examples

1. Printing where you are

pwd

Output:

/home/ada/projects/website

No arguments needed — pwd just reads $PWD and prints it. This is the fastest way to orient yourself, especially after a long series of cd commands or inside a script where you can’t be sure what the caller’s starting directory was.

2. Listing with detail and hidden files

ls -la ~/projects/website

Output:

total 44
drwxr-xr-x  6 ada ada 4096 Aug  4 09:02 .
drwxr-xr-x  5 ada ada 4096 Jul 28 14:11 ..
drwxr-xr-x  8 ada ada 4096 Aug  4 09:02 .git
-rw-r--r--  1 ada ada   23 Jul 20 10:15 .gitignore
-rw-r--r--  1 ada ada 1096 Aug  1 16:40 README.md
drwxr-xr-x  2 ada ada 4096 Jul 30 11:22 css
-rw-r--r--  1 ada ada 3820 Aug  4 08:55 index.html
drwxr-xr-x  2 ada ada 4096 Jul 30 11:25 js
-rw-r--r--  1 ada ada  612 Jul 22 09:03 package.json

The -l flag adds the long format: the first character of each line is the file type (d for directory, - for regular file), followed by nine permission characters, then the link count, owner, group, size in bytes, modification date, and name. The -a flag reveals .git and .gitignore — entries a bare ls would have hidden entirely, along with the . and .. self/parent entries.

3. Moving around and back

cd ~/projects/website
ls
cd ..
pwd
cd -
pwd

Output:

css  index.html  js  package.json  README.md
/home/ada/projects
/home/ada/projects/website
/home/ada/projects/website

cd ~/projects/website produces no output — success is silent, by Unix convention. The plain ls lists the five visible entries. cd .. moves up one level to the parent, which pwd confirms. cd - then jumps back to the directory you were in immediately before your last cd — Bash stores that in $OLDPWD — and, unlike a normal cd, it also prints the directory it switched to, which is why you see the path twice: once from cd - itself, once from the following pwd.

How It Works Step by Step

Consider running cd ~/projects/website && ls -l at a fresh prompt:

  1. Bash parses the line into two commands joined by &&, meaning the second only runs if the first succeeds (exits with status 0).
  2. Before cd ever runs, Bash performs tilde expansion, rewriting ~/projects/website to /home/ada/projects/website using $HOME. Neither cd nor the kernel ever sees the ~ character.
  3. cd, running as a builtin inside the shell’s own process, calls the chdir() system call with that absolute path. The kernel checks that the path exists, is a directory, and that the process has execute (search) permission on every directory component along the way. If any check fails, chdir() returns an error and cd reports it (for example, Permission denied or No such file or directory).
  4. On success, the kernel updates the shell process’s internal cwd reference, and the cd builtin updates the shell variables $OLDPWD (your previous location) and $PWD (your new one) — no extra syscall needed for those; they are shell bookkeeping.
  5. Because cd exited with status 0, && allows the second command to run.
  6. ls is not a builtin — it lives at /usr/bin/ls. Bash fork()s a child process and exec()s ls in it. That child inherits the shell’s cwd at the moment of the fork, so ls, given no path argument, lists the directory the shell just moved into.
  7. ls -l opens the directory, reads its raw entries with getdents(), then calls stat() on each one to gather the permission bits, owner, size, and timestamp needed for the long format, sorts the results alphabetically, and writes formatted lines to standard output.
  8. The child process exits with status 0; Bash reaps it and returns control to you at a prompt that is now, and remains, inside ~/projects/website.

Common Mistakes

1. Forgetting to quote a path variable

Unquoted variable expansion is subject to word splitting: Bash breaks the value on whitespace before passing it along as arguments.

target_dir="/home/ada/My Reports"
cd $target_dir
bash: cd: too many arguments

Bash splits /home/ada/My Reports into two words, /home/ada/My and Reports, and cd refuses to accept two positional arguments. Quoting the expansion keeps the value intact as a single word:

target_dir="/home/ada/My Reports"
cd "$target_dir"

2. Assuming a directory is empty because a bare ls shows nothing

mkdir -p ~/sandbox/config
touch ~/sandbox/config/.env
ls ~/sandbox/config
ls -a ~/sandbox/config
.  ..  .env

The first ls prints nothing at all, because .env starts with a dot and ls hides it by default — it looks like an empty directory but is not. The second command, with -a, reveals it. Check this before you delete or overwrite a directory you believe is empty.

3. Expecting cd inside a script to change your interactive shell

#!/usr/bin/env bash
set -euo pipefail
cd /var/www/app
git pull
$ pwd
/home/ada
$ ./deploy.sh
$ pwd
/home/ada

Running ./deploy.sh executes the script in its own child process, which gets its own cwd copied from your shell at the moment it was launched. The cd inside the script changes that process’s working directory, then the process exits and its cwd disappears with it — your interactive shell never moves. If you actually want the directory change to persist in your current shell, run the commands directly or source the script so it executes in your shell’s own process instead of a child:

source deploy.sh

Best Practices

  • Always quote path variables and command substitutions used with cd (cd "$dir"), so spaces and special characters don’t cause word splitting.
  • Use ls -lh for long listings so file sizes show as 4.0K or 842K instead of raw byte counts.
  • Reach for ls -la whenever you need to confirm a directory really is empty or check for configuration dotfiles.
  • Use cd - to bounce back to your previous directory instead of retyping a long path.
  • Prefer pwd -P when you specifically need the real, symlink-resolved location rather than the path you typed.
  • Don’t parse ls output in scripts — filenames can contain spaces, newlines, or start with dashes. Use globs (*.log) or find instead for reliable, script-safe file matching.
  • Use Tab completion when typing paths with cd or ls — it avoids typos and confirms a path exists before you commit to it.
  • Show your current directory in your shell prompt (PS1) so you rarely need to run a bare pwd just to orient yourself.

Practice Exercises

  • Starting from your home directory, create the nested path project/src/utils in one command, move into utils, and print your location two ways: once trusting $PWD as typed, and once resolving any symlinks to the real path.
  • A teammate claims a directory named build-cache is empty. Prove or disprove that using a single command, accounting for the possibility of hidden dotfiles.
  • Change into /var/log, produce a long, human-readable listing of its five most recently modified files sorted newest first, then return to whatever directory you started in — without typing that starting path out by hand.

Summary

  • pwd prints your shell’s current working directory; -P resolves symlinks to show the real path, while the default -L shows the path as typed.
  • ls lists directory contents; combine -l for detail, -a for hidden dotfiles, and -h for human-readable sizes.
  • cd is a shell builtin, not an external program, because it must change the shell process’s own working directory via chdir() — a child process could never do that on the shell’s behalf.
  • Paths are absolute (start with /) or relative to the current directory; ~ expands to $HOME, . means here, and .. means the parent directory.
  • cd - toggles back to $OLDPWD, the directory you were in before your last cd, and prints it.
  • Always quote path variables ("$dir") used with cd to avoid word splitting on spaces, and check for hidden files with -a before assuming a directory is empty.