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?".
pwd/home/devThe 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.
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' .
lsREADME.md
build
build.sh
docs
notes.txt
srcThe 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?".
ls -ltotal 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 srcEach 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.
ls -a.
..
.hidden-config
README.md
build
build.sh
docs
notes.txt
srcThe 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).
ls -l build
ls -lh buildtotal 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.binThe 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.
ls -t
ls -lt
ls -ltrsrc
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 srcIn 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.
ls -R.:
README.md
build
build.sh
docs
notes.txt
src
./build:
app.bin
small.bin
./docs:
guide.md
./src:
Main.java
util
./src/util:
Helper.javaThe 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.
cd src
pwd
ls
cd util
pwd/home/dev/demo/src
Main.java
util
/home/dev/demo/src/utilcd 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.
pwd
cd /home/dev/demo/docs
pwd
cd -
pwd/home/dev/demo/src/util
/home/dev/demo/docs
/home/dev/demo/src/util
/home/dev/demo/src/utilThe 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.
cd ~
pwd
cd ~/demo/docs
pwd
cd
pwd
echo ~
cd ~/demo
pwd/home/dev
/home/dev/demo/docs
/home/dev
/home/dev
/home/dev/demoecho ~ 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.
pwd
ls /home/dev/demo/docs
ls docs
cd src/util
ls ..
ls ../../docs
ls ./Helper.java
cd ../..
pwd/home/dev/demo
guide.md
guide.md
Main.java
util
guide.md
./Helper.java
/home/dev/demoThe 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.
mkdir reports
mkdir archive/2026/09
mkdir -p archive/2026/09
mkdir -p archive/2026/09
ls -R archivemkdir: 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.
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-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 lineThe 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.
cp notes.txt notes-copy.txt
cp notes.txt docs/
ls -l notes.txt notes-copy.txt docs-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.txtThe 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.
cp src src-backup
cp -r src src-backup
ls -R src-backup
cp -r src src-backup
ls src-backupcp: -r not specified; omitting directory 'src'
src-backup:
Main.java
util
src-backup/util:
Helper.java
Main.java
src
utilThe 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.
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.txtcp: overwrite 'notes-copy.txt'? first line
second line
cp: overwrite 'notes-copy.txt'? changedThe 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.
cp -p notes.txt notes-kept.txt
ls -l notes.txt notes-kept.txt-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.txtBoth 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.
mv notes-copy.txt archive/
ls archive
ls notes-copy.txt2026
notes-copy.txt
ls: cannot access 'notes-copy.txt': No such file or directorymv 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.
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.txt2026
old-notes.txt
monthly-reports
-rw-r--r-- 1 root root 0 Oct 3 15:19 b.txtThe 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.
ls todo.txt other.txt
rm todo.txt
ls todo.txt
rm other.txt notes-kept.txt
lsother.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-backuprm 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.
rm src-backup
rmdir src-backup
rm -r src-backup
lsrm: 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
srcThe 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.
touch x1.tmp x2.tmp
printf 'n\ny\n' | rm -i x1.tmp x2.tmp
ls *.tmprm: remove regular empty file 'x1.tmp'? rm: remove regular empty file 'x2.tmp'? x1.tmpThe 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).
rm missing.txt; echo "exit status: $?"
rm -f missing.txt; echo "exit status: $?"
rm -f x1.tmp; echo "exit status: $?"
ls *.tmprm: 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 directoryWithout -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.
dir=""
echo rm -rf "$dir/"
echo rm -rf build .tmp
echo rm -rf build *.tmp
echo rm -rf build * .tmp
echo rm -rf ~/demo/docsrm -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/docsThe 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
lsorechowith 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 -riorrm -Iwhen deleting by hand. - Keep work in version control (lesson 12), so deleted tracked files can be restored.
Example 25: ln -s creates a symbolic link
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.
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.txtlrwxrwxrwx 1 root root 9 Sep 11 09:00 notes-link.txt -> notes.txt
first line
second line
notes.txtThe 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:
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-1cat: dangling.txt: No such file or directory
lcat 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.
Example 26: hard links share the same data
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.
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.txt2040716 -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 linels -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).
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.
├── 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 filesThe 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.
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.binREADME.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-streamText 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.
stat notes.txt 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 +0000The 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.
stat -c '%n: %s bytes, mode %a (%A), modified %y' notes.txt
stat -c '%i %h %n' notes.txtnotes.txt: 23 bytes, mode 644 (-rw-r--r--), modified 2026-09-10 08:00:00.000000000 +0000
2039832 1 notes.txtThe 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.
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 *.xyza1.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
*.xyzecho 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.
echo a?.log
echo a??.log
echo ??.log
echo b?.*a1.log a2.log
a10.log
a1.log a2.log b1.log
b1.log b2.txta?.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].
echo [ab]1.log
echo [a-b]2.*
echo [0-9]*.log
echo [!a]*
echo report-202[67].csva1.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.
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*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.bakThe 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
pwdprints the working directory, and relative paths resolve from it. Absolute paths start with/.ls -lcolumns: type and permissions, link count, owner, group, size in bytes, modification time, name.- Files whose names start with
.are hidden fromlsand from the glob*, and appear withls -a. cd -returns to the previous directory (OLDPWD), and~expands to the home directory before the command runs.mkdir -pcreates missing parents and succeeds when the directory exists.cp -ris required for directories, and an existing destination directory makescpcopy into it.mvrenames within a directory and moves across directories. It overwrites silently unless-ior-nis used.rmhas no trash.rm -rfcombined with an empty variable or a stray space in a glob deletes more than intended, so preview withechoand 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. fileidentifies content by its leading bytes, andstatshows inode, size, mode, links, and the access, modify, and change times.
Summary
pwdprints the current directory andcdchanges it.cdwith no argument orcd ~goes home, andcd -goes back.lsoptions:-llong format,-ahidden files,-hreadable sizes with-l,-tnewest first,-rreversed,-Rrecursive,-dthe directory itself..is the current directory and..is the parent directory.mkdir -p pathcreates all missing directories.touchcreates empty files and sets modification times, and-dsets a chosen time.cp source destcopies.-rcopies directories,-iasks before overwriting,-pkeeps times and modes, and-acombines them with link preservation.mv source destmoves and renames. Without-ior-nit replaces an existing destination.rmdeletes files,rm -rdeletes directories,rm -iasks, andrm -fignores missing files and never prompts.rmdirremoves only empty directories.ln -s target linkmakes a symbolic link, which stores a path.ln target linkmakes a hard link, which shares the inode and the data.treedraws a directory, with-Ldepth,-ahidden files,-Iexclusions, and-ddirectories only.filereports the type from the content, andstatshows 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 norm -I.
