Guide contents

Lesson 1 of 14

Navigating and Managing Files

This lesson covers the commands a developer uses every day to find their place in the file system, list what is there, and create, copy, move, link, and delete files. After it, a reader can read a long ls -l line, explain the difference between an absolute and a relative path, and delete files without deleting the wrong ones.

Command Purpose
pwd Print the current directory
ls List files and directories
cd Change the current directory
mkdir Create directories
touch Create an empty file or update a file's timestamp
cp Copy files and directories
mv Move or rename files and directories
rm Delete files and directories
ln Create hard links and symbolic links
tree Draw a directory as a tree
file Detect the type of a file from its content
stat Show the metadata of a file
*, ?, [], {} Glob and brace patterns that the shell expands into file names

Conventions in this course. Commands run on Linux with GNU tools in the bash shell. Lines that start with # are comments and are not executed. The home directory in the output is /home/dev; on a reader's machine it is a different path. The ls -l owner and group columns show root root here, the account that ran the examples; a reader sees their own user name. The examples build on each other inside one working directory, so run them in order. Where macOS (BSD tools) differs, a note gives the macOS form, and the table at the end of the lesson lists the differences.

Example 1: pwd prints the working directory

Every shell session has a working directory, the directory that relative paths and commands like ls start from. The pwd command (print working directory) answers the question "where am I?".

Terminal
pwd
Output
/home/dev

The output is one absolute path. A new terminal normally starts in the home directory of the user, which is why this path ends in the user's name. The path starts with /, the root of the whole file system, and each / after that separates a directory name from the next. The shell also keeps this value in the variable PWD, which lesson 3 covers.

Example 2: ls lists a directory

The ls command lists the names of the files and directories inside a directory. Without arguments it lists the working directory. The next block first builds a small project to list. The mkdir, printf, head, and touch commands in the setup are explained in examples 12 and 13 and in later lessons; here they only create files with fixed sizes and dates so the listings below are repeatable.

Terminal
mkdir -p demo/src/util demo/docs demo/build
cd demo
printf 'Demo project\n' > README.md
printf 'first line\nsecond line\n' > notes.txt
printf 'debug=true\n' > .hidden-config
printf 'echo build\n' > build.sh
printf 'public class Main {}\n' > src/Main.java
printf 'class Helper {}\n' > src/util/Helper.java
printf '# Guide\n' > docs/guide.md
head -c 3000000 /dev/zero > build/app.bin
head -c 2048 /dev/zero > build/small.bin
touch -d '2026-09-01 10:00' README.md
touch -d '2026-09-02 11:30' notes.txt
touch -d '2026-09-03 09:15' build.sh
touch -d '2026-09-04 16:45' .hidden-config
touch -d '2026-09-05 08:00' src/Main.java src/util/Helper.java docs/guide.md build/app.bin build/small.bin
touch -d '2026-08-28 10:00' docs
touch -d '2026-08-29 10:00' build
touch -d '2026-09-07 10:00' src src/util
touch -d '2026-09-06 12:00' .
ls
Output
README.md
build
build.sh
docs
notes.txt
src

The project has two files at the top (README.md, notes.txt), a script (build.sh), and three directories (build, docs, src). ls prints one name per line here because its output goes to a file or a pipe, not to a terminal; on a terminal it prints names in columns. The names are sorted, and the sort order depends on the locale. This output uses the byte order (C locale), so README.md with a capital letter comes first. A typical desktop locale such as en_US.UTF-8 ignores case and prints build, build.sh, docs, notes.txt, README.md, src. The order of the other examples can differ the same way on a reader's machine.

ls does not show .hidden-config. The next example shows why.

Example 3: ls -l shows the long format

The -l option prints one line per entry with the type, permissions, owner, size, and date. It answers "what is this file, how big is it, and when did it change?".

Terminal
ls -l
Output
total 24
-rw-r--r-- 1 root root   13 Sep  1 10:00 README.md
drwxr-xr-x 2 root root 4096 Aug 29 10:00 build
-rw-r--r-- 1 root root   11 Sep  3 09:15 build.sh
drwxr-xr-x 2 root root 4096 Aug 28 10:00 docs
-rw-r--r-- 1 root root   23 Sep  2 11:30 notes.txt
drwxr-xr-x 3 root root 4096 Sep  7 10:00 src

Each line has seven columns.

Column Example Meaning
1 -rw-r--r-- The first character is the type: - regular file, d directory, l symbolic link. The next nine characters are permissions for the owner, the group, and everyone else, each as rwx (read, write, execute) or - when the permission is missing. Lesson 9 explains permissions in detail.
2 1, 2, 3 The number of hard links. For a directory it is 2 plus the number of subdirectories, so src shows 3 (it holds util).
3 root The owner.
4 root The group.
5 13 The size in bytes. README.md holds the 13 bytes of Demo project and a newline. A directory shows 4096, the space its entry list uses on this file system.
6 Sep 1 10:00 The time of the last content change (modification time).
7 README.md The name.

The first line, total 24, is the number of disk blocks (1 KiB units here) that the listed entries use. It counts allocated space, not the sum of the sizes, so it varies with the file system. ls -l shows a time such as Sep 1 10:00 for files changed in the last six months and a year such as Sep 1 2025 for older files, so a reader who repeats this later sees a year instead.

Example 4: ls -a shows hidden files

A file whose name starts with a dot is hidden: ls skips it by default. This convention keeps configuration files such as .gitignore and .bashrc out of normal listings. The -a option (all) includes them.

Terminal
ls -a
Output
.
..
.hidden-config
README.md
build
build.sh
docs
notes.txt
src

The new entries are .hidden-config, ., and ... The name . is the directory itself and .. is its parent directory. Every directory contains both, which is why they appear here. The option -A (almost all) lists hidden files but omits . and ... The options combine: ls -la is the long format including hidden files.

Example 5: ls -h prints human-readable sizes

Sizes in bytes get hard to read above a few thousand. The -h option converts them to units with a suffix and only has an effect together with -l (or -s).

Terminal
ls -l build
ls -lh build
Output
total 2936
-rw-r--r-- 1 root root 3000000 Sep  5 08:00 app.bin
-rw-r--r-- 1 root root    2048 Sep  5 08:00 small.bin
total 2.9M
-rw-r--r-- 1 root root 2.9M Sep  5 08:00 app.bin
-rw-r--r-- 1 root root 2.0K Sep  5 08:00 small.bin

The directory argument build makes ls list that directory instead of the working one. The first command prints 3000000 and 2048. The second prints 2.9M and 2.0K. The suffixes are powers of 1024: K is 1,024 bytes and M is 1,048,576 bytes, so 3,000,000 bytes is 2.86 MiB, rounded up to 2.9M. The option --si uses powers of 1000 instead and would print 3.0M. The total 2.9M line is also shown in human units.

Example 6: ls -t sorts by modification time

By default ls sorts by name. The -t option sorts by modification time, newest first, which answers "what changed most recently?". The option -r reverses any sort order.

Terminal
ls -t
ls -lt
ls -ltr
Output
src
build.sh
notes.txt
README.md
build
docs
total 24
drwxr-xr-x 3 root root 4096 Sep  7 10:00 src
-rw-r--r-- 1 root root   11 Sep  3 09:15 build.sh
-rw-r--r-- 1 root root   23 Sep  2 11:30 notes.txt
-rw-r--r-- 1 root root   13 Sep  1 10:00 README.md
drwxr-xr-x 2 root root 4096 Aug 29 10:00 build
drwxr-xr-x 2 root root 4096 Aug 28 10:00 docs
total 24
drwxr-xr-x 2 root root 4096 Aug 28 10:00 docs
drwxr-xr-x 2 root root 4096 Aug 29 10:00 build
-rw-r--r-- 1 root root   13 Sep  1 10:00 README.md
-rw-r--r-- 1 root root   23 Sep  2 11:30 notes.txt
-rw-r--r-- 1 root root   11 Sep  3 09:15 build.sh
drwxr-xr-x 3 root root 4096 Sep  7 10:00 src

In the first listing src comes first because its directory time (Sep 7) is the newest. Then come build.sh (Sep 3), notes.txt (Sep 2), README.md (Sep 1), build (Aug 29), and docs (Aug 28). The second command adds the long format so the dates are visible. The third adds -r, which prints the oldest first. The combination ls -ltr is common after a build or a download, because the file of interest ends up at the bottom of the screen, next to the next prompt. Two entries with the same time sort by name.

A directory's modification time changes when an entry is added to it, removed from it, or renamed in it, not when a file inside it changes content.

Example 7: ls -R lists directories recursively

The -R option (recursive) lists the directory, then every subdirectory below it, each under its own header.

Terminal
ls -R
Output
.:
README.md
build
build.sh
docs
notes.txt
src

./build:
app.bin
small.bin

./docs:
guide.md

./src:
Main.java
util

./src/util:
Helper.java

The header .: is the directory being listed (. is the working directory). Each following header such as ./src: is a subdirectory path, followed by its entries and a blank line. The listing shows that src contains Main.java and the directory util, and the header ./src/util: then lists Helper.java. For large trees ls -R is hard to read. Example 27 shows tree, and lesson 4 shows find.

Example 8: cd changes the working directory

The cd command (change directory) moves the shell into another directory. It prints nothing when it succeeds, so the examples print the result with pwd.

Terminal
cd src
pwd
ls
cd util
pwd
Output
/home/dev/demo/src
Main.java
util
/home/dev/demo/src/util

cd src is a relative path: it is resolved from the working directory. After it, pwd prints /home/dev/demo/src and ls lists the files there. The next cd util is again relative to the new directory. cd is a shell built-in, not a program, because a child program could not change the directory of its parent shell. cd with a name that does not exist prints an error and stays in place.

Example 9: cd - returns to the previous directory

The shell remembers the directory it was in before the last cd. The command cd - goes back to it and prints its path. This is the quickest way to switch between two distant directories.

Terminal
pwd
cd /home/dev/demo/docs
pwd
cd -
pwd
Output
/home/dev/demo/src/util
/home/dev/demo/docs
/home/dev/demo/src/util
/home/dev/demo/src/util

The first pwd shows /home/dev/demo/src/util, left from the previous example. After the jump to docs, cd - prints /home/dev/demo/src/util (the command echoes the destination) and moves there. A second cd - would return to docs, so repeating it toggles between the two places. The shell stores the previous directory in the variable OLDPWD.

Example 10: cd and cd ~ go to the home directory

The tilde ~ is a shortcut that the shell expands to the home directory of the current user. The command cd ~ and the command cd with no argument both go home. A path can start with ~, as in ~/demo/docs.

Terminal
cd ~
pwd
cd ~/demo/docs
pwd
cd
pwd
echo ~
cd ~/demo
pwd
Output
/home/dev
/home/dev/demo/docs
/home/dev
/home/dev
/home/dev/demo

echo ~ shows that the shell replaces the tilde with /home/dev before echo runs, so echo never sees the ~. The path ~/demo/docs is the home directory plus /demo/docs. The last command returns to the project directory for the next examples. The form ~name (for example ~root) expands to the home directory of another user.

Example 11: absolute and relative paths, . and ..

An absolute path starts with / and names a location from the root of the file system. It means the same thing from every directory. A relative path does not start with / and is resolved from the working directory. Two special names help in relative paths: . is the current directory and .. is the parent directory.

Terminal
pwd
ls /home/dev/demo/docs
ls docs
cd src/util
ls ..
ls ../../docs
ls ./Helper.java
cd ../..
pwd
Output
/home/dev/demo
guide.md
guide.md
Main.java
util
guide.md
./Helper.java
/home/dev/demo

The first two ls commands list the same directory. /home/dev/demo/docs is absolute and works anywhere, and docs is relative and works only from demo. After cd src/util, the path .. is src, so ls .. lists Main.java and util. The path ../../docs goes up two levels to demo and down into docs. The path ./Helper.java names a file in the current directory; the ./ prefix is optional for most commands but required to run a program from the working directory, for example ./build.sh. The final cd ../.. returns to demo.

Absolute paths are the safer choice in scripts that run from unknown directories, and relative paths are shorter at the keyboard.

Example 12: mkdir -p creates directories

The mkdir command creates a directory. By default it creates only the last component of the path and fails when the parent is missing. The option -p (parents) creates every missing parent and does not complain when the directory already exists.

Terminal
mkdir reports
mkdir archive/2026/09
mkdir -p archive/2026/09
mkdir -p archive/2026/09
ls -R archive
Output
mkdir: cannot create directory ‘archive/2026/09’: No such file or directory
archive:
2026

archive/2026:
09

archive/2026/09:

mkdir reports creates one directory and prints nothing. mkdir archive/2026/09 fails because archive and archive/2026 do not exist. With -p the same path is created in one command, and a second mkdir -p with the same path succeeds without a message, which makes -p safe to repeat in scripts. ls -R confirms the chain archive, 2026, 09. Brace expansion (example 33) creates several directories at once: mkdir -p project/{src,docs,tests}.

Example 13: touch creates files and updates timestamps

The touch command sets the access and modification times of a file to the current time, and creates the file, empty, when it does not exist. Developers use it mostly to create empty files. The option -d sets a chosen time instead of the current one.

Terminal
touch todo.txt a.txt b.txt
ls -l todo.txt a.txt b.txt
touch -d '2026-09-10 08:00' notes.txt
ls -l notes.txt
cat notes.txt
Output
-rw-r--r-- 1 root root 0 Oct  3 15:19 a.txt
-rw-r--r-- 1 root root 0 Oct  3 15:19 b.txt
-rw-r--r-- 1 root root 0 Oct  3 15:19 todo.txt
-rw-r--r-- 1 root root 23 Sep 10 08:00 notes.txt
first line
second line

The first command creates three files with size 0. The time column of their ls -l lines is the current time, so it differs on every run (this is true for every file created or copied without a fixed date in the examples below). The second touch uses -d on an existing file: the date column of notes.txt changes to Sep 10 08:00 and the content stays. cat notes.txt (lesson 5 covers cat) prints the two lines, which shows touch does not alter content. Without -d, touch notes.txt would set the time to now. Build tools such as make compare modification times, so touch is a way to mark a file as changed.

On macOS, touch -d does not accept this format; use touch -t 202609100800 (year, month, day, hour, minute).

Example 14: cp copies a file

The cp command copies a file. The syntax is cp source destination. When the destination is an existing directory, the copy goes into it and keeps its name.

Terminal
cp notes.txt notes-copy.txt
cp notes.txt docs/
ls -l notes.txt notes-copy.txt docs
Output
-rw-r--r-- 1 root root   23 Oct  3 15:19 notes-copy.txt
-rw-r--r-- 1 root root   23 Sep 10 08:00 notes.txt

docs:
total 8
-rw-r--r-- 1 root root  8 Sep  5 08:00 guide.md
-rw-r--r-- 1 root root 23 Oct  3 15:19 notes.txt

The first command creates notes-copy.txt with the same content. The second copies into the directory docs, where the file keeps the name notes.txt. The trailing / is optional but makes the intent clear, and cp fails with an error if docs does not exist instead of creating a file named docs. Both copies have the size 23. cp replaces an existing destination file without asking, and the copy gets the current time as its modification time. Examples 16 and 17 show how to prevent the first and change the second.

Example 15: cp -r copies a directory

Without -r (recursive), cp refuses to copy a directory. With -r it copies the directory and everything inside it.

Terminal
cp src src-backup
cp -r src src-backup
ls -R src-backup
cp -r src src-backup
ls src-backup
Output
cp: -r not specified; omitting directory 'src'
src-backup:
Main.java
util

src-backup/util:
Helper.java
Main.java
src
util

The first command fails with cp: -r not specified; omitting directory 'src' and copies nothing. The second creates src-backup with the same structure, and ls -R shows Main.java and util/Helper.java inside it. The third command shows a trap: when the destination directory already exists, cp -r src src-backup puts the copy inside it as src-backup/src. The last ls shows Main.java, src, and util. The rule is the same as for one file: an existing directory as destination means "copy into it". To copy only the contents of a directory, write cp -r src/. dest/.

Example 16: cp -i asks before overwriting

The -i option (interactive) makes cp ask before it replaces an existing file. The prompt reads an answer from the keyboard, and only y or Y at the start of the answer confirms. The example feeds the answers through a pipe (lesson 2 explains pipes) so the output is repeatable.

Terminal
printf 'changed\n' > other.txt
printf 'n\n' | cp -i other.txt notes-copy.txt
cat notes-copy.txt
printf 'y\n' | cp -i other.txt notes-copy.txt
cat notes-copy.txt
Output
cp: overwrite 'notes-copy.txt'? first line
second line
cp: overwrite 'notes-copy.txt'? changed

The prompt cp: overwrite 'notes-copy.txt'? goes to the standard error stream and ends without a newline, so the next output continues on the same line. After the answer n the file keeps its old content, first line and second line. After y it contains changed. The option -n never overwrites and asks nothing, and -b keeps a backup copy of the replaced file. Many Linux distributions define the alias cp as cp -i for the root user, which lesson 3 shows how to inspect.

Example 17: cp -p preserves timestamps and permissions

The -p option (preserve) keeps the mode, the owner when permitted, and the modification time of the original. Backups and deployments use it so that tools that compare times still see the original dates.

Terminal
cp -p notes.txt notes-kept.txt
ls -l notes.txt notes-kept.txt
Output
-rw-r--r-- 1 root root 23 Sep 10 08:00 notes-kept.txt
-rw-r--r-- 1 root root 23 Sep 10 08:00 notes.txt

Both lines show Sep 10 08:00, the time set in example 13. Without -p, the copy notes-copy.txt made in example 14 got the time of the copy command. The option -a (archive) is -r plus -p plus preservation of links, so cp -a src src-archive is the usual way to copy a whole tree faithfully.

Example 18: mv moves a file

The mv command moves a file or directory to a new location. The syntax matches cp: mv source destination. A move inside one file system only changes a directory entry, so it is fast for any file size.

Terminal
mv notes-copy.txt archive/
ls archive
ls notes-copy.txt
Output
2026
notes-copy.txt
ls: cannot access 'notes-copy.txt': No such file or directory

mv notes-copy.txt archive/ removes the file from the working directory and puts it into archive. The first ls shows it there, and the second ls fails with No such file or directory, because nothing remains at the old path. Like cp, mv replaces an existing destination without asking. The options -i (ask first) and -n (never overwrite) prevent that. Moving a directory needs no -r.

Example 19: mv renames files and directories

Linux has no separate rename command in the basic toolset. A rename is a move within the same directory: the destination is the new name.

Terminal
mv archive/notes-copy.txt archive/old-notes.txt
mv reports monthly-reports
mv a.txt b.txt
ls archive
ls -d monthly-reports
ls -l b.txt
Output
2026
old-notes.txt
monthly-reports
-rw-r--r-- 1 root root 0 Oct  3 15:19 b.txt

The first command renames a file inside archive. The second renames the directory reports to monthly-reports; ls -d lists the directory entry itself instead of its content. The third command shows the danger: b.txt already existed (example 13), so mv a.txt b.txt silently replaced it, and ls -l shows the file b.txt (size 0, the content of the former a.txt). The old b.txt is gone. Use mv -i or mv -n when the destination name may exist. Renaming many files at once is done with a loop or with rename, which lesson 13 touches on.

Example 20: rm deletes files

The rm command removes a file. There is no recycle bin: the directory entry is removed and the space is released at once. Recovery from a backup or a version-control system is the only way back.

Terminal
ls todo.txt other.txt
rm todo.txt
ls todo.txt
rm other.txt notes-kept.txt
ls
Output
other.txt
todo.txt
ls: cannot access 'todo.txt': No such file or directory
README.md
archive
b.txt
build
build.sh
docs
monthly-reports
notes.txt
src
src-backup

rm todo.txt prints nothing on success. The second ls todo.txt fails with No such file or directory, confirming the deletion. rm accepts several names, so rm other.txt notes-kept.txt deletes both. The final ls lists what remains. rm on a directory fails, which leads to the next example.

Example 21: rm -r deletes directories

The option -r (recursive) deletes a directory and everything inside it. Without it, rm refuses a directory. The command rmdir removes only empty directories and is a safer alternative for those.

Terminal
rm src-backup
rmdir src-backup
rm -r src-backup
ls
Output
rm: cannot remove 'src-backup': Is a directory
rmdir: failed to remove 'src-backup': Directory not empty
README.md
archive
b.txt
build
build.sh
docs
monthly-reports
notes.txt
src

The first command prints rm: cannot remove 'src-backup': Is a directory. The second prints rmdir: failed to remove 'src-backup': Directory not empty, because rmdir refuses to delete content. The third, rm -r, deletes the directory with src and util inside it, and the final ls no longer lists src-backup. The command walks the whole tree, so deleting a large tree takes time, and a wrong path deletes a lot.

Example 22: rm -i asks before each deletion

The -i option makes rm ask for every file. -I (capital i, GNU) asks once when deleting more than three files or when using -r, which is less tedious and still catches mistakes.

Terminal
touch x1.tmp x2.tmp
printf 'n\ny\n' | rm -i x1.tmp x2.tmp
ls *.tmp
Output
rm: remove regular empty file 'x1.tmp'? rm: remove regular empty file 'x2.tmp'? x1.tmp

The prompts rm: remove regular empty file 'x1.tmp'? and rm: remove regular empty file 'x2.tmp'? appear on the standard error stream. The answer n keeps the first file and y deletes the second. The final ls lists only x1.tmp. The wording regular empty file is the file type and size that rm found. A reader on macOS sees the shorter prompt remove x1.tmp?, and the option -I does not exist in the BSD rm.

Example 23: rm -f ignores missing files and skips prompts

The option -f (force) has two effects. It does not report an error when a file does not exist, and it never asks for confirmation, not even for write-protected files. The exit status of the command shows the difference. The variable $? holds the exit status of the previous command, where 0 means success (lesson 2 covers exit codes).

Terminal
rm missing.txt; echo "exit status: $?"
rm -f missing.txt; echo "exit status: $?"
rm -f x1.tmp; echo "exit status: $?"
ls *.tmp
Output
rm: cannot remove 'missing.txt': No such file or directory
exit status: 1
exit status: 0
exit status: 0
ls: cannot access '*.tmp': No such file or directory

Without -f, deleting a file that does not exist prints rm: cannot remove 'missing.txt': No such file or directory and returns status 1. With -f the command stays silent and returns 0, which makes it suitable for cleanup steps in scripts that run whether or not the file exists. The third command deletes x1.tmp, and the final ls fails because no .tmp file is left. The same -f also removes a read-only file without a prompt, so -f removes the last safety check that rm has.

Example 24: the risks of rm -rf

The combination rm -rf (recursive and forced) deletes a tree without prompts and without an error for missing paths. Most data-loss accidents with it come from an expansion that does not produce the expected path. The commands below print the command with echo instead of running it, which is the safe way to preview any rm.

Terminal
dir=""
echo rm -rf "$dir/"
echo rm -rf build .tmp
echo rm -rf build *.tmp
echo rm -rf build * .tmp
echo rm -rf ~/demo/docs
Output
rm -rf /
rm -rf build .tmp
rm -rf build *.tmp
rm -rf build README.md archive b.txt build build.sh docs monthly-reports notes.txt src .tmp
rm -rf /home/dev/demo/docs

The first line shows an empty variable: rm -rf "$dir/" becomes rm -rf /, a command that targets the whole file system. Modern GNU rm refuses / unless --no-preserve-root is given, but the same mistake with "$dir/src" becomes /src, and a partial path is deleted without any guard. The second line is the intended command and the third shows a glob that matches nothing (*.tmp) staying literal, which is harmless here. The last line shows that the shell expands ~ to the full home path before echo prints it, so the command receives an absolute path. The fourth line shows the classic typo: a space in * .tmp makes * a separate argument, and the shell expands it to every file in the directory (README.md, archive, build, and so on), so the command removes all of them. These habits reduce the risk:

  • Run ls or echo with the same arguments first, and check the expanded list.
  • Quote variables ("$dir") and fail on empty ones with ${dir:?}, shown in lesson 3.
  • Use rm -ri or rm -I when deleting by hand.
  • Keep work in version control (lesson 12), so deleted tracked files can be restored.

A symbolic link (symlink) is a small file that stores the path of another file. Opening the link follows the path to the target. The syntax is ln -s target linkname. Symlinks can point to directories and across file systems.

Terminal
ln -s notes.txt notes-link.txt
touch -h -d '2026-09-11 09:00' notes-link.txt
ls -l notes-link.txt
cat notes-link.txt
readlink notes-link.txt
Output
lrwxrwxrwx 1 root root 9 Sep 11 09:00 notes-link.txt -> notes.txt
first line
second line
notes.txt

The ls -l line starts with l (type link) and ends with notes-link.txt -> notes.txt, which means the link stores the path notes.txt. The size 9 is the length of that path in bytes. touch -h -d set a fixed date on the link itself (not on its target) only so the output stays repeatable. cat prints the content of the target, because opening a link opens what it points to. readlink prints the stored path.

A relative target such as notes.txt is resolved from the directory that holds the link, not from the directory where ln ran. If the target is deleted or the link is moved to another directory, the link dangles:

Terminal
cp notes.txt tmp-target.txt
ln -s tmp-target.txt dangling.txt
rm tmp-target.txt
cat dangling.txt
ls -l dangling.txt | cut -c1-1
Output
cat: dangling.txt: No such file or directory
l

cat fails with No such file or directory even though dangling.txt is listed, because the path it stores no longer leads anywhere. cut -c1-1 keeps the first character of the ls -l line, which is l, so the entry is still a link.

A hard link is a second name for the same file data. Both names point to one inode, the file system record that holds the size, owner, permissions, and the location of the data. The syntax is ln target linkname without -s. The number of names is the link count, the second column of ls -l.

Terminal
printf 'shared data\n' > data.txt
ln data.txt data-hard.txt
ls -li data.txt data-hard.txt
printf 'extra line\n' >> data.txt
cat data-hard.txt
rm data.txt
ls -l data-hard.txt
cat data-hard.txt
Output
2040716 -rw-r--r-- 2 root root 12 Oct  3 15:19 data-hard.txt
2040716 -rw-r--r-- 2 root root 12 Oct  3 15:19 data.txt
shared data
extra line
-rw-r--r-- 1 root root 23 Oct  3 15:19 data-hard.txt
shared data
extra line

ls -i prints the inode number in the first column. The numbers differ from machine to machine, but the two lines carry the same number, which is the proof that both names are one file. The link count in the third column is 2. The line appended through data.txt shows up when reading data-hard.txt, because there is only one set of data. After rm data.txt the link count drops to 1 and the data stays reachable through the other name. The system frees the data when the count reaches 0 and no process holds the file open.

Symbolic link Hard link
Stores A path (text) Another name for the same inode
Survives deletion of the original No, it dangles Yes
Can point to a directory Yes No (for ordinary users)
Can cross file systems Yes No
Shows as l and -> target in ls -l A regular file with link count 2 or more

Example 27: tree draws a directory as a tree

The tree command prints a directory and all the entries below it with lines that show the nesting, then a count of directories and files. It is usually not installed by default (sudo apt install tree on Debian and Ubuntu, brew install tree on macOS).

Terminal
rm -rf archive monthly-reports
rm -f b.txt dangling.txt data-hard.txt
tree
tree -L 1
tree -a -I 'build|docs' -L 2
Output
.
├── README.md
├── build
│   ├── app.bin
│   └── small.bin
├── build.sh
├── docs
│   ├── guide.md
│   └── notes.txt
├── notes-link.txt -> notes.txt
├── notes.txt
└── src
    ├── Main.java
    └── util
        └── Helper.java

5 directories, 10 files
.
├── README.md
├── build
├── build.sh
├── docs
├── notes-link.txt -> notes.txt
├── notes.txt
└── src

4 directories, 4 files
.
├── .hidden-config
├── README.md
├── build.sh
├── notes-link.txt -> notes.txt
├── notes.txt
└── src
    ├── Main.java
    └── util

3 directories, 6 files

The first lines clean up the files that earlier examples left over, so the tree stays small. The plain tree prints each name with ├── for an entry that has following siblings and └── for the last one, and the │ bar continues the parent's line. The last line of each listing, such as 5 directories, 10 files, counts the entries shown. Common options are in the table.

Option Effect
-L 1 Limit the depth to 1 level
-a Include hidden files
-d Show directories only
-I 'a|b' Exclude names that match the pattern
-h Show human-readable sizes
-f Print the full path of each entry

In the last command, -a adds .hidden-config, -I 'build|docs' hides two directories, and -L 2 stops at two levels. The quotes keep the shell from treating | as a pipe.

Example 28: file detects the type of a file

The file command reads the first bytes of a file and reports its type from a database of known patterns, instead of trusting the name. It answers "what is this file?" for an unknown download or a file without an extension.

Terminal
file README.md build.sh src/Main.java build/app.bin docs notes-link.txt
file -b notes.txt
file --mime-type notes.txt build/app.bin
Output
README.md:      ASCII text
build.sh:       ASCII text
src/Main.java:  ASCII text
build/app.bin:  data
docs:           directory
notes-link.txt: symbolic link to notes.txt
ASCII text
notes.txt:     text/plain
build/app.bin: application/octet-stream

Text files are reported as ASCII text (or UTF-8 Unicode text when they contain non-ASCII characters). The file build/app.bin, which holds only zero bytes, has no recognizable pattern and is reported as data, the name file gives to unknown binary content. The directory docs is reported as directory and the symbolic link as symbolic link to notes.txt. The option -b (brief) leaves out the file name, which suits scripts. The option --mime-type prints the internet media type, such as text/plain and application/octet-stream. On a real project, file identifies executables (ELF 64-bit LSB pie executable), archives, images, and scripts with a shebang line (Bourne-Again shell script). Its answer depends on content, so a PNG renamed to photo.txt is still reported as a PNG.

Example 29: stat shows the metadata of a file

The stat command prints everything the file system stores about a file other than its content. It answers questions that ls -l cannot, such as the inode number and the exact timestamps.

Terminal
stat notes.txt
Output
  File: notes.txt
  Size: 23        	Blocks: 8          IO Block: 4096   regular file
Device: 254,0	Inode: 2039832     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2026-10-03 15:19:43.535773699 +0000
Modify: 2026-09-10 08:00:00.000000000 +0000
Change: 2026-10-03 15:19:43.531773699 +0000
 Birth: 2026-10-03 15:19:43.467390381 +0000

The lines mean the following. Several values vary between machines and runs, as marked.

Line Meaning
File: The name
Size: Size in bytes (23), the number of 512-byte blocks allocated, the I/O block size, and the type
Device: and Inode: The device and the inode number; they vary
Links: The hard link count
Access: (first line) The permission mode in octal (0644) and symbolic (-rw-r--r--) form
Uid: and Gid: The owner and group, as number and name; they vary
Access: (time) The last time the content was read; it varies
Modify: The last time the content changed (the time touch -d set)
Change: The last time the metadata changed (permissions, owner, name, link count); it varies and cannot be set by touch
Birth: The creation time, when the file system records it; it varies

The option -c (GNU) formats the output with a template, which is handy in scripts.

Terminal
stat -c '%n: %s bytes, mode %a (%A), modified %y' notes.txt
stat -c '%i %h %n' notes.txt
Output
notes.txt: 23 bytes, mode 644 (-rw-r--r--), modified 2026-09-10 08:00:00.000000000 +0000
2039832 1 notes.txt

The format codes are %n name, %s size, %a permissions in octal, %A permissions as text, %y modification time, %i inode number, and %h hard link count. The second line prints three values separated by spaces. On macOS, stat has a different syntax: stat -f '%N: %z bytes, mode %Lp, modified %Sm' notes.txt.

Example 30: the glob * matches any characters

A glob is a pattern that contains wildcard characters. The shell, not the command, expands a glob into the sorted list of matching file names before it starts the command. The star * matches any sequence of characters, including none. The examples use a new directory.

Terminal
cd ~/demo
mkdir -p globs && cd globs
touch a1.log a2.log a10.log b1.log b2.txt report-2026.csv report-2027.csv .env
echo *.log
echo b*
echo *
ls *.csv
echo *.xyz
Output
a1.log a10.log a2.log b1.log
b1.log b2.txt
a1.log a10.log a2.log b1.log b2.txt report-2026.csv report-2027.csv
report-2026.csv
report-2027.csv
*.xyz

echo is used to display the expansion that ls or any other command would receive. *.log expands to the four .log files, sorted by name. In byte order a1.log comes before a10.log because the dot sorts before the digit 0, and a10.log comes before a2.log. b* matches both b files. A bare * lists all files except .env, because a star does not match a leading dot. A pattern that matches nothing, such as *.xyz, stays as typed in bash, so the command receives the literal text *.xyz and usually fails with No such file or directory. A star never crosses a /: */*.java is needed for one directory down.

Example 31: the glob ? matches one character

The question mark ? matches exactly one character. It is stricter than * and picks files with names of a known length.

Terminal
echo a?.log
echo a??.log
echo ??.log
echo b?.*
Output
a1.log a2.log
a10.log
a1.log a2.log b1.log
b1.log b2.txt

a?.log matches a1.log and a2.log but not a10.log, which has two characters between a and the dot. a??.log matches only a10.log. ??.log matches the names with two characters before .log: a1, a2, and b1. b?.* combines both wildcards: one character after b, a dot, and anything after it, so b1.log and b2.txt match.

Example 32: the glob [] matches one character from a set

Square brackets match one character that is in the list or range. A leading ! or ^ negates the set. Ranges use -, as in [0-9] or [a-c].

Terminal
echo [ab]1.log
echo [a-b]2.*
echo [0-9]*.log
echo [!a]*
echo report-202[67].csv
Output
a1.log b1.log
a2.log b2.txt
[0-9]*.log
b1.log b2.txt report-2026.csv report-2027.csv
report-2026.csv report-2027.csv

[ab]1.log matches a1.log and b1.log. [a-b]2.* matches the files that start with a or b followed by 2., which are a2.log and b2.txt. [0-9]*.log matches nothing here, because no name starts with a digit, so the text stays literal. [!a]* matches every name that does not start with a. report-202[67].csv matches the years 2026 and 2027. Character ranges depend on the locale in some shells, so [a-z] can include uppercase letters in non-C locales; the class [[:lower:]] is stable.

Example 33: brace expansion {a,b} generates names

Braces are not a glob. They are brace expansion: the shell generates the listed words whether or not files exist. It is used to create several files or directories and to avoid repeating a long path.

Terminal
echo report-202{6,7}.csv
echo {a,b}{1,2}
touch file-{1..3}.txt
ls file-*
mkdir -p project/{src,docs,tests}
ls project
cp report-2026.csv{,.bak}
ls report-2026*
Output
report-2026.csv report-2027.csv
a1 a2 b1 b2
file-1.txt
file-2.txt
file-3.txt
docs
src
tests
report-2026.csv
report-2026.csv.bak

The first line produces both file names without looking at the disk. {a,b}{1,2} produces every combination: a1 a2 b1 b2. The range {1..3} produces 1 2 3, so touch creates file-1.txt, file-2.txt, and file-3.txt. mkdir -p project/{src,docs,tests} creates three directories in one command. The form cp report-2026.csv{,.bak} expands to cp report-2026.csv report-2026.csv.bak, a common way to make a quick backup copy. Brace expansion happens before globbing, and it is a feature of bash and zsh, not of the minimal POSIX sh.

Quick reference for macOS (BSD tools)

Task Linux (GNU) macOS (BSD)
Colored ls ls --color=auto ls -G
Set a file time touch -d '2026-09-10 08:00' f touch -t 202609100800 f
Custom stat output stat -c '%s' f stat -f '%z' f
Ask once before a bulk delete rm -I Not available
Sort ls by size ls -lS ls -lS
Install tree Package manager brew install tree
Recursive copy keeping everything cp -a cp -a (works on current macOS)
readlink for a full path readlink -f link readlink link or realpath on current versions

Interview points

  • pwd prints the working directory, and relative paths resolve from it. Absolute paths start with /.
  • ls -l columns: type and permissions, link count, owner, group, size in bytes, modification time, name.
  • Files whose names start with . are hidden from ls and from the glob *, and appear with ls -a.
  • cd - returns to the previous directory (OLDPWD), and ~ expands to the home directory before the command runs.
  • mkdir -p creates missing parents and succeeds when the directory exists. cp -r is required for directories, and an existing destination directory makes cp copy into it.
  • mv renames within a directory and moves across directories. It overwrites silently unless -i or -n is used.
  • rm has no trash. rm -rf combined with an empty variable or a stray space in a glob deletes more than intended, so preview with echo and quote variables.
  • A symlink stores a path and can dangle. A hard link is another name for one inode: the data stays until the last name is removed.
  • The shell expands *, ?, [], and {} before the command starts, so the command never sees the pattern when files match. An unmatched glob stays literal in bash.
  • file identifies content by its leading bytes, and stat shows inode, size, mode, links, and the access, modify, and change times.

Summary

  • pwd prints the current directory and cd changes it. cd with no argument or cd ~ goes home, and cd - goes back.
  • ls options: -l long format, -a hidden files, -h readable sizes with -l, -t newest first, -r reversed, -R recursive, -d the directory itself.
  • . is the current directory and .. is the parent directory.
  • mkdir -p path creates all missing directories. touch creates empty files and sets modification times, and -d sets a chosen time.
  • cp source dest copies. -r copies directories, -i asks before overwriting, -p keeps times and modes, and -a combines them with link preservation.
  • mv source dest moves and renames. Without -i or -n it replaces an existing destination.
  • rm deletes files, rm -r deletes directories, rm -i asks, and rm -f ignores missing files and never prompts. rmdir removes only empty directories.
  • ln -s target link makes a symbolic link, which stores a path. ln target link makes a hard link, which shares the inode and the data.
  • tree draws a directory, with -L depth, -a hidden files, -I exclusions, and -d directories only.
  • file reports the type from the content, and stat shows size, inode, mode, owner, link count, and the access, modify, and change times.
  • * matches any characters, ? matches one, [abc] matches one of a set, and {a,b} generates words. None of the glob characters match a leading dot or a /.
  • Examples that need macOS forms: touch -t, stat -f, ls -G, and no rm -I.