BSCS and BSIT students who need to actually get comfortable at a Linux prompt — not just pass an Operating Systems exam about one — can work through “The Linux Command Line” by William Shotts, a free, complete, 36-chapter introduction published directly by its author. It assumes no prior Linux or programming experience at all, starting from what a terminal and a shell even are, and builds up through the filesystem, permissions, processes, package management, text processing, and networking before spending its entire fourth part — 13 chapters on its own — teaching real bash shell scripting from a first script through arrays and debugging.
The current Seventh Internet Edition (Version 25.12, January 2026) is free to download directly from the author’s own site, linuxcommand.org, under a CC BY-NC-ND 3.0 licence, and its content lines up with the paid No Starch Press print 3rd Edition. Every one of its 36 chapters is built entirely around real, typeable command-line examples rather than abstract description, which is exactly why it remains one of the most recommended free starting points for a student who needs working Linux fluency — for a lab course, an internship, or a first real server — rather than just theoretical familiarity with the idea of a command line.
Book Overview
| Course | Linux / Shell Fundamentals — command-line use, system administration basics, and shell scripting |
| Degree Programs | BSCS, BSIT |
| Level | University level — no prior Linux or programming experience assumed |
| Edition | Seventh Internet Edition, Version 25.12 (January 5, 2026) — the free edition, textually aligned with the No Starch Press print “3rd Edition” (February 2026) |
| Author(s) | William Shotts |
| Structure | 36 chapters in 4 parts: Part 1 – Learning the Shell (Ch. 1–10), Part 2 – Configuration and the Environment (Ch. 11–13), Part 3 – Common Tasks and Essential Tools (Ch. 14–23), Part 4 – Writing Shell Scripts (Ch. 24–36) |
| Exercises | No formal end-of-chapter problem sets; every chapter teaches through worked command-line examples meant to be typed and run directly, which this page’s own Important Questions and Practice Tips are built to extend |
| Language | English |
| License | CC BY-NC-ND 3.0 United States |
| Format | Free PDF (Internet Edition), 596 pages; a paid print/ebook 3rd Edition from No Starch Press covers the same content |
Chapter List
Part 1: Learning the Shell
Chapter 1: What Is the Shell?
Difficulty: Easy · Course: Linux / Shell · Key topics: terminal emulators, the shell vs. the OS, bash, virtual terminals, the prompt, first commands
Opens by separating three things students usually conflate: the terminal emulator (the window), the shell (the program interpreting what you type), and the operating system underneath. Explains that Linux distributions expose several virtual terminals in addition to the graphical terminal window, and that bash (the Bourne Again SHell) is the default command interpreter on almost every distribution. Walks through the very first commands — date, cal, df, free — purely to build confidence typing at a prompt before any real work begins, and closes by showing exit to leave the shell.
Key Points:
- A terminal emulator is the window; the shell (commonly
bash) is the program reading and running what you type inside it — they are not the same thing. - Linux systems normally provide several virtual terminals in addition to terminal windows inside the graphical desktop.
dateprints the current date and time;calprints a calendar;dfreports filesystem disk space usage;freereports memory usage.- The shell prompt commonly ends in
$for a normal user and#for the root (administrative) user. exitends the current shell session, closing the terminal window if it was the only shell running in it.
Practice Tip: Run df -h and free -h (-h = human readable) on your own system in the first five minutes — real numbers for your own disks and memory make the shell feel like a tool, not a lecture.
Common Mistake: Assuming every distribution’s default shell is identical — bash is the overwhelming default, but some minimal systems and containers default to dash or plain sh, which lack bash-only features covered later, most visibly arrays and [[ ]] tests.
Important Questions:
- What is the difference between a terminal emulator and a shell? A terminal emulator is the window that displays text and accepts keystrokes; the shell is the actual program — typically
bash— running inside that window that reads each line, interprets it, and runs the corresponding command. - What does
exitdo, and why does the book introduce it before any file commands?exitends the current shell session (closing the terminal window if nothing else depends on it); the book introduces it immediately so that opening and closing a shell to experiment never feels risky.
Chapter 2: Navigation
Difficulty: Easy · Course: Linux / Shell · Key topics: Filesystem Hierarchy Standard, pwd, cd, ls, absolute & relative pathnames, ~, ., ..
Introduces the Linux filesystem as a single tree rooted at /, governed by the Filesystem Hierarchy Standard so that /etc, /home, /var, and similar directories mean the same thing on every distribution. Teaches the three basic navigation commands — pwd to print the current working directory, cd to change it, and ls to list its contents — and the pathname notation that makes them useful: absolute paths starting from /, relative paths starting from wherever you already are, and the shortcuts ., .., and ~.
Key Points:
- The Linux filesystem is one tree rooted at
/; the Filesystem Hierarchy Standard defines what/etc,/home,/var, and/usrare for. pwdprints the current working directory;cd <dir>changes it;lslists a directory’s contents.- An absolute pathname starts with
/and works from any current directory; a relative pathname omits the leading/and resolves from the current directory. .means the current directory and..means its parent;cd ..moves up one level.~expands to the current user’s home directory;cdwith no argument returns you there from anywhere.cd -returns you to the previous working directory, useful for toggling between two locations.
Practice Tip: From your home directory run cd /usr/share/doc, then cd - twice in a row — watching it toggle you back and forth makes “previous working directory” concrete.
Common Mistake: Dropping the leading slash by accident — cd etc looks for a subdirectory named etc inside the current directory (usually missing), while cd /etc goes to the real system-wide directory; the single leading / is the entire difference.
Important Questions:
- What is the practical difference between an absolute and a relative pathname? An absolute pathname begins with
/and names a location unambiguously from the root, working from any current directory; a relative pathname has no leading/and is resolved starting from wherever you currently are, so the same relative name can point to different places depending on where you type it. - How would you move from
/home/alice/projectsto/home/aliceusing a shortcut? Runcd .., which moves one level up to the parent directory, or simplycdwith no argument, which always returns to~— for this user, also/home/alice.
Chapter 3: Exploring the System
Difficulty: Easy · Course: Linux / Shell · Key topics: ls options (-l, -a, -F, -R, -t), file types, the file command, the less pager
Deepens ls into a real inspection tool, working through its most useful options — -l for a long listing showing permissions, owner, group, size and modification time; -a to reveal hidden dotfiles; -F to append a type indicator like / or *; -R and -t for recursive and time-sorted listings. Introduces file for identifying a file’s actual type regardless of its name, and less as the standard pager for reading a text file one screen at a time without opening an editor.
Key Points:
ls -lshows one line per entry: file type + permissions, link count, owner, group, size, modification date, and name.- The first character of an
ls -lline identifies the file type:-regular file,ddirectory,lsymbolic link. ls -areveals hidden files — any name beginning with a dot, such as.bashrc.ls -Fappends a classifying character to each name without needing-l‘s full detail.file <name>examines a file’s actual content to report its real type, catching a mislabeled file.less <file>opens a file for paged viewing; Space/b page forward/back,/patternsearches,qquits.
Practice Tip: Run ls -l /bin | less and use / inside less to search for a specific program name — a combination you will reuse constantly once directories get too long for one screen.
Common Mistake: Trusting a file’s extension to know what it really is — Linux does not enforce extensions, so a file named report.txt could actually be a binary; file report.txt checks the real content instead of the name.
Important Questions:
- How do you tell a directory apart from a regular file in the output of
ls -l? The first character of each line’s permission field encodes the type: a directory’s line begins withd, a regular file’s with-;ls -Fgives the same information more compactly by appending a trailing/to directory names. - Why would
file suspicious.txtreport “ELF 64-bit executable” even though the name ends in.txt? Linux does not use the extension to determine a file’s type;fileinspects the actual byte content to determine what it really is, so a binary renamed with a.txtextension is still correctly identified as an executable.
Chapter 4: Manipulating Files and Directories
Difficulty: Medium · Course: Linux / Shell · Key topics: cp, mv, rm, mkdir, rmdir, ln (hard & symbolic links), wildcards
Moves from looking at files to changing them: mkdir to create directories, cp to copy files and (with -r) whole directory trees, mv to move or rename, and rm/rmdir to delete files and empty directories. Gives special attention to ln, distinguishing hard links (a second directory entry pointing at the same underlying data on one filesystem) from symbolic links (a small separate file that simply names another path, which can cross filesystems and can point to nothing at all).
Key Points:
mkdir <dir>creates a directory;mkdir -p a/b/ccreates any missing parent directories in one call.cp source destcopies a file;cp -r sourcedir destdiris required to copy an entire directory tree.mvboth renames a file (same directory) and moves it (different directory) — there is no separate rename command.rmdeletes permanently with no recycle bin;rmdirdeletes only empty directories, whilerm -rdeletes a directory and everything in it.- A hard link is a second name for the exact same data on disk; deleting either name leaves the data intact as long as one link remains.
- A symbolic link (
ln -s) is a small file that points at a path by name; it can span filesystems but becomes “broken” if the target moves.
Practice Tip: Create a file, hard-link it (ln original hardlink), symlink it (ln -s original symlink), then delete original — the hard link still opens fine, the symlink is now broken. This single test explains the whole distinction better than any diagram.
Common Mistake: Running rm * or rm -r * from the wrong directory — there is no confirmation and no recycle bin, so double-checking pwd and the wildcard’s actual match (with ls * first) before an rm with a wildcard is not optional caution, it is the only safeguard that exists.
Important Questions:
- What actually happens on disk when you create a hard link to a file, versus a symbolic link? A hard link adds a second directory entry pointing at the exact same underlying data (the same inode) on the same filesystem, so both names are equally “real” and the data survives as long as one name remains; a symbolic link instead creates a distinct small file whose only content is the path it points to, so it can point across filesystems but stops working if that target is later moved, renamed, or deleted.
- Why is there no dedicated
renamecommand in the shell?mvalready covers both cases: moving a file to a new directory and renaming it in place are the same underlying operation — changing the directory entry that points at the file — somv oldname newnamewithin the same directory functions exactly as a rename, and a separate command would be redundant.
Chapter 5: Working with Commands
Difficulty: Easy · Course: Linux / Shell · Key topics: type, which, help, man, apropos, whatis, alias
Teaches how to find out what a command actually is and how to learn more about it before running it blindly. type reports whether a name is a shell builtin, an alias, or an executable file, and which reports exactly which file on $PATH would run. Surveys the documentation system — man for full manual pages organized into numbered sections, apropos for keyword-searching those pages, whatis for a one-line summary, and each program’s own --help — then introduces alias for defining a personal shortcut command.
Key Points:
type <name>reports whether a command is a shell builtin, an alias, a function, or an executable file, and where.which <name>reports the exact executable file on$PATHthat a bare command name would run.man <command>opens the full manual page; man pages are organized into numbered sections (1 = user commands, 5 = file formats, 8 = admin commands).apropos <keyword>searches man page descriptions for a keyword when you don’t know a command’s exact name.whatis <command>prints a single-line summary of what a command does.alias name='command'defines a shortcut for the rest of the session; aliases are usually made permanent in a startup file like.bashrc, covered in Chapter 11.
Practice Tip: Run type cd, then type ls, and compare the two outputs — one reports a shell builtin and the other a file path, which is the fastest way to see the builtin/executable distinction this chapter is building toward.
Common Mistake: Confusing type and which and using which on a builtin like cd — since cd has no executable file on $PATH at all, which cd reports nothing useful (or nothing at all on some systems), while type cd correctly reports “cd is a shell builtin.”
Important Questions:
- What is the difference between what
typeandwhicheach report?typereports the general category a command name resolves to — shell builtin, alias, function, or executable file — and is therefore accurate for everything;whichonly searches$PATHfor an executable file, so it correctly reports the location of an external program but gives no useful answer for a builtin likecd, which is not a separate file at all. - How would you find the name of a command to convert an image format if you didn’t already know it? Run
apropos convert(orapropos image), which searches every installed man page’s one-line description for that keyword and lists matching commands; once a likely name turns up,whatisgives its one-line summary andmangives the full manual page.
Chapter 6: Redirection
Difficulty: Medium · Course: Linux / Shell · Key topics: cat, sort, uniq, wc, grep, head, tail, tee, standard streams, pipelines, >, >>, <
Introduces every program’s three standard streams — standard input, standard output, and standard error — and the shell operators that redirect them: > to overwrite a file with output, >> to append, < to feed a file in as input, and the pipe | to connect one program’s output directly to another’s input. Builds this into working pipelines using a set of small text tools — cat, sort, uniq, wc, grep, head, tail, and tee — each doing one narrow job well.
Key Points:
- Every program has three standard streams: stdin (input), stdout (normal output), and stderr (error messages), each independently redirectable.
>redirects stdout to a file, overwriting it;>>appends instead;2>redirects stderr specifically.- The pipe operator
|connects one command’s stdout directly to the next command’s stdin, chaining any number of programs together. sortorders lines of text;uniqremoves adjacent duplicate lines (so input is usually sorted first);wccounts lines/words/bytes.grep <pattern> <file>prints only the lines matching a pattern;head/tailprint the first/last lines of a file (tail -ffollows a growing file live).teesplits a stream, writing it both to a file and onward to stdout, useful for saving a log while still watching it scroll.
Practice Tip: Run ls /etc | sort | uniq -c | sort -rn | head on a real directory and read the pipeline left to right, one stage at a time — seeing what each stage actually changes about the previous stage’s output is how pipelines stop feeling like magic.
Common Mistake: Writing sort file.txt > file.txt expecting an in-place sort — the shell opens (and truncates) the output file before sort ever reads it, so this empties the file before sort can read anything from it; sort to a temporary file and then mv it over the original instead, or use sort -o file.txt file.txt, which handles this safely.
Important Questions:
- Why does
ls nonexistent 2> errors.txtstill print an error message to the terminal even though output was redirected?2>redirects only stderr (file descriptor 2);ls nonexistentwrites its “No such file or directory” message to stderr, so it is correctly captured intoerrors.txt— if the message still appears on screen, the redirection target or descriptor was written incorrectly, since a correct2>redirect does silence the terminal output for that stream. - What does the pipeline
cat access.log | grep ERROR | wc -lcompute, stage by stage?cat access.logstreams the file’s contents to stdout;grep ERRORreceives that stream on stdin and passes through only the lines containing the text ERROR;wc -lreceives just those filtered lines and counts how many there are — the whole pipeline reports the number of lines in the log containing the word ERROR.
Chapter 7: Seeing the World as the Shell Sees It
Difficulty: Medium · Course: Linux / Shell · Key topics: pathname & tilde expansion, arithmetic expansion, brace expansion, parameter expansion, command substitution, quoting
Explains expansion: the shell rewrites the text you type before running anything, and this chapter is about seeing that rewriting happen. Covers pathname expansion (wildcards like * and ? turning into matching filenames), tilde expansion (~ into a home directory), arithmetic expansion ($((...))), brace expansion ({a,b,c} or {1..5} generating multiple strings), parameter expansion ($variable), and command substitution ($(command)). Closes with quoting — double quotes suppress most expansions except variables, single quotes suppress all of them, and a backslash escapes one character at a time.
Key Points:
- Pathname expansion (globbing) turns
*.txtinto every matching filename before the command ever runs; if nothing matches, most shells pass the literal pattern through unchanged. - Brace expansion generates strings without matching existing files:
file{1,2,3}.txtproduces three names, and{1..5}produces a numeric range. - Arithmetic expansion
$((expression))evaluates integer math directly in the command line, e.g.echo $((5 * 3)). - Command substitution
$(command)replaces itself with that command’s stdout, letting you embed one command’s output inside another command. - Double quotes (
"...") suppress pathname and word-splitting expansion but still allow$variableand$(command)to expand. - Single quotes (
'...') suppress every kind of expansion — the text inside is completely literal. - A backslash escapes the single character that follows it, useful for including one special character (like a literal
$) inside an otherwise unquoted or double-quoted string.
Practice Tip: Run echo *, then echo "*", then echo '*' in the same directory and compare all three outputs — seeing exactly where each quoting style stops pathname expansion makes the rule concrete instead of abstract.
Common Mistake: Forgetting to double-quote a variable that might contain a space, e.g. cp $file /backup — if $file holds “my report.txt”, the shell word-splits it into two separate arguments and cp fails or does the wrong thing; cp "$file" /backup keeps it as one argument.
Important Questions:
- What is the practical difference between
echo $(date)andecho “$(date)”when the date output contains multiple words? Without quotes, the shell word-splits the command substitution’s output on whitespace before passing it toecho, so multiple spaces between fields can collapse to one; wrapped in double quotes, the substitution’s full output (including its internal spacing) is passed toechoas a single argument, preserving the original formatting exactly. - How would you create ten empty files named
log1.txtthroughlog10.txtin one command? Use brace expansion:touch log{1..10}.txt; the shell expands{1..10}into the ten numbers 1 through 10 beforetouchever runs, producing ten separate filename arguments in a single command rather than needing a loop.
Chapter 8: Advanced Keyboard Tricks
Difficulty: Easy · Course: Linux / Shell · Key topics: readline editing, cursor movement, cut & paste, command history, !!, Ctrl-R, tab completion
Covers the line-editing and history features the GNU Readline library gives every bash prompt for free. Teaches emacs-style cursor movement (Ctrl-A/Ctrl-E to jump to line start/end, Alt-F/Alt-B to move by word) and cut-and-paste (Ctrl-K to cut to end of line, Ctrl-Y to paste it back), then the history mechanism: the history command, re-running a previous command with !! or !n, and searching backward interactively with Ctrl-R. Closes with tab completion, which completes filenames, commands, and (with programmable completion enabled) even command-specific options and arguments.
Key Points:
- Ctrl-A moves the cursor to the start of the line, Ctrl-E to the end; Alt-F and Alt-B move forward/back one word at a time.
- Ctrl-K cuts (kills) from the cursor to end of line into a buffer; Ctrl-Y yanks (pastes) that buffer back at the cursor.
historylists previously run commands with their numbers;!nre-runs command number n, and!!re-runs the immediately previous command.- Ctrl-R starts an incremental reverse search through history — typing characters narrows the match live, and pressing Ctrl-R again cycles to the next older match.
- Tab completion fills in the rest of a filename, directory, or command name as far as it is unambiguous, and lists all possibilities on a second Tab press when it is not.
Practice Tip: Type a long command incorrectly on purpose, then instead of retyping it, press Ctrl-R and type a few characters from the middle of it — finding and reusing it that way, rather than with the Up arrow, is the habit this chapter is really trying to build.
Common Mistake: Relying only on the Up arrow to browse history in a long session — it works, but requires stepping through every command one at a time; Ctrl-R jumps directly to a matching command by content, which is dramatically faster once your history has more than a screenful of entries.
Important Questions:
- What does
!!do, and why is it commonly chained withsudo?!!re-runs the immediately previous command exactly as typed; it is commonly chained assudo !!after a command fails with a permissions error, since it re-runs that same failed command with root privileges without having to retype it. - How does Ctrl-R differ from repeatedly pressing the Up arrow to find a previous command? The Up arrow steps backward through history one command at a time regardless of content, so finding a command typed fifty commands ago means pressing it fifty times; Ctrl-R instead starts an incremental search where typing any substring of the command jumps directly to the most recent match, and pressing Ctrl-R again cycles to the next older match, which scales far better in a long session.
Chapter 9: Permissions
Difficulty: Medium · Course: Linux / Shell · Key topics: owner/group/other, read/write/execute, chmod symbolic & octal notation, umask, chown, chgrp, suid/sgid/sticky bit
Explains Linux’s multi-user permission model: every file has an owner and a group, and read, write, and execute permissions are set separately for the owner, the group, and everyone else, visible as the ten-character field at the start of an ls -l line. Teaches chmod in both its symbolic form (u+x, go-w) and its octal form (chmod 755 file), and explains how execute permission means something different for a directory (the ability to enter it and list what’s inside) than for a file. Covers umask for default permissions on newly created files, chown/chgrp for changing ownership, and the special setuid, setgid, and sticky bits.
Key Points:
- Permissions are read (
r), write (w), and execute (x), each set independently for owner (u), group (g), and others (o). - On a directory, execute permission means the ability to enter it and access files inside by name, not the ability to “run” it; read permission on a directory only allows listing its contents.
chmod 755 filesets owner=rwx (7), group=r-x (5), other=r-x (5) in one octal call;chmod u+x fileadds execute for the owner only, symbolically.umasksets the default permission mask subtracted from new files/directories at creation time — a common default of022removes write access for group and other.chown user:group filechanges both owner and group at once;chgrp group filechanges only the group.- The setuid bit on an executable runs it with the file owner’s privileges rather than the caller’s; the sticky bit on a shared directory like
/tmplets each user delete only their own files there.
Practice Tip: Run chmod 750 file then ls -l file and mentally re-derive 750 from the displayed rwxr-x--- string, digit by digit — once you can go from octal to symbolic and back in your head without pausing, permission mistakes get far easier to spot on sight.
Common Mistake: Running chmod 777 on a file or directory to “just make it work” — this grants write access to every user on the system, not just the owner, which on a shared or internet-facing machine is a real security hole; the narrowest permission that actually solves the problem (often chmod 755 for a script, or a targeted chmod g+w) is almost always the correct fix instead.
Important Questions:
- What does the permission string
drwxr-xr-xmean for a directory? The leadingdmarks it as a directory; the owner has read, write, and execute (rwx, meaning they can list it, create/delete entries in it, and enter it); the group and everyone else have only read and execute (r-x, meaning they can list its contents and enter it, but not create or delete files inside it). - Why does
chmod 644on a shell script leave it unable to run even though the owner can read and write it?644grants read and write to the owner and read-only to group and other, but zero execute permission to anyone; a script (or any program) needs execute permission set for whichever user is trying to run it, sochmod 755(or at minimumchmod u+x) is required before./script.shwill actually run instead of failing with “Permission denied.”
Chapter 10: Processes
Difficulty: Medium · Course: Linux / Shell · Key topics: ps, top, jobs, fg, bg, Ctrl-Z, Ctrl-C, kill, killall, signals, nice/renice
Introduces processes as running instances of programs, each with a process ID (PID) and a parent process. Teaches ps for a static snapshot of running processes and top for a live, continuously updating view sorted by resource use. Covers job control — suspending a foreground process with Ctrl-Z, resuming it in the background with bg or the foreground with fg, and listing suspended/backgrounded jobs with jobs — and signals, explaining that Ctrl-C sends SIGINT and kill/killall send other signals (SIGTERM by default, SIGKILL when a process won’t otherwise stop) to a process by PID or by name.
Key Points:
- Every running program is a process with a unique PID and a parent process;
psshows a snapshot,ps auxorps -efshows every process on the system. top(orhtop) shows a live, auto-refreshing view of processes sorted by CPU or memory use, useful for finding what is actually consuming resources right now.- Ctrl-Z suspends the current foreground process and returns you to the prompt;
jobslists suspended and backgrounded jobs for the current shell. bgresumes the most recently suspended job in the background;fgbrings a backgrounded or suspended job back to the foreground.- Ctrl-C sends SIGINT, asking a process to stop;
kill <PID>sends SIGTERM by default, andkill -9 <PID>(SIGKILL) forces termination when a process ignores SIGTERM. killall <name>sends a signal to every process matching a program name instead of requiring a PID lookup first.
Practice Tip: Start sleep 300 in a terminal, suspend it with Ctrl-Z, run jobs, then bg it and confirm with ps that it is still running in the background — walking the whole suspend/background/verify cycle once makes job control click.
Common Mistake: Reaching for kill -9 as the first option instead of a last resort — SIGKILL gives the process no chance to clean up open files or save state, unlike the default SIGTERM, which asks the process to shut down gracefully; try a plain kill <PID> first and escalate to -9 only if the process genuinely ignores it.
Important Questions:
- What is the difference between what
kill <PID>andkill -9 <PID>actually do? Plainkill <PID>sends SIGTERM, a polite request asking the process to terminate itself, which well-behaved programs catch in order to save state and close files cleanly before exiting;kill -9 <PID>sends SIGKILL, which the process cannot catch, ignore, or clean up after — the kernel terminates it immediately, which is why it is reserved for processes that don’t respond to a normal SIGTERM. - You start a long-running command in the foreground and realize you need the prompt back without stopping the command. What do you do? Press Ctrl-Z to suspend the process and return to the shell prompt, then run
bgto resume that same suspended job running in the background;jobsconfirms it is running, andfgbrings it back to the foreground later if needed.
Part 2: Configuration and the Environment
Chapter 11: The Environment
Difficulty: Medium · Course: Linux / Shell · Key topics: env/printenv, login vs. interactive shells, startup files, .bashrc, PATH, export
Explains the environment as the set of named variables (like PATH, HOME, PS1) that every process inherits from its parent, viewable with printenv or env. Distinguishes a login shell from an interactive non-login shell and maps out bash’s startup-file sequence — system-wide /etc/profile, then a personal .bash_profile or .profile for login shells, and .bashrc for interactive non-login shells — explaining why aliases and functions belong in .bashrc specifically. Covers modifying PATH to add a personal script directory and export to make a shell variable visible to child processes.
Key Points:
- The environment is the set of variables a process inherits from its parent;
printenvorenvlists the current shell’s environment. - A login shell (starting a whole new session, e.g. via console login or SSH) reads
/etc/profilethen a personal login startup file; an interactive non-login shell (a new terminal window) reads.bashrcinstead. .bashrcis the right place for aliases, functions, and prompt customization, since it runs for every new interactive shell, not just the first one of a session.- A shell variable is local to the current shell until
exported, after which child processes (including any command it runs) inherit a copy of it too. - Adding a personal directory to
PATH(commonlyexport PATH="$HOME/bin:$PATH"in.bashrc) lets your own scripts run by name from anywhere. PS1defines the interactive prompt string itself and is one of the most commonly customized environment variables, covered in full in Chapter 13.
Practice Tip: Run echo $PATH, add a test directory to it with export PATH="$HOME/test:$PATH", confirm with another echo $PATH, then close the terminal and reopen it — the change is gone, which is exactly why permanent changes belong in .bashrc, not typed at the prompt.
Common Mistake: Editing PATH or adding aliases directly at the prompt and expecting the change to persist — anything typed interactively lives only in that one shell session and vanishes when it closes; only edits saved into a startup file like .bashrc (which then must be re-sourced with source ~/.bashrc or a new terminal opened) survive.
Important Questions:
- Why does a new terminal window read
.bashrcwhile a fresh SSH login reads.bash_profile(or.profile) instead? A new terminal window inside an already-logged-in desktop session is an interactive non-login shell, which bash configures to read.bashrc; an SSH login starts an entirely new session and counts as a login shell, which reads the system-wide/etc/profileand then a personal login file — the distinction exists so login-specific setup (run once per session) doesn’t have to re-run every time you open one more terminal tab. - What is the difference between setting
MYVAR=helloandexport MYVAR=hello? Both create a shell variable visible to the current shell, but only the exported version is copied into the environment of any child process the shell subsequently starts (a script, another program); a plain, non-exportedMYVAR=hellois invisible to a script run from that shell, even thoughecho $MYVARin the same shell still shows it.
Chapter 12: A Gentle Introduction to vi(m)
Difficulty: Medium · Course: Linux / Shell · Key topics: Command/Insert/Last-line (ex) modes, basic movement, editing, yy/dd/p, undo, searching
Introduces vi (in its modern vim form, available on essentially every Linux system) as the editor worth learning because it is always there, even on a minimal server with no desktop. Explains its three modes — Command mode for movement and editing keystrokes, Insert mode for typing text (entered with i, exited with Esc), and Last-line (ex) mode for commands like saving and quitting (:w, :q, :wq) — and walks through the core Command-mode vocabulary: movement (hjkl), deleting (dd, x), yanking and pasting (yy, p), undo (u), and searching (/pattern).
Key Points:
- vi always opens in Command mode;
ienters Insert mode to type text, and Esc always returns to Command mode. :wsaves,:qquits,:wq(orZZfrom Command mode) saves and quits, and:q!quits without saving unsaved changes.hjklmove the cursor left/down/up/right in Command mode, useful even without arrow keys.dddeletes (and copies) the current line,xdeletes the character under the cursor, andyyyanks (copies) the current line without deleting it.ppastes whatever was last deleted or yanked, after the current line or cursor position.uundoes the last change;/patternfollowed by Enter searches forward for text, andnrepeats the search.
Practice Tip: Open any text file with vim testfile, and before typing anything else, practice the cycle Esc, i, type a word, Esc, dd, u, :wq — that one loop covers every mode transition you’ll use in ordinary editing.
Common Mistake: Typing text immediately after opening vi without first pressing i — every keystroke in Command mode is interpreted as a command rather than text, so typing normally can delete lines, jump around the file, or trigger other unexpected edits; Insert mode must be entered explicitly first, and Esc must be pressed before any Command-mode keystroke.
Important Questions:
- You open a file in vi, make several changes, and want to discard all of them and quit. What do you type?
:q!from Command mode (pressing Esc first if you’re still in Insert mode) quits without saving, discarding every unsaved change; plain:qwould refuse to quit at all while unsaved changes exist, precisely to prevent losing them by accident. - What is the difference between
ddandyyon the current line?dddeletes the current line from the file and also copies it into the unnamed register, so it can still be pasted withp;yyonly copies (yanks) the line into that same register without removing it from the file — both make the line available top, but onlyddchanges the file’s content.
Chapter 13: Customizing the Prompt
Difficulty: Easy · Course: Linux / Shell · Key topics: PS1 anatomy, backslash-escaped sequences, color escape sequences, PS2
Takes apart the PS1 environment variable that defines the interactive prompt, showing that its default value is built from backslash-escaped sequences rather than literal text — \u for username, \h for hostname, \w for the current working directory, and \$ for the $/# privilege indicator. Shows how to add ANSI color escape sequences to make parts of the prompt stand out, and introduces PS2, the secondary prompt (usually >) shown when a command continues onto a second line.
Key Points:
PS1holds the primary interactive prompt string, built from literal text plus backslash-escaped special sequences.\uexpands to username,\hto hostname (short form),\wto the current working directory (with~substituted for home), and\$to$for a normal user or#for root.- Color is added with ANSI escape sequences wrapped in
\[and\]so bash knows they are non-printing and doesn’t miscount the prompt’s visible width. PS2defines the secondary prompt (>by default) shown while bash is waiting for a multi-line command, like an unclosed quote, to be completed.- A custom
PS1must be exported and saved in.bashrcto persist across new terminal sessions, the same rule covered for any other environment customization in Chapter 11.
Practice Tip: Temporarily set PS1='\u@\h:\w\$ ' at the prompt and watch it update live as you cd to different directories — then decide which pieces you actually want before making it permanent in .bashrc.
Common Mistake: Forgetting to wrap color escape sequences in \[ and \] — without those markers bash counts the invisible color codes as visible characters, which throws off line-wrapping and cursor positioning during command-line editing, producing a prompt that visually corrupts itself as soon as a long command wraps or history is recalled.
Important Questions:
- What does the default prompt piece
\wactually show, and how does it differ from\W?\wexpands to the full current working directory path, with the user’s home directory abbreviated to~when applicable (e.g.~/projects/app);\Winstead shows only the final component of that path (justapp), which some users prefer for a shorter prompt. - Why must color codes in
PS1be wrapped in\[and\]rather than used directly? Those markers tell bash’s line-editing code that the enclosed characters are non-printing and take up no space on screen; without them, bash includes the invisible ANSI escape codes in its count of the prompt’s visible width, which causes it to miscalculate cursor position once a command line wraps or command history is recalled, visibly corrupting the display.
Part 3: Common Tasks and Essential Tools
Chapter 14: Package Management
Difficulty: Medium · Course: Linux / Shell · Key topics: Debian-style (apt/dpkg) vs. Red Hat-style (yum/dnf/rpm), installing/removing/upgrading, searching, finding which package owns a file
Explains why Linux software is normally installed through a distribution’s package manager rather than downloaded and compiled by hand: packages track dependencies, are cryptographically signed, and can be cleanly upgraded or removed. Covers the two major package families in parallel — Debian-based systems using apt/apt-get as the high-level tool over low-level dpkg, and Red Hat-based systems using dnf/yum over low-level rpm — for the same core tasks: installing, removing, upgrading, searching for a package by name or keyword, and finding which installed package owns a given file.
Key Points:
- A package manager resolves dependencies automatically, so installing one program also installs the libraries it needs without you tracking them manually.
- Debian family (Ubuntu, Debian, Mint):
apt install <pkg>,apt remove <pkg>,apt updaterefreshes the package index,apt upgradeupgrades installed packages. - Red Hat family (Fedora, RHEL, CentOS):
dnf install <pkg>,dnf remove <pkg>,dnf search <term>, withyumas the older equivalent name still present on many systems. dpkg -L <pkg>andrpm -ql <pkg>list every file a package installed;dpkg -S <file>andrpm -qf <file>reverse the lookup, reporting which package owns a given file.- Package operations that modify the system (install, remove, upgrade) require root privileges, normally invoked with
sudo.
Practice Tip: On whichever family your own system belongs to, run the “which package owns this file” lookup (dpkg -S or rpm -qf) on a command you already use daily, like /bin/ls — seeing that even core utilities come from a tracked package makes the whole system feel less opaque.
Common Mistake: Mixing package managers on one system, or installing software by downloading and compiling it over top of a package-managed installation, without telling the package manager — this creates files the package database doesn’t know about, so a later upgrade or removal through the normal package tool can silently leave stray files behind or conflict with what it thinks is installed.
Important Questions:
- On a Debian-based system, what is the practical difference between
apt updateandapt upgrade?apt updateonly refreshes the local index of what packages and versions are currently available from the configured repositories — it installs nothing;apt upgradeactually installs newer versions of currently-installed packages, and it needs an up-to-date index (from a recentapt update) to know what newer versions exist. - How would you find out which installed package a mystery file like
/usr/bin/convertcame from? On a Debian-based system, rundpkg -S /usr/bin/convert; on a Red Hat-based system, runrpm -qf /usr/bin/convert— both reverse-lookup the file against the local package database and report the name of the package that installed it, which is useful for figuring out what to remove (or what to file a bug against) without guessing.
Chapter 15: Storage Media
Difficulty: Hard · Course: Linux / Shell · Key topics: device files (/dev), mount/umount, /etc/fstab, mkfs, fsck, partitioning (fdisk), dd
Explains that Linux represents storage devices as files under /dev (like /dev/sda for a whole disk, /dev/sda1 for its first partition), and that a device must be mounted — attached to a specific directory in the filesystem tree — before its contents are reachable. Covers mount and umount for attaching and detaching devices, /etc/fstab for defining mounts that happen automatically at boot, and the lower-level tools for working with storage directly: fdisk for partitioning, mkfs for creating a new filesystem, fsck for checking and repairing one, and dd for copying raw blocks between devices or files.
Key Points:
- Storage devices appear as files under
/dev;/dev/sdais a whole disk and/dev/sda1is its first partition, using the same naming pattern for USB drives. mount <device> <mountpoint>attaches a device’s filesystem to an existing directory;umountdetaches it, and a device should always be unmounted before physical removal./etc/fstablists devices to be mounted automatically at boot, mapping each device (often by UUID rather than device name, since device names can shift) to its mountpoint and options.fdisk(orparted) creates and modifies partition tables on a disk;mkfs -t ext4 <partition>creates a new filesystem of a given type on a partition.fsckchecks a filesystem for errors and can repair many of them, but must be run on an unmounted (or read-only mounted) filesystem to be safe.ddcopies raw blocks of data between a device and a file (or another device), which is how a bootable USB image is written directly to a drive — a wrong target device withddsilently destroys data with no confirmation prompt.
Practice Tip: Run lsblk before and after plugging in a USB drive and compare the two outputs — identifying the exact new device name this way, rather than guessing from /dev/sdX habit, is the single habit that prevents the worst dd mistakes.
Common Mistake: Getting the source and destination reversed in a dd command, or targeting the wrong device name entirely — dd has no confirmation prompt and no undo, and a typo like of=/dev/sda instead of /dev/sdb overwrites an entire disk in seconds; always double-check the exact device name with lsblk or fdisk -l immediately before running any dd command that writes to a device.
Important Questions:
- Why do many
/etc/fstabentries reference a partition by its UUID rather than by a name like/dev/sdb1? Device names like/dev/sdb1are assigned by detection order at boot and can shift if a drive is added, removed, or plugged into a different port, silently pointing anfstabentry at the wrong device; a filesystem’s UUID is generated once when it is created and never changes, so referencing storage by UUID guarantees the correct filesystem is mounted at its intended mountpoint regardless of detection order. - What must be true of a filesystem before it is safe to run
fsckon it? The filesystem should be unmounted, or at minimum mounted read-only, beforefsckchecks or repairs it; runningfsckon a filesystem that is actively mounted read-write risks the filesystem changing underneath the check, which can produce incorrect repairs or new corruption rather than fixing the original problem.
Chapter 16: Networking
Difficulty: Medium · Course: Linux / Shell · Key topics: ping, traceroute, ip/ifconfig, netstat/ss, dig/host, ssh, scp, wget
Surveys the command-line tools for checking, examining, and using a network connection. ping tests basic reachability to a host, and traceroute/tracepath shows the sequence of routers a packet passes through to get there. ip (the modern replacement for the older ifconfig) reports and configures network interfaces, while ss (replacing netstat) lists active network connections and listening ports. dig/host query DNS directly, and the chapter closes with two tools for actually using a remote machine or file: ssh for a secure remote shell (with scp for copying files over that same connection) and wget for downloading a file directly from a URL.
Key Points:
ping <host>tests basic reachability and round-trip time;traceroute <host>shows every router hop along the path to it.ip addrshows network interface addresses (the modern replacement for the olderifconfig);ip routeshows the routing table.ss -tulnlists active TCP/UDP connections and listening ports (the modern replacement fornetstat).dig <domain>orhost <domain>queries DNS directly to resolve a domain name to an IP address, useful for diagnosing DNS problems specifically.ssh user@hostopens a secure encrypted shell session on a remote machine;scp file user@host:/pathcopies a file over that same secure connection.wget <url>downloads a file directly from a URL on the command line without needing a browser.
Practice Tip: Run dig example.com and compare its output section by section against the simpler host example.com — seeing the same lookup at two levels of detail makes DNS resolution far more concrete than reading about it.
Common Mistake: Using the old ifconfig/netstat tool names on a modern distribution and being confused when the command isn’t found — both are deprecated and often not installed by default anymore; ip addr replaces ifconfig and ss replaces netstat on current systems, and both are worth learning as the primary tools rather than as replacements to fall back on.
Important Questions:
- What is the practical difference between what
pingandtracerouteeach tell you when a remote server seems unreachable?pingonly reports whether the destination host responds at all and how long a round trip takes, giving a yes/no answer about reachability;tracerouteinstead reveals every intermediate router the packet passes through on the way there, so if connectivity fails partway, you can see exactly which hop stopped responding rather than only knowing that the final destination is unreachable. - How would you securely copy a local file named
report.pdfto the/backupdirectory on a remote server you can alreadysshinto? Runscp report.pdf user@host:/backup, which copies the file over the same encrypted connectionsshuses, authenticating the same way (password or key) — there is no need for a separate unencrypted file-transfer protocol whensshaccess already works.
Chapter 17: Searching for Files
Difficulty: Medium · Course: Linux / Shell · Key topics: locate, find (-name, -type, -size, -mtime), combining tests, -exec, xargs
Contrasts two very different search tools. locate searches a periodically updated index of the whole filesystem, making it extremely fast but potentially stale for very recently created files. find instead searches the real filesystem live, starting from a given directory and applying tests like -name, -type, -size, and -mtime, which can be combined with implicit AND, explicit -or, and -not. Closes with -exec, which runs a command on every match (one invocation per match by default, terminated with \;), and xargs, which builds one or a few invocations covering many matches at once instead.
Key Points:
locate <pattern>searches a prebuilt index (updated byupdatedb, usually daily via cron) — extremely fast, but may miss files created since the last index update.find <dir> -name <pattern>searches the live filesystem starting at<dir>;-inamemakes the name match case-insensitive.-type fmatches regular files,-type dmatches directories;-sizeand-mtimefilter by file size and modification age.- Multiple
findtests combine with implicit AND by default;-orand-notbuild more complex conditions explicitly. find . -name "*.log" -exec rm {} \;runsrmonce per matching file, with{}replaced by that file’s path each time.find . -name "*.log" | xargs rminstead batches many filenames into as fewrminvocations as possible, which is significantly faster over a very large number of matches.
Practice Tip: Run find . -type f -mtime -7 in a project directory to list every file modified in the last week — a small, safe test of combining a type test with an age test before trying a destructive -exec on real results.
Common Mistake: Adding -exec rm {} \; to a find command without first running the same find alone to check exactly what it matches — a slightly wrong -name pattern or a missing -type f can match far more (or different) files than intended; run the search alone first, review the list, and only then add the destructive action, or use -ok instead of -exec to get a per-file confirmation prompt.
Important Questions:
- Why might
locate report.txtfail to find a file you created five minutes ago, even though it definitely exists?locatesearches a prebuilt database rather than the live filesystem, and that database is normally refreshed only periodically (often once a day viaupdatedbrun by cron); a file created since the last database update simply isn’t in the index yet, solocatereports nothing even thoughfind, which searches live, would find it immediately. - What is the practical performance difference between
find . -name "*.tmp" -exec rm {} \;andfind . -name "*.tmp" | xargs rmover ten thousand matching files? The-exec ... \;form launches a brand-newrmprocess for every single matching file — ten thousand separate process launches; thexargsform instead collects as many filenames as will fit on one command line and passes them tormin a small number of batched invocations, dramatically reducing process-launch overhead on a large match set.
Chapter 18: Archiving and Backup
Difficulty: Medium · Course: Linux / Shell · Key topics: gzip/bzip2/xz, tar (create/extract/list), rsync, dd for imaging
Separates two related but distinct jobs: compression, which shrinks a single file, and archiving, which bundles many files and directories into one. Covers the compression tools gzip, bzip2, and xz, each trading speed against compression ratio differently, and tar as the standard archiving tool, walking through creating (-c), extracting (-x), and listing (-t) an archive, with -v for verbose output and -z/-j/-J to pipe through gzip/bzip2/xz automatically. Closes with rsync for efficiently synchronizing two directory trees (transferring only what changed) as the practical tool for real backups.
Key Points:
- Compression (
gzip,bzip2,xz) shrinks one file; archiving (tar) bundles many files/directories into one, and the two are commonly combined. tar -czvf archive.tar.gz dir/creates a gzip-compressed archive;-xextracts one, and-tlists an archive’s contents without extracting.xzgenerally compresses smaller thanbzip2, which compresses smaller thangzip— at the cost of more CPU time in that same order.rsync -av source/ destination/synchronizes a directory tree, transferring only the parts of files that actually changed rather than copying everything again.rsyncoverssh(rsync -av source/ user@host:/backup/) makes remote incremental backups practical, since only changed data crosses the network.ddcan also image an entire disk or partition byte-for-byte, useful for a full system backup but far slower and less space-efficient than a file-level tool for routine backups.
Practice Tip: Compress the same moderately large file with gzip, bzip2, and xz separately and compare both the resulting sizes and how long each one took — the speed/ratio tradeoff the chapter describes becomes an actual number instead of a general claim.
Common Mistake: Forgetting the trailing slash on rsync‘s source directory when the intent is to copy its contents rather than the directory itself — rsync -av source dest/ creates dest/source/, while rsync -av source/ dest/ copies source‘s contents directly into dest/; the trailing slash on the source changes what gets nested where.
Important Questions:
- What is the practical advantage of
rsyncover plaincp -ror re-runningtarfor a nightly backup?rsynccompares source and destination and transfers only the portions of files that have actually changed since the last run, rather than copying every file’s full content again; for a large directory tree where most files are unchanged from one backup to the next, this makes each incremental backup dramatically faster and lighter on network or disk I/O than a full re-copy. - Why would you choose
xzovergzipfor compressing an archive you plan to store long-term, butgzipfor one you’re about to send over a slow link right now?xzgenerally achieves a smaller final size thangzipat the cost of noticeably more CPU time to compress (and somewhat more to decompress), which is a good tradeoff for a long-term archive compressed once and stored, but a poor one when compression speed matters more than a few extra percent of size — for an immediate transfer,gzip‘s faster compression can get the data moving sooner even though the resulting archive is larger.
Chapter 19: Regular Expressions
Difficulty: Hard · Course: Linux / Shell · Key topics: grep -E, anchors, character classes, POSIX classes, quantifiers, alternation, grouping
Introduces regular expressions as a pattern-matching mini-language distinct from shell wildcards, used through grep (and later, sed and awk). Builds up the vocabulary: the anchors ^ and $ for line start/end, . for any single character, character classes like [abc] and negated classes [^abc], POSIX classes like [[:alpha:]], and the asterisk * for zero-or-more repetitions of the preceding item. Extends this into extended regular expressions (grep -E), adding +, ?, {n,m} quantifiers, | alternation, and () grouping, with worked examples distinguishing a regex’s * from a shell glob’s very different *.
Key Points:
- A shell wildcard and a regex use the same characters for very different meanings — a glob’s
*means “any characters” while a regex’s*means “zero or more of the preceding item.” ^anchors a match to the start of a line and$to the end;^$alone matches only a completely empty line.[abc]matches any one of the listed characters;[^abc]matches any character not in that list; POSIX classes like[[:digit:]]match a whole predefined category portably.- In basic regular expressions,
*means zero or more of the preceding item; extended regular expressions (grep -E) add+(one or more),?(zero or one), and{n,m}(a specific repetition range). |means alternation (either side matches) and()groups a subexpression, both requiring extended regex mode or backslash-escaping in basic mode.grep -E 'foo|bar'matches lines containing either “foo” or “bar”; without-E, the same pattern requires escaping the pipe as\|to get the same alternation behavior.
Practice Tip: Write grep -E '^[0-9]{3}-[0-9]{4}$' and test it against a small file of made-up phone-number-shaped lines, some matching and some deliberately off by one digit — watching a regex reject a near-miss is what makes anchors and exact quantifiers click.
Common Mistake: Writing a regex the way you’d write a shell glob, e.g. expecting grep 'file*.txt' to match any filename-shaped line — in a regex, * only repeats the single character immediately before it (here, just the letter e), so this pattern actually matches “fil”, “file”, “filee”, “fileee” followed directly by “.txt”, not an arbitrary run of characters the way a glob’s * would.
Important Questions:
- What does the extended regular expression
^[A-Z][a-z]+$match, and what would it reject? It matches a line consisting of exactly one uppercase letter followed by one or more lowercase letters and nothing else — so “Hello” matches, but “hello” (no leading capital), “HELLO” (no lowercase letters after the first), and “Hello!” (extra character after the required pattern, since$anchors to end of line) would all be rejected. - What is the difference between
grep 'a*'andgrep 'a+'under basic versus extended regex mode? In both basic and extended regex,*means zero or more of the preceding character, soa*matches even a line with noaat all (zero occurrences still counts);+(one or more) is an extended-regex-only quantifier — under basic regex it is treated as a literal+character rather than a quantifier, sogrep -E 'a+'is needed to require at least onea, while plaingrep 'a+'without-Einstead searches for the literal two-character string “a+”.
Chapter 20: Text Processing
Difficulty: Hard · Course: Linux / Shell · Key topics: sort, uniq, tr, diff/patch, printf, sed, awk
Assembles a toolkit for transforming text rather than just viewing it. Revisits sort and uniq with their fuller option sets, introduces tr for translating or deleting individual characters, and diff/patch for comparing two files and applying the difference elsewhere. Gives real depth to printf for precise formatted output, sed as a stream editor that applies substitution and other commands line by line without opening a file interactively, and awk, treated as a small programming language in its own right for extracting and transforming column-structured text.
Key Points:
sort -nsorts numerically instead of lexically (so 9 sorts before 10);sort -k2sorts by the second whitespace-separated field.tr 'a-z' 'A-Z'translates every lowercase letter in stdin to uppercase, character by character;tr -ddeletes matching characters instead.diff file1 file2reports the lines that differ between two files;patchapplies adiff-produced changeset to a file to reproduce those same edits elsewhere.sed 's/old/new/'substitutes the first match of “old” with “new” on each line; adding a trailingg(s/old/new/g) replaces every match on the line, not just the first.awk '{print $2}'prints the second whitespace-separated field of every input line;awksupports full conditionals, variables, and its own built-in variables likeNF(number of fields) andNR(current record/line number).printf "%-10s %5d\n" name countgives precise control over field width and alignment that plainechocannot.
Practice Tip: Take any CSV-like file and run awk -F, '{print $1, $3}' on it to pull out just the first and third columns — going from “awk is a text tool” to actually extracting real columns from real data is the fastest way to see why the book calls it a small programming language.
Common Mistake: Writing sed 's/old/new/' and expecting every occurrence on the line to be replaced — by default sed‘s substitute command only replaces the first match per line; the global flag g (s/old/new/g) is required to replace every match on each line instead of just the leftmost one.
Important Questions:
- Why does
sort file.txtput “10” before “9” by default, and how do you fix it for a file of numbers? Plainsortcompares lines lexically (character by character, like dictionary order), where the character “1” sorts before “9” regardless of what follows, so “10” sorts before “9”; adding-n(sort -n) tellssortto interpret each line as a number and compare numerically instead, correctly placing 9 before 10. - What does the one-liner
awk '{sum += $3} END {print sum}'compute when run over a file? For every input line, it adds the value of the third whitespace-separated field to a running total stored in the variablesum; theENDblock runs once after all lines have been processed, printing the final accumulated total — effectively summing the third column of the entire file.
Chapter 21: Formatting Output
Difficulty: Easy · Course: Linux / Shell · Key topics: nl, fold, fmt, pr, groff/troff document formatting, man page formatting
Covers the older but still-present tools for shaping plain text for display or printing before it reaches a page. nl numbers a file’s lines, fold wraps long lines to a fixed width without regard for word boundaries, and fmt reflows a paragraph to a target width while respecting word breaks. pr paginates text into a printer-ready layout with headers and page breaks. The chapter closes by tracing how a man page is actually produced — written in the groff/troff markup language and rendered on demand — connecting the documentation readers use in every earlier chapter back to a real formatting toolchain.
Key Points:
nl file.txtnumbers each line of a file, useful before referencing specific lines in a discussion or review.fold -w 60wraps every line to a fixed 60-character width, breaking mid-word if necessary, regardless of where words fall.fmtreflows text into evenly wrapped paragraphs at (by default) 75 characters, breaking only at word boundaries, unlikefold.prformats plain text into paginated output with headers, footers, and page breaks, historically meant to feed directly into a printer.- Man pages themselves are written in the
groff/trofftypesetting markup language and rendered into readable text (or PostScript/PDF) on demand by themancommand.
Practice Tip: Run fold -w 40 and then fmt -w 40 on the same paragraph of text and compare the two outputs side by side — seeing fold break a word mid-letter while fmt never does is the clearest way to remember which tool respects word boundaries.
Common Mistake: Reaching for fold when the actual goal is readable paragraph wrapping — fold is a strictly mechanical fixed-width line breaker that will split a word in half at the width boundary with no regard for readability; fmt is almost always the right tool when the goal is text that still reads naturally after wrapping.
Important Questions:
- What is the practical difference between
fold -w 50andfmt -w 50run on the same paragraph?fold -w 50mechanically breaks the text every 50 characters regardless of word boundaries, potentially splitting a word across two lines;fmt -w 50instead reflows the paragraph to fit within roughly 50 characters per line while only breaking between words, producing output that still reads naturally. - Where does the actual content of a man page come from, and what tool ultimately renders it into the readable text you see? A man page’s source is written in the
groff/trofftypesetting markup language, stored on disk in that markup form rather than as plain text; when you runman <command>, themanprogram locates that source file and renders it throughgroffon the spot into the formatted, paginated text displayed in your terminal.
Chapter 22: Printing
Difficulty: Easy · Course: Linux / Shell · Key topics: CUPS, lp/lpr, lpstat/lpq, cancel/lprm, setting a default printer, sharing a printer
Covers command-line printing through CUPS (the Common Unix Printing System), which underlies printing on virtually every Linux desktop. Introduces lp (and the older BSD-style lpr) for sending a file to a printer, lpstat/lpq for checking queue and printer status, and cancel/lprm for removing a job from the queue. Shows how to set a default printer so plain lp file.txt works without naming one every time, and how CUPS’s own web-based administration interface complements these command-line tools for tasks like sharing a printer on a network.
Key Points:
- CUPS (Common Unix Printing System) is the print server most Linux distributions use underneath both the GUI and the command-line printing tools.
lp file.txt(or the olderlpr file.txt) sends a file to the default printer;lp -d <printer> file.txtsends it to a named printer instead.lpstat -preports printer status;lpqshows the current print queue for a printer.cancel <job-id>(or the olderlprm) removes a specific job from the print queue before it finishes printing.- CUPS also exposes a web-based administration interface (typically at
localhost:631) for tasks like adding printers or sharing one across a network, complementing the command-line tools.
Practice Tip: Run lpstat -p even on a system with no printer physically attached — seeing either a real configured printer or a clear “no destinations added” message confirms whether CUPS itself is even running before troubleshooting anything else.
Common Mistake: Assuming a stuck print job disappears once the file is deleted or the application is closed — a job already submitted to CUPS lives independently in the print queue; cancel <job-id> (found via lpstat) is the actual way to remove it, and deleting the source file afterward has no effect on a job already queued.
Important Questions:
- What is CUPS, and why does the book cover the same printing task through both GUI tools and command-line tools like
lp? CUPS (Common Unix Printing System) is the print server that actually manages printers and print queues on nearly every Linux distribution, and both the desktop’s graphical print dialogs and command-line tools likelpare just different front ends to that same underlying system; knowing the command-line side matters because it works identically over SSH on a headless server with no graphical interface at all, where a GUI print dialog isn’t an option. - A print job is stuck in the queue and won’t finish. How do you remove it from the command line? First run
lpstat -o(orlpq) to list queued jobs and find the specific job’s ID number, then runcancel <job-id>to remove that specific job from the queue; this works independently of whatever application originally submitted the print job.
Chapter 23: Compiling Programs
Difficulty: Hard · Course: Linux / Shell · Key topics: obtaining source, README/INSTALL/LICENSE, ./configure, make, make install, static vs. dynamic linking
Walks through building a program from its published source code instead of installing a pre-built package, covering why a developer or advanced user might need to: a newer version than the distribution ships, custom build options, or a package unavailable for that distribution at all. Explains the standard files a source tarball ships with (README, INSTALL, a licence file) and the classic three-step build sequence — ./configure to detect the system and generate a Makefile, make to compile, and make install to copy the built files into place — while flagging common build failures (a missing development library or header) and the distinction between static and dynamic linking.
Key Points:
READMEandINSTALLin a source tarball describe what the software does and exactly how to build it — always read them before running any build command blindly../configureprobes the local system for available libraries and compilers and generates aMakefiletailored to that machine; it commonly fails when a required development library (a-dev/-develpackage) is missing.makereads the generatedMakefileand compiles the source code into binaries, using dependency rules so only files that actually changed are recompiled on a rebuild.make install(usually needingsudo) copies the newly built binaries, libraries, and documentation into their final system locations.- A dynamically linked program depends on shared library files present on the system at run time and stays smaller on disk; a statically linked program bundles everything it needs into one larger, self-contained binary.
- Installing from source bypasses the package manager entirely, so
apt/dnfhave no record of it — upgrading or removing it later has to be done manually unless the build process supports uninstalling itself.
Practice Tip: Before running ./configure on any real source tarball, run ./configure --help first to see the actual build options available for that specific project — the flags vary widely between projects, and skimming them takes seconds compared to discovering a missing option after a failed build.
Common Mistake: Running ./configure and make repeatedly after each failed attempt to fix a missing dependency, without clearing the previous partial build state — stale generated files from an earlier failed configure can cause confusing follow-on errors; make clean (or re-running ./configure fresh) before retrying avoids debugging a problem that is really just leftover state from the last failed attempt.
Important Questions:
- Why does
./configuresometimes fail with an error about a missing library, even when the corresponding program is already installed and working? Running a program only requires its shared library files, but compiling new software against that library requires the separate development package (commonly named with a-devor-develsuffix, e.g.libssl-dev) containing the header files and linking metadata a compiler needs; most distributions package these separately from the runtime library specifically to keep normal installations smaller, so a working installed program doesn’t guarantee the matching development package is also present. - What is the practical tradeoff between a statically linked and a dynamically linked build of the same program? A statically linked binary bundles every library it needs directly into the executable file, making it larger on disk but able to run on another system without needing matching shared libraries already installed there; a dynamically linked binary stays smaller and shares one copy of each library across every program that uses it, but depends on compatible versions of those shared libraries actually being present on whatever system runs it.
Part 4: Writing Shell Scripts
Chapter 24: Writing Your First Script
Difficulty: Easy · Course: Linux / Shell · Key topics: the shebang line, script file format, chmod +x, script storage location, basic scripting practices
Opens the book’s final part by establishing that a shell script is simply a text file of the same commands you already type interactively, run all at once. Introduces the shebang line (#!/bin/bash) as the first line telling the kernel which interpreter should run the rest of the file, explains why the file needs execute permission (chmod +x script.sh) before ./script.sh will run it, and covers good early habits: using a #-prefixed comment header describing the script’s purpose, and choosing a storage location such as ~/bin that is already (or can be added to) $PATH.
Key Points:
- A shell script is a plain text file containing the same commands you’d type at a prompt, run in sequence by an interpreter.
- The shebang line
#!/bin/bashmust be the very first line of the file; it tells the kernel exactly which program should interpret the rest of the file’s contents. - A script needs execute permission before it can run as
./script.sh;chmod +x script.sh(orchmod 755) grants it. ./script.shrequires the leading./unless the script’s directory is on$PATH, since the current directory is deliberately not searched by default for security reasons.- A comment header at the top of a script (lines beginning with
#, after the shebang) documenting its purpose and usage is considered good practice from the very first script onward. - Storing personal scripts in a dedicated directory like
~/bin, added to$PATHas covered in Chapter 11, lets them run by name from anywhere without the./prefix.
Practice Tip: Write a two-line script (#!/bin/bash then echo "Hello, $USER"), try running it as ./hello.sh before setting execute permission to see the “Permission denied” error firsthand, then chmod +x it and run it again.
Common Mistake: Forgetting that ./ is required to run a script sitting in the current directory — typing just script.sh instead of ./script.sh produces “command not found,” not because the script is broken but because the current directory is intentionally left out of $PATH by default, unlike some other systems.
Important Questions:
- What does the shebang line
#!/bin/bashactually do, and what happens if it’s missing or wrong? It tells the kernel exactly which interpreter program should execute the rest of the file when it’s run directly; without a correct shebang line, the kernel either falls back to running the file with the current shell (which may not support bash-specific syntax the script uses) or fails outright, so a script written with bash-only features but no shebang can behave inconsistently depending on what shell happens to invoke it. - You wrote and saved a script, but running
./myscript.shimmediately reports “Permission denied.” What’s the fix? The script file lacks execute permission, which is required before the kernel will run it directly as a program; runningchmod +x myscript.shto add execute permission (orchmod 755for the full standard permission set) resolves it, after which./myscript.shruns normally.
Chapter 25: Starting a Project
Difficulty: Medium · Course: Linux / Shell · Key topics: a real script project, functions, variables, capturing output, extending a script incrementally
Walks through building a real script — a system-information report — from a first working draft into something more capable, rather than presenting scripting rules in the abstract. Introduces functions as a way to organize a script’s logic into named, reusable pieces, and local shell variables for storing values a script computes or receives. Shows the script growing incrementally: first producing plain output, then having that output captured and formatted, then piped into another program (like less or lp) — establishing the pattern of building a script in small, testable steps rather than writing it all at once.
Key Points:
- A function groups a block of commands under one name, defined with
function_name () { commands; }, and called just by writing that name like any other command. - Variables in a script are assigned with no spaces around
=(name=value) and read back with a leading$($name). - Wrapping a variable reference in braces (
${name}) avoids ambiguity when the variable name is immediately followed by more text, e.g.${name}_backup. - A script’s own output can be redirected or piped exactly like any other command’s, e.g.
./report.sh | lessor./report.sh > report.txt. - Building a script incrementally — get a small version working, test it, then add the next piece — catches errors far earlier than writing the entire script before running it once.
Practice Tip: Write a small function that prints the current date, disk usage, and logged-in users, call it once from the bottom of the script, then run the whole script and pipe its output into less — this single exercise touches every idea the chapter introduces.
Common Mistake: Writing an entire multi-section script in one sitting before running any of it — this makes debugging much harder, because a failure could be anywhere in the file; the book’s own example script is deliberately built and tested in small increments precisely so each new addition can be verified working before the next one is layered on top.
Important Questions:
- Why does the book wrap the current date and disk-usage lookup inside separate functions rather than just writing those commands inline in the script’s main body? Functions give each piece of logic a name that documents what it does and lets it be called again (or reordered, or reused in another script) without retyping the underlying commands; as the script grows to include more sections, functions keep the main body of the script readable as a short sequence of named steps instead of one long unbroken block of commands.
- What is the difference between
$nameand${name}when reading a variable’s value? Both read the value of the variablenamein the simple case, but${name}‘s braces explicitly mark where the variable name ends, which matters the moment more text follows immediately after it with no separator —${name}_report.txtis unambiguous, while$name_report.txtwould be parsed as the single (likely nonexistent) variablename_reportfollowed by.txt.
Chapter 26: Top-Down Design
Difficulty: Medium · Course: Linux / Shell · Key topics: decomposing a script into functions, stub functions, local variables, code layout & indentation
Teaches top-down design as a deliberate method for planning a script before writing its full implementation: state the overall task, break it into a handful of named functions each responsible for one piece, write those functions initially as empty stubs that just print a placeholder message, then fill each one in and test it individually. Introduces local for declaring a variable scoped only to the function it’s declared in, preventing it from leaking into or colliding with the rest of the script’s variables, and reinforces consistent indentation as essential once a script has more than a couple of nested blocks.
Key Points:
- Top-down design starts from the overall task and breaks it into named functions before writing any function’s real implementation.
- A stub function is a placeholder — often just
echo "function_name not yet implemented"— that lets the overall script structure be tested for correct flow before every piece is finished. local varname=valueinside a function scopes that variable to the function only; withoutlocal, a variable assigned inside a function is global and can silently overwrite a same-named variable elsewhere in the script.- Consistent indentation of nested blocks (inside
if, loops, and function bodies) is treated as essential readability practice, not cosmetic preference, once a script grows past a few lines. - Designing top-down and testing each stub-then-implemented function individually catches structural problems (a function called in the wrong place, a missing case) far earlier than debugging one large finished script all at once.
Practice Tip: Sketch a script’s function names and one-line stub bodies first, run the whole stub version top-to-bottom to confirm it calls things in the right order, and only then go back and fill in each function’s real logic one at a time.
Common Mistake: Assigning a variable inside a function without local and assuming it stays contained to that function — in bash, a variable assigned inside a function is global by default unless explicitly declared local, so it can silently overwrite a same-named variable used elsewhere in the script, producing a bug that only shows up once two functions happen to share a variable name.
Important Questions:
- What problem does writing stub functions before implementing them actually solve? It separates two different kinds of mistakes: errors in the script’s overall structure (functions called in the wrong order, a missing branch) versus errors in one function’s specific logic; testing the stub version first confirms the overall flow is correct while every function still just prints a placeholder, so any structural bug found afterward can be isolated to a specific function’s real implementation rather than searched for across the whole unfinished script at once.
- Why can a variable assigned inside one function unexpectedly change the value of a same-named variable used in a completely different part of the script? Bash variables assigned inside a function are global by default, not automatically scoped to that function the way variables are in many other programming languages; without explicitly declaring the variable with
localinside the function, any assignment to it there modifies the one shared global variable of that name, which is why a same-named variable used elsewhere in the script can appear to change value for no visible reason.
Chapter 27: Flow Control: Branching with if
Difficulty: Medium · Course: Linux / Shell · Key topics: if/then/else/elif, exit status, the test command & [ ], comparison operators, &&/|| compound commands
Introduces conditional branching, starting from the idea every later flow-control chapter depends on: every command that finishes returns a numeric exit status, where 0 means success and any nonzero value means some kind of failure, and if branches purely on that status rather than on any special boolean type. Covers the full if/elif/else structure, the test command and its [ ] shorthand for building conditions — string comparisons, numeric comparisons, and file tests like “does this file exist” — and the &&/|| compound commands as a lightweight alternative to a full if statement for simple success/failure chaining.
Key Points:
- Every command returns a numeric exit status on completion: 0 means success, any nonzero value means failure;
$?holds the most recently finished command’s exit status. if command; then ...; elif other_command; then ...; else ...; fibranches based on the exit status of the command in theif/elifline, not on a boolean value.[ condition ]is shorthand for thetestcommand, commonly used as the condition in anifstatement;[[ condition ]]is a bash-specific extended version with fewer quoting pitfalls.- String tests include
=/!=for equality; numeric tests use-eq,-ne,-lt,-gt(not</>, which mean something different inside plain[ ]). - File tests include
-e(exists),-f(is a regular file),-d(is a directory), and-x(is executable). command1 && command2runscommand2only ifcommand1succeeds;command1 || command2runscommand2only ifcommand1fails — a compact alternative toiffor simple cases.
Practice Tip: Prefer bash’s [[ condition ]] over plain [ condition ] in new scripts — it handles unquoted variables and pattern matching more safely, and is worth the habit even though [ ] still appears throughout this book for portability to non-bash shells.
Common Mistake: Writing [$x -eq 5] with no spaces around the brackets, or comparing numbers with = instead of -eq — [ is actually a command name and requires whitespace around it and every argument exactly like any other command, so [$x is parsed as one malformed token and fails with a syntax error; likewise -eq is required for numeric comparison since = and == compare strings, where “5” and “05” would not be considered equal.
Important Questions:
- Why does
if grep -q ERROR logfile.txt; then echo "found errors"; fiwork correctly, given thatifis supposed to test a condition rather than run a command?ifdoes not evaluate a boolean expression at all — it runs the command that follows it and branches based on that command’s exit status;grep -qexits with status 0 if it found at least one match and a nonzero status if it found none, soif grep -q ERROR logfile.txtis really testing “did grep succeed in finding a match,” which is exactly the exit-status-based branching everyifstatement performs. - What is wrong with writing
if [ $age > 18 ]to test whether a number is greater than 18? Inside a single-bracket[ ]test,>and<are not numeric comparison operators at all — they are shell redirection operators, so[ $age > 18 ]is actually interpreted as running thetestcommand with its output redirected into a file literally named18, silently creating that file rather than comparing anything; the correct numeric comparison operator is-gt, as in[ $age -gt 18 ].
Chapter 28: Reading Keyboard Input
Difficulty: Medium · Course: Linux / Shell · Key topics: the read command, -p/-t/-n/-s options, IFS, validating input, redirecting input from a file
Covers read, the builtin that pauses a script and stores a line of typed input into one or more variables. Works through its most useful options: -p to show a prompt inline, -t to time out after a set number of seconds, -n to read a fixed number of characters without waiting for Enter, and -s to read silently for passwords. Explains how IFS (the Internal Field Separator) controls how a line is split across multiple variables named in one read call, shows a validation loop that keeps re-prompting until the input passes a test, and closes by redirecting read‘s input from a file instead of the keyboard.
Key Points:
read varnamepauses the script, reads one line typed by the user, and stores it invarname;read -p "Prompt: " varnameshows a prompt on the same line first.read -t 10 varnamegives the user 10 seconds to respond beforereadtimes out and continues (with a nonzero exit status signaling the timeout).read -s passwordreads input without echoing it to the screen, the standard way to prompt for a password in a script.read var1 var2 var3splits one line of input across multiple variables using the characters inIFS(whitespace by default) as separators, with any leftover words all going into the last variable.- A validation loop — a
whileloop that keeps callingreadand testing the result — is the standard pattern for re-prompting until the user provides acceptable input. read line < file.txtorwhile read line; do ...; done < file.txtredirectsread‘s input from a file instead of the keyboard, letting the same input-processing logic work on either source.
Practice Tip: Write a short script that uses read -p "Continue? (y/n): " answer inside a while loop that keeps asking until the answer is exactly y or n — a small, complete example of the validation-loop pattern this chapter builds toward.
Common Mistake: Using read var1 var2 on a line with more than two words and expecting var2 to only hold the second word — every extra word beyond the number of variables named is appended to the last variable rather than discarded, so a three-word line read into two variables leaves the second and third words both stored together in var2.
Important Questions:
- What happens when you run
read a b cand the user types “one two three four”?readsplits the input onIFS(whitespace by default) and assigns one word to each named variable in order, but any words beyond the last variable are all appended to that last variable together; hereagets “one”,bgets “two”, andcgets “three four” (the extra word attached to the last variable rather than dropped or causing an error). - How would you prompt a user for a password without it appearing on screen as they type it? Use
read -s -p "Password: " password; the-sflag suppresses echoing the typed characters to the terminal, while-pdisplays the prompt text on the same line, so the user sees the prompt but never sees their own keystrokes echoed back.
Chapter 29: Flow Control: Looping with while/until
Difficulty: Medium · Course: Linux / Shell · Key topics: while syntax, until syntax, break, continue, reading a file line by line with while read
Covers the two condition-based loops. while repeats its body for as long as its test command keeps succeeding (exit status 0), and until is its mirror image, repeating for as long as its test command keeps failing — making until the natural fit for a loop meant to run until some condition finally becomes true. Covers break to exit a loop immediately and continue to skip straight to the next iteration, and revisits the while read line; do ... done < file pattern from the previous chapter as the standard, memory-efficient way to process a file’s lines one at a time rather than loading it all at once.
Key Points:
while command; do ...; donerepeats the body for as long ascommandkeeps returning exit status 0 (success).until command; do ...; doneis the mirror image ofwhile: it repeats for as long ascommandkeeps returning a nonzero (failure) status, stopping once it finally succeeds.breakexits the innermost enclosing loop immediately, skipping any remaining iterations entirely.continueskips the rest of the current iteration’s body and jumps straight to the loop’s next condition check, without exiting the loop.while read line; do ...; done < file.txtprocesses a file one line at a time without loading the whole file into memory first, the standard pattern for line-by-line file processing.- An infinite loop,
while true; do ...; done, is a deliberate and common pattern for a script meant to run continuously until stopped by an internalbreakor an external signal.
Practice Tip: Write while read line; do echo "Line: $line"; done < /etc/hosts and run it — seeing the loop process a real system file one line at a time is a small, concrete example worth keeping as a reusable template for any future line-processing script.
Common Mistake: Piping into a while read loop and then finding variables set inside the loop have reverted outside it — a pipeline like cat file | while read line; do count=$((count+1)); done runs the loop in a subshell because it’s on the receiving end of a pipe, so any variable changes made inside are lost once the loop ends; redirecting input with < file instead of piping keeps the loop in the current shell and avoids this.
Important Questions:
- What is the difference between
whileanduntil, and when woulduntilread more naturally?whilecontinues looping as long as its test keeps succeeding, whileuntilcontinues looping as long as its test keeps failing, stopping once the test finally succeeds;untilreads more naturally for a loop framed as waiting for something to become true, such asuntil ping -c1 host &>/dev/null; do sleep 1; done, which reads as “keep waiting until the host responds” rather than the more awkward negatedwhileequivalent. - Why does a variable incremented inside
cat file.txt | while read line; do count=$((count+1)); donestill show its starting value after the loop finishes? Because thewhileloop is the receiving end of a pipe, bash runs it in a subshell — a separate child process with its own copy of the shell’s variables; any variable assignments made inside that subshell, including the incrementing ofcount, are discarded when the subshell exits, leaving the parent shell’s originalcountunchanged, which is why redirecting with< file.txtinstead of piping is the usual fix when the loop needs to affect variables outside it.
Chapter 30: Troubleshooting
Difficulty: Hard · Course: Linux / Shell · Key topics: syntax vs. logical errors, defensive programming, checking exit status, bash -x tracing, trap for signals
Separates two very different classes of script bugs: syntax errors, which bash reports immediately and refuses to run past, and logical errors, where the script runs without complaint but produces the wrong result — usually the harder and more common of the two once a script is more than a few lines long. Covers defensive programming habits like checking a command’s exit status before trusting its result, and the practical debugging toolkit: adding temporary echo statements, running the whole script with bash -x script.sh to trace every command as it executes, and using trap to catch a signal (like Ctrl-C) and run cleanup code before the script actually exits.
Key Points:
- A syntax error (a missing
fi, an unmatched quote) is caught by bash before the script runs at all and is usually straightforward to locate from the reported line number. - A logical error runs without any error message but produces a wrong result — a comparison operator reversed, a loop bound off by one — and is typically far harder to find than a syntax error.
- Checking
$?(or using it directly in anif) after any command whose success can’t be assumed is basic defensive programming, catching a failure immediately instead of letting the script continue on bad data. bash -x script.shtraces the script, printing every command with its expanded variable values as it executes, which is often the fastest way to see exactly where behavior diverges from what was expected.- Temporary
echostatements at key points, removed once the bug is found, remain a simple and effective debugging technique even alongside more advanced tools. trap 'cleanup_function' EXIT(or a specific signal likeINT) runs a specified command or function when the script exits or receives that signal, ensuring cleanup (like removing a temporary file) happens even if the script is interrupted.
Practice Tip: Take a script with a deliberately introduced bug (a reversed -gt/-lt, or a missing quote around a variable) and run it once normally, then once with bash -x — comparing the two runs is the fastest way to see what the trace output actually adds.
Common Mistake: Trusting a command’s result without checking whether it actually succeeded — a script that runs cd /some/directory and then immediately deletes files with a wildcard, without checking whether the cd itself succeeded, can end up deleting files in the wrong directory entirely if that cd silently failed; checking $? (or writing cd /some/directory || exit 1) after any command whose failure would have serious consequences is not optional in a script meant to run unattended.
Important Questions:
- What is the practical difference between a syntax error and a logical error in a shell script? A syntax error violates the shell’s own grammar (an unmatched quote, a missing
fiordone) and is caught by bash before the script executes at all, usually with a specific line number pointing close to the problem; a logical error is syntactically valid and runs to completion without any error message, but produces an incorrect result because the script’s actual logic doesn’t do what was intended — it requires actually tracing execution (with tools likebash -xor addedechostatements) rather than just reading an error message, since there isn’t one. - What problem does
trap 'rm -f $TMPFILE' EXITsolve that simply puttingrm -f $TMPFILEat the end of the script does not? A plainrm -f $TMPFILEat the end of the script only runs if execution actually reaches that line, so a script interrupted early (Ctrl-C, an unexpected error causing an earlyexit) would leave the temporary file behind;trap ... EXITregisters that cleanup command to run automatically whenever the script exits for any reason, normal completion or early interruption alike, guaranteeing the temporary file is removed either way.
Chapter 31: Flow Control: Branching with case
Difficulty: Easy · Course: Linux / Shell · Key topics: case syntax, wildcard patterns, multiple patterns with |, menu-driven scripts
Introduces case as a cleaner alternative to a long chain of elif branches when a script needs to check one value against several possible patterns. Covers its syntax — a value, a list of pattern) commands ;; blocks, and a closing esac — and shows that each pattern supports the same wildcard matching as filename globbing (*, ?, [abc]), plus multiple alternative patterns joined with | in a single branch. Demonstrates the pattern’s most common real use: a menu-driven script that reads a single-character user choice and branches to the matching action with a wildcard *) catch-all for any invalid input.
Key Points:
case value in pattern1) commands ;; pattern2) commands ;; esaccomparesvalueagainst each pattern in order and runs the first match’s commands.- Each pattern supports the same wildcard syntax as filename globbing —
*matches anything,[yY])matches a single character from a set. - Multiple alternative patterns can share one branch by separating them with
|, e.g.y|Y) echo "yes" ;;. - A trailing
*)pattern, matching anything not caught by an earlier pattern, is the standard way to handle invalid or unexpected input as a final catch-all. casereads more clearly than an equivalent chain ofelifbranches whenever a script is really checking one value against a list of discrete possibilities rather than a range of numeric conditions.
Practice Tip: Rewrite a small three-branch if/elif/else block you’ve already written as an equivalent case statement — seeing the same logic in both forms side by side makes it obvious when each one reads more clearly.
Common Mistake: Forgetting the double semicolon ;; at the end of a case branch — without it, bash does not know where that branch’s commands end, producing a syntax error at the next pattern instead of pointing directly at the actual missing ;;.
Important Questions:
- Why would a script use
caseinstead of a chain ofif/elifstatements to handle a menu choice? When a script is checking one single value against several discrete, known possibilities — exactly what a menu choice is —caseexpresses that intent more directly than a chain ofeliftests, each of which would otherwise need to repeat the same variable and comparison operator;casealso supports pattern matching (wildcards, character sets, alternatives joined with|) directly in each branch label, which a plain string-equalityelifchain cannot do as concisely. - What does the pattern
y|Y|yes)in acasestatement match? It matches if the value being tested is exactlyy, or exactlyY, or exactlyyes— the|character separates multiple alternative patterns within one branch, and the branch’s commands run if the value matches any one of them.
Chapter 32: Positional Parameters
Difficulty: Medium · Course: Linux / Shell · Key topics: $1..$9, $0, $#, shift, $* vs $@, handling more than nine arguments
Covers how a script receives the arguments it was called with. $0 holds the script’s own invocation name, $1 through $9 hold individual positional arguments, and $# holds the total argument count, letting a script check whether it was called with the right number of arguments before using them. Introduces shift for handling more arguments than there are numbered variables, sliding every positional parameter down by one (or more) each time it’s called, and draws out the real, easily-missed difference between $* and $@ when either is used unquoted versus quoted as "$@".
Key Points:
$0is the script’s own name as invoked;$1through$9are the first nine positional arguments;$#is the total count of arguments passed.- A script should check
$#before assuming the expected arguments were actually provided, exiting with a usage message if not. shiftdiscards$1and shifts every remaining positional parameter down by one, so what was$2becomes the new$1— the standard way to loop over more arguments than$1..$9can name directly.$*and$@both expand to all positional parameters, but behave identically unquoted, joining everything on whitespace; the real difference only appears quoted."$@"expands to each positional parameter as its own separate, correctly-quoted word (preserving any embedded spaces in a single argument);"$*"instead collapses all of them into one single string."$@"is almost always the correct choice when forwarding a script’s own arguments on to another command, precisely because it preserves each original argument’s boundaries.
Practice Tip: Write a tiny script containing only for arg in "$@"; do echo "[$arg]"; done, then call it with one argument containing a space, quoted (./test.sh "hello world") — seeing it print as one bracketed item rather than two is the clearest proof of what "$@" actually preserves.
Common Mistake: Using unquoted $@ or $* to forward a script’s arguments to another command — without quotes, both word-split every argument on whitespace, so an argument containing a space (like a filename with a space in it) gets silently split into two separate arguments passed to the next command; "$@" quoted is required to preserve each original argument intact.
Important Questions:
- A script is called as
./deploy.sh staging "my app". What do$1,$2, and$#each hold?$1holds “staging”,$2holds “my app” as a single argument (the quotes at the call site kept it together as one positional parameter despite the embedded space), and$#holds2, since exactly two arguments were passed regardless of how many words are inside either one. - Why does
cp $@ /backupbreak when one of the script’s arguments is a filename containing a space, whilecp "$@" /backupdoes not? Unquoted$@word-splits every positional parameter on whitespace before passing the result tocp, so a single argument like “my file.txt” is split into two separate words, “my” and “file.txt”, whichcpthen tries to treat as two different filenames;"$@"quoted instead expands to each original positional parameter as its own separate, intact word, so “my file.txt” is correctly passed tocpas one single filename argument.
Chapter 33: Flow Control: Looping with for
Difficulty: Medium · Course: Linux / Shell · Key topics: for word in list, C-style for ((…)), looping over command output, pathname expansion, IFS & word splitting
Covers for, the loop for iterating over a known or generated list of items rather than looping on a condition. Works through its traditional shell form, for word in list; do ...; done, where the list can be literal words, a wildcard expansion (each matching filename becoming one iteration), or a command substitution’s output split into words. Adds the bash-specific C-style form, for (( i=0; i<10; i++ )), for straightforward numeric counting loops, and shows how changing IFS temporarily controls exactly how a command’s output gets split into the list for iterates over, which matters when items contain spaces.
Key Points:
for word in list; do commands; doneruns the loop body once per item inlist, withwordholding the current item each time.for file in *.txt; do ...; doneuses pathname expansion directly as the list, so the loop body runs once per matching filename.for line in $(command); do ...; doneloops over a command’s output, but the output is word-split onIFSfirst — a line containing spaces becomes multiple separate iterations unlessIFSis changed.- The C-style form,
for (( i=0; i<10; i++ )); do ...; done, is a bash extension for straightforward numeric counting, familiar from C-family languages. - Temporarily setting
IFS=$'\n'before a loop makes it split only on newlines instead of any whitespace, the standard fix for iterating correctly over lines that themselves contain spaces.
Practice Tip: Run for f in *.sh; do echo "Found: $f"; done in a directory of scripts, then try for i in {1..5}; do echo "Count: $i"; done right after it — one loop over real files and one over a generated numeric range covers the two most common everyday uses of for.
Common Mistake: Looping for line in $(cat file.txt) to process a file line by line — command substitution’s output is word-split on whitespace by default, so a file with any spaces within its lines gets iterated word by word rather than line by line, silently producing far more iterations than there are actual lines; while read line; do ...; done < file.txt from Chapter 29 is the correct tool for genuine line-by-line processing instead.
Important Questions:
- Why does
for word in $(cat file.txt); do echo "$word"; doneprint more lines of output than the file actually has, when the file’s lines contain multiple words? Command substitution’s output is word-split onIFSbeforeforever sees it, and by defaultIFSincludes spaces, tabs, and newlines all together — so a file’s line “hello world” becomes two separate list items, “hello” and “world”, each triggering its own loop iteration, rather than one iteration per actual line of the file. - What does the C-style loop
for (( i=1; i<=5; i++ )); do echo $i; doneprint? It prints the numbers 1 through 5, one per line:istarts at 1, the loop body runs and prints it,iis incremented byi++, and the loop repeats as long as the conditioni<=5still holds, stopping onceireaches 6.
Chapter 34: Strings and Numbers
Difficulty: Hard · Course: Linux / Shell · Key topics: parameter expansion for strings (length, substring, search & replace), arithmetic expansion, declare -i, number bases
Covers the string- and number-manipulation features built into parameter expansion itself, without needing an external tool like sed or awk for simple cases. Works through ${#var} for string length, ${var:offset:length} for extracting a substring, and the search-and-replace forms ${var/pattern/replacement} (first match) and ${var//pattern/replacement} (every match). Returns to arithmetic in more depth — $(( )) expansion, the let command, and declare -i for a variable that always behaves numerically — and closes with how bash represents different number bases (0x for hex, 0 for octal) inside arithmetic expressions.
Key Points:
${#var}expands to the length of the string stored invar, in characters.${var:offset:length}extracts a substring starting atoffset(0-based) forlengthcharacters; omittinglengthextracts to the end of the string.${var/pattern/replacement}replaces only the first match ofpattern;${var//pattern/replacement}replaces every match, mirroringsed‘ss///versuss///g.$(( expression ))evaluates integer arithmetic directly, supporting+ - * / %and comparison/logical operators, without spawning an external program likeexpr.declare -i varmarks a variable as always integer-typed, so any later assignment to it is automatically evaluated as an arithmetic expression rather than stored as a literal string.- Bash arithmetic recognizes a leading
0xfor hexadecimal and a leading0for octal directly inside$(( )), e.g.$((0xFF))evaluates to 255.
Practice Tip: Given filename="report.tar.gz", try to extract just “report” using parameter expansion alone (${filename%%.*}) instead of piping to an external tool — seeing bash’s own string operations solve a real problem is what makes this chapter’s toolkit worth remembering over reaching for sed out of habit.
Common Mistake: Using integer arithmetic operators on a variable that was never actually assigned a number, or expecting $((10/3)) to return a decimal — bash’s built-in arithmetic is integer-only, so $((10/3)) truncates to 3 rather than producing 3.33; a tool like bc or awk is needed for genuine floating-point math, since $(( )) silently discards any fractional remainder instead of erroring.
Important Questions:
- What does
${filename%.*}produce whenfilename="archive.tar.gz", and how is it different from${filename%%.*}?${filename%.*}removes the shortest match of the pattern.*from the end of the string, stripping only the final extension and leaving “archive.tar”;${filename%%.*}instead removes the longest matching suffix starting from the first dot, leaving just “archive” — the single%is a shortest-match trim from the end, and the double%%is a greedy, longest-match trim. - Why does
echo $((7 / 2))print3instead of3.5? Bash’s built-in arithmetic expansion$(( ))only supports integer arithmetic, not floating-point; division between two integers truncates any fractional remainder rather than producing a decimal result, so7 / 2evaluates to the integer3. Getting an actual3.5requires an external tool capable of floating-point math, such asecho "7/2" | bc -l.
Chapter 35: Arrays
Difficulty: Hard · Course: Linux / Shell · Key topics: indexed arrays, array assignment, associative arrays (declare -A), length, listing indexes, adding/removing elements
Introduces bash arrays, first as indexed arrays holding an ordered list of values under one variable name, created either by assigning individual elements (arr[0]=value) or all at once (arr=(a b c)). Covers reading back an element (${arr[2]}), every element (${arr[@]}), and the array’s length (${#arr[@]}), plus adding, removing, and listing indexes. Closes with associative arrays (declare -A), a bash feature indexing by arbitrary string keys instead of sequential numbers, useful for anything shaped like a lookup table — mapping a set of names directly to values without a separate parallel array for each.
Key Points:
arr=(one two three)creates an indexed array in one assignment; individual elements can also be set directly witharr[0]=one.${arr[2]}reads a single element by its (zero-based) index;${arr[@]}expands to every element as separate words;${#arr[@]}gives the total number of elements.unset arr[1]removes a single element, leaving a gap in the numbering rather than shifting later elements down;arr+=(four)appends a new element to the end.${!arr[@]}expands to the array’s list of indexes rather than its values, useful for iterating when index gaps might exist.declare -A mapcreates an associative array indexed by arbitrary string keys instead of sequential integers, e.g.map[hostname]=server1.- Associative arrays must be explicitly declared with
declare -Abefore use; assigning a string key to a plain (non-associative) array silently fails to work as intended.
Practice Tip: Build a small declare -A associative array mapping a few fruit names to their colors, then loop over it with for key in "${!fruit[@]}"; do echo "$key is ${fruit[$key]}"; done — this is the standard idiom for iterating an associative array’s keys and values together.
Common Mistake: Assuming unset arr[1] shifts every following element down by one index, the way removing an item from a list does in many other languages — bash instead leaves a genuine gap at index 1, so ${#arr[@]} (the count of actual elements) and the array’s highest index can diverge, which breaks a plain numeric for (( i=0; i<${#arr[@]}; i++ )) loop; iterating with ${arr[@]} directly, or with ${!arr[@]} for indexes, avoids assuming indexes are contiguous.
Important Questions:
- What is the practical difference between an indexed array and an associative array in bash? An indexed array is ordered and addressed by sequential integer positions starting at 0, created either element by element or all at once with
arr=(a b c); an associative array, created withdeclare -A, is instead addressed by arbitrary string keys the way a dictionary or hash map is in other languages, somap[hostname]=server1stores a value under the key “hostname” rather than under a numeric position. - After running
arr=(a b c d); unset arr[1], what does${#arr[@]}report, and why might a loop using that count as an upper bound behave unexpectedly?${#arr[@]}reports3, the number of elements actually remaining, but the array’s valid indexes are now 0, 2, and 3 — index 1 is simply missing rather than the later elements shifting down to close the gap; a plain numeric loop likefor (( i=0; i<3; i++ ))would therefore try to access the now-nonexistentarr[1]and skip the real element at index 3 entirely, which is why iterating with${arr[@]}or${!arr[@]}is safer once elements might have been removed.
Chapter 36: Exotica
Difficulty: Hard · Course: Linux / Shell · Key topics: subshells, process substitution, trap revisited, asynchronous execution & wait, named pipes, here documents & here strings
Closes the book with a deliberate grab-bag of advanced techniques that don’t fit neatly under earlier chapters. Revisits subshells — a command group in ( ) running in a separate child shell process, whose variable changes and cds don’t affect the parent — and process substitution (<(command)), which lets a command’s output be treated as if it were a file. Covers running commands asynchronously in the background with & and waiting for them with wait, named pipes (FIFOs) for connecting two otherwise-unrelated processes through the filesystem, and closes with here documents (<<EOF) for embedding a multi-line block of text directly inside a script and here strings (<<<) for feeding one short string in as if it were a file.
Key Points:
- A subshell, written
( commands ), runs in a separate child shell process; any variable assignment orcdinside it has no effect on the parent shell once it finishes. - Process substitution,
<(command), lets a command’s output be treated as a readable file by another command that expects a filename argument rather than piped stdin, e.g.diff <(ls dir1) <(ls dir2). - Running a command with a trailing
&starts it in the background immediately, returning control to the script;waitpauses until a specific background job (or all of them) finishes. - A named pipe (
mkfifo) is a special file that connects two independent processes’ stdout/stdin through the filesystem, letting them communicate without one being the direct parent of the other. - A here document,
command <<EOF ... EOF, feeds a multi-line block of text directly to a command’s stdin, embedded right inside the script instead of a separate file. - A here string,
command <<< "$variable", feeds a single string to a command’s stdin without needing a full here-document block, useful for a quick one-line input.
Practice Tip: Run diff <(ls dirA) <(ls dirB) on two real directories, then rerun the same comparison the long way — ls dirA > /tmp/a; ls dirB > /tmp/b; diff /tmp/a /tmp/b — to see the two temporary-file steps process substitution is quietly doing behind the scenes.
Common Mistake: Expecting a variable set inside a ( ) subshell, or inside a backgrounded & command, to be visible in the main script afterward — both run in a separate process with its own copy of the shell’s variables, so any assignment made inside is lost the moment that subprocess ends; a variable genuinely needed back in the parent script has to be communicated some other way, such as through a temporary file or the process’s actual output captured with command substitution.
Important Questions:
- What problem does process substitution,
<(command), solve that a plain pipe cannot? A pipe connects one command’s stdout directly to another command’s stdin, which only works when the receiving command is willing to read from stdin; some commands (likediff, which expects two filename arguments) are not written that way, so process substitution instead makes a command’s output available as something that looks like a regular file path, letting a program that only accepts filenames — likediff <(cmd1) <(cmd2)— consume live command output without a real temporary file ever being created by hand. - Why would a variable assigned inside a background job started with
¬ be visible in the rest of the script after that job finishes? Starting a command with a trailing&runs it as a separate child process running concurrently with the script, and like any subshell or child process it gets its own independent copy of the shell’s variables; changes made to that copy inside the background job never propagate back to the parent script’s variables, so the parent script’s own value for that variable name is unaffected no matter what the background job assigns to it internally.
Download The Linux Command Line PDF (Free)
This is the genuinely free Internet Edition, published directly by author William Shotts under a Creative Commons licence — there is no paywall and nothing to sign up for. The button below opens the book’s official page at linuxcommand.org, where the current Seventh Internet Edition PDF (Version 25.12) is linked; always download from the author’s own page rather than a random mirror, so you get the current, corrected edition.
↓ Download PDFHow to Study This Book
The book’s own four-part structure is also the right study order for almost everyone: Part 1 (Chapters 1–10) first, since every later chapter assumes you can already navigate the filesystem, read permissions, and manage a process without thinking about it. Do not skip ahead to shell scripting in Part 4 before Part 1 is genuinely comfortable — scripts are built entirely out of the same commands introduced there.
Part 1 is the load-bearing part of the whole book. Chapters 6 (Redirection) and 7 (Expansion and Quoting) in particular are worth re-reading once, since almost every later chapter — text processing, scripting, even package management one-liners — leans on pipes, redirection, and correct quoting without re-explaining them.
Part 2 (Chapters 11–13) is short and can be read quickly, but Chapter 12’s vi(m) introduction is worth actually practicing rather than reading passively — open a real file and do the chapter’s own movement and editing exercises in it, since vi is the one editor guaranteed to exist on every Linux system you will ever SSH into.
Part 3 (Chapters 14–23) is the least sequential part of the book — each chapter covers one largely independent tool area (package management, networking, text processing, printing). Read them in whatever order matches your actual coursework or project needs; Chapter 19 (Regular Expressions) and Chapter 20 (Text Processing) are the two most worth prioritizing, since sed, awk, and grep patterns reappear constantly in later real-world scripting.
Part 4 (Chapters 24–36) is written to be read in order — each flow-control chapter (if, while/until, case, for) builds on positional parameters and input-reading from the chapters just before it, and Chapter 30 (Troubleshooting) is placed deliberately in the middle of Part 4, not at the end, because the book wants you debugging real scripts while you’re still learning to write them, not just once at the very end.
Chapters 34 (Strings and Numbers), 35 (Arrays), and 36 (Exotica) are the most advanced material in the book and are reasonably treated as optional on a first pass through a one-semester course — come back to them once a real scripting project actually needs string manipulation, arrays, or a technique like process substitution, rather than studying them in the abstract with nothing to apply them to.
Because the whole book is one continuous, cumulative command-line vocabulary, the single most effective study method the book itself assumes is typing every example into a real terminal as you read it, not reading the commands as text; a command you have actually run and seen the output of is retained completely differently from one you have only read.
Used In These Programs
This book is widely used as the primary or recommended reading for a Linux Fundamentals, Operating Systems Lab, or introductory Systems Administration course in BS Computer Science and BS Information Technology programs, and as self-study material for anyone preparing for their first real command-line job or internship. Browse all Linux books or all Computer Science books.
Who Should Read This
The Linux Command Line is written for a complete beginner to the command line — it assumes no prior Linux experience and no programming background at all, starting from what a terminal even is. That makes it the right first book for BSCS/BSIT students starting an Operating Systems lab, anyone about to work on a Linux server or in a DevOps-adjacent role for the first time, and self-taught programmers who learned a language but never learned the shell properly. It teaches no programming language of its own beyond bash scripting, so it pairs well as a practical companion to a formal Operating Systems textbook such as Operating Systems: Three Easy Pieces, which explains the OS concepts this book’s commands sit on top of. It is not the right choice for someone who already has solid shell fluency and wants deep systems internals, kernel programming, or a rigorous treatment of any single tool covered here — each topic is a practical, complete introduction rather than a specialist’s reference.
Applicable Universities
This book is useful for students at Pakistani universities offering BSCS, BSIT, or BSSE programs, including Punjab University, Virtual University, COMSATS, FAST, UET, NUST, GIKI, LUMS, and other HEC-recognized institutions, and for any self-taught learner preparing for Linux system administration, DevOps, or backend development work.
FAQs
Is The Linux Command Line free and legal to download?
Yes. Author William Shotts publishes the “Internet Edition” of the book free of charge on its official site, linuxcommand.org, under a CC BY-NC-ND 3.0 licence — you may read and share it for free, non-commercially, without modification, as long as you credit the author. A separate paid print/ebook edition is also sold by No Starch Press for readers who want a physical or DRM-free commercial copy; downloading the free PDF from the author’s own site is entirely legitimate.
Which edition is this, and is it current?
This page is built from the Seventh Internet Edition, Version 25.12, dated January 5, 2026 — the current free edition as published on the author’s own site. Its content lines up with the No Starch Press print “3rd Edition,” published February 2026. The book is actively maintained; earlier free Internet Editions (going back to the Second Internet Edition in 2013) remain available in various archives online, but the current edition is the one linked from this page.
How many chapters does the book have, and how is it organized?
36 chapters across four parts: Part 1, Learning the Shell (Chapters 1–10) covers navigation, files, permissions, and processes; Part 2, Configuration and the Environment (Chapters 11–13) covers environment variables, the vi(m) editor, and prompt customization; Part 3, Common Tasks and Essential Tools (Chapters 14–23) covers package management, networking, text processing, and compiling software; Part 4, Writing Shell Scripts (Chapters 24–36) teaches bash scripting from a first script through flow control, arrays, and advanced techniques.
Is it good for complete beginners who have never used Linux before?
Yes — this is explicitly the book’s target reader. Chapter 1 starts from what a terminal and a shell even are, and every later chapter builds up gradually from there with no assumed prior Linux or programming experience. Someone who has already used Linux comfortably for a year or more may find Part 1 slow, but Part 4’s shell-scripting material still rewards a careful first read even for an experienced user who has never formally learned scripting.
Does it teach shell scripting, or is it only about using commands interactively?
Both, in roughly equal depth. Parts 1 through 3 (23 chapters) cover interactive command-line use — navigation, permissions, processes, networking, text processing, and more; Part 4 (13 chapters) is a complete introduction to bash scripting, covering variables, functions, every flow-control construct (if, while/until, case, for), positional parameters, string and array manipulation, and debugging — enough to write real, non-trivial automation scripts by the end.
What is the exact licence, and what does CC BY-NC-ND 3.0 actually allow?
The free Internet Edition is licensed under Creative Commons Attribution-NonCommercial-NoDerivatives 3.0 United States (CC BY-NC-ND 3.0), stated directly in the book’s own front matter and on its official download page. In practice this means you may freely read, copy, and share the unmodified PDF as long as you credit the author (Attribution), you don’t sell it or use it commercially (NonCommercial), and you don’t create or distribute a modified version of it (NoDerivatives) — it is stricter than a share-alike licence, since no derivative works are permitted at all.
Does this book cover shell scripting well enough to skip a separate scripting course?
For most introductory purposes, yes — its 13-chapter Part 4 is a genuinely complete bash scripting course on its own, covering everything from a first script through functions, all flow-control constructs, positional parameters, arrays, and debugging techniques like bash -x tracing. It does not cover more advanced systems-programming topics like writing daemons, signal handling beyond trap, or scripting in a language other than bash, so a dedicated Systems Programming course still adds real value beyond what this book teaches.
Related Books
- Operating Systems: Three Easy Pieces – Arpaci-Dusseau (the OS theory behind the processes, permissions, and storage commands this book teaches you to use)
- Think OS – Allen B. Downey (a shorter, Linux-based companion for students who want the programmer’s-eye view of the operating system underneath the shell)
- Computer Networking: Principles, Protocols and Practice – Bonaventure (goes deep on the networking concepts behind this book’s Chapter 16 tools like
ping,dig, andssh) - Think Python – Allen B. Downey (a natural next step for students who want a full general-purpose programming language once bash scripting’s limits start to show)
The Linux Command Line, by William Shotts, is a free, complete introduction to the Linux shell — from your very first commands through file permissions, processes, text processing, and a full 13-chapter course in bash shell scripting. Browse more Computer Science books for the rest of your semester.
The Linux Command Line, by William Shotts. Seventh Internet Edition, Version 25.12 (January 5, 2026), published free by the author at linuxcommand.org, textually aligned with the No Starch Press print 3rd Edition (February 2026). Licensed under Creative Commons Attribution-NonCommercial-NoDerivatives 3.0 United States (CC BY-NC-ND 3.0). Read it free at its official source, http://linuxcommand.org/tlcl.php
© Copyright Policy
Freebooks.pk is an educational resource for students in Pakistan. Textbooks and other books belong to their respective boards, authors and publishers, and all rights remain with them. If you are the copyright holder of any material on this page and want it changed or removed, please contact us or file a request under our DMCA policy. We review every request and remove reported content promptly.
