Saltar a contenido

Linux From Scratch (Beginner)

Beginner path: never-opened-a-terminal → contributing to Linux-adjacent OSS (scripts, dotfiles, docs).

Printing this page

Use your browser's PrintSave as PDF. The print stylesheet hides navigation, comments, and other site chrome; pages break cleanly at section boundaries; advanced content stays included regardless of beginner-mode state.


Linux From Scratch - Beginner to Command-Line Contributor

From "I have never opened a terminal" to "I can read /etc, write a useful shell script, and contribute small fixes to Linux-adjacent open source projects (sysadmin tools, configs, dotfiles, docs)."

Who this is for

  • You have never used a terminal, OR
  • You can cd and ls and that's about it.

That's it. If you need to know something, this path will teach it.

Note: this path is different

Other "from scratch" paths in this site teach a programming language. Linux is an operating system, not a language. This path teaches:

  • The command line - how to use Linux through a terminal.
  • The filesystem - where things live and why.
  • Permissions, users, processes - the basics of multi-user computing.
  • Shell scripting - automating with bash.
  • Package management, networking, common admin tasks.
  • Reading config files in /etc and understanding what they do.
  • Contributing to Linux-adjacent OSS: sysadmin tools, dotfiles, distro docs, small CLI utilities.

We do not teach kernel programming, C, device drivers, or compiling the kernel from source. Those are advanced topics; the platform's "Linux Kernel" path covers them.

What you'll need

  • A Linux environment. Options:
  • Native Linux (Ubuntu, Fedora, Arch, anything).
  • macOS - already Unix; most commands work identically.
  • Windows - install WSL2 (Windows Subsystem for Linux). Free, runs a full Linux inside Windows.
  • A virtual machine (VirtualBox + Ubuntu ISO) if you're cautious.
  • A text editor - nano (in the terminal, beginner-friendly) or VS Code (with the Remote-WSL extension if you're on Windows).
  • About 5 hours per week. Path is sized for 4-6 months at that pace.

Why Linux

  • Most servers in the world run Linux. Cloud, containers, Kubernetes, all Linux underneath.
  • Free and open. No license cost; thousands of free distros.
  • Long-term skill. Linux changes slowly. What you learn now is useful in 10 years.
  • Linux-adjacent OSS is huge. Sysadmin tools, dotfiles repos, distro docs, package configurations - endless opportunities to contribute even without being a programmer.

How this path works

Same template as the other paths: one concept per page, code/commands shown then walked through, exercise per page, "what you might wonder" Q&A.

The pages

# Title What you'll know after
00 Introduction What we're doing and why
01 Setup Linux/WSL/Mac terminal open
02 The shell - ls, cd, pwd Navigating the filesystem
03 Files and directories Creating, copying, moving, deleting
04 Reading files cat, less, head, tail, wc
05 Searching grep, find, locate
06 Permissions and users chmod, chown, sudo
07 Pipes and redirection Everything-is-a-stream
08 Processes ps, top, kill, jobs
09 Shell scripting basics bash, variables, if, for
10 Editing in the terminal nano, vim (just enough)
11 Package managers apt, dnf, brew, where software lives
12 Networking essentials curl, ssh, scp, ports
13 Picking a project Linux-adjacent OSS candidates
14 Anatomy of a small project Case study
15 Your first contribution Workflow + PR

Start with Introduction.

00 - Introduction

What this session is

A 10-minute read. Sets expectations.

What you're going to be able to do, eventually

By the end:

  • Open a terminal and navigate the filesystem with confidence.
  • Read and edit text files from the command line.
  • Manage files and directories - create, copy, move, delete, search.
  • Understand users, permissions, and sudo.
  • Write small shell scripts that automate routine tasks.
  • Install software via your distro's package manager.
  • Use ssh to connect to remote servers.
  • Read configuration files in /etc and understand them.
  • Clone a small Linux-adjacent open-source project (a dotfiles repo, a sysadmin tool, a CLI utility), make a small improvement, and submit a pull request.

That last point is the goal.

What this path is NOT

This path is not about programming in C, writing kernel modules, compiling the kernel from source, or device drivers. Those are different - covered by the "Linux Kernel" path on this site.

This path is the foundation. The kernel path assumes you can already use Linux comfortably.

The deal

  • It's slow on purpose. One concept per page.
  • It assumes nothing. Every command explained.
  • You have to type the commands. Reading without typing won't stick.
  • You will get errors. Often. That's normal. Errors aren't scary; they tell you what's wrong.

What you need

  • A Linux environment:
  • macOS - works fine. The terminal is already a Unix shell (zsh by default).
  • Linux native (Ubuntu, Fedora, etc.) - perfect.
  • Windows - install WSL2 (Windows Subsystem for Linux). Microsoft's official, free, runs a real Linux inside Windows. Search "install WSL2" - Microsoft has a one-command installer.
  • A virtual machine - VirtualBox + Ubuntu ISO. Slower; works.
  • A text editor - VS Code is fine; we'll also teach nano (terminal-based, beginner-friendly).
  • ~5 hours/week.

What you do NOT need

  • Math.
  • A computer-science background.
  • A new computer - Linux/WSL/Mac runs on almost anything from the last 10 years.
  • Any programming experience.

How long this realistically takes

4 to 6 months at 5 hours/week to reach "submit a PR to a Linux-adjacent project."

The first 6 weeks (pages 01-06) are the steepest part - terminal navigation, file management, permissions. After that you're doing useful things and it gets easier.

What success looks like

You'll be able to: - Open a terminal on any Linux system and feel at home. - Write a small shell script to automate something tedious. - Understand what a sysadmin's job involves. - Read most config files in /etc and know what they configure. - Submit a small PR to a Linux-adjacent project.

You will not be able to: - Write kernel code (different path). - Pass a senior SRE interview (years of experience past this).

What you'll have: the foundation to keep going.

Where this path leads next

After this, depending on interest: - Linux Kernel path - go deep. Kernel internals, eBPF, mm, networking. - Container Internals path - Linux primitives (namespaces, cgroups) underlie containers. - Kubernetes path - orchestration on top of Linux. - A programming language path (Python, Go) if you want to write applications.

One last thing before we start

The terminal is intimidating. It looks alien. After 2 weeks of daily use it feels normal. After 6 months it feels faster than a GUI for many tasks. Patience.

If a page feels too dense - stop, re-read. Still dense? Skip, come back.

Ready? Next: Setup →

01 - Setup

What this session is

About 30 minutes. You'll get a working Linux environment, open a terminal, and run your first commands.

Step 1: Get a Linux environment

Pick one based on your machine:

macOS

You already have a Unix shell. Open the Terminal app (search via Spotlight: ⌘ Space, type "terminal"). Done.

macOS isn't strictly Linux but it's close enough for everything in this path. ~95% of commands work identically.

Linux (native, dual-boot, or VM)

You're using Linux. Open a terminal. (Most desktop environments: Ctrl+Alt+T, or find "Terminal" in apps.)

If you don't have Linux yet and want to install it: - Ubuntu is the gentlest entry. ubuntu.com/download/desktop. Install in a VM (VirtualBox) if you don't want to dual-boot.

Windows: install WSL2

WSL2 (Windows Subsystem for Linux) runs a real Linux kernel inside Windows. Free, supported by Microsoft, the right way to learn Linux on a Windows machine.

Open PowerShell as Administrator and run:

wsl --install

That installs WSL2 and Ubuntu by default. Reboot if asked. Open the new "Ubuntu" app from your Start menu. The first launch sets up a username and password.

You now have a Linux terminal on Windows. Everything in this path works there.

Step 2: Your first command

In the terminal, type:

echo "hello, world"

Press Enter. You should see:

hello, world

That's a Linux command. echo is "print this." You just used the terminal.

Step 3: A few more essentials

Try these one at a time. After each, look at the output.

whoami

Prints your username.

hostname

Prints your machine's name.

pwd

"Print working directory." Shows the folder you're currently in. Probably /home/yourname or /Users/yourname.

ls

"List." Shows the files and folders in the current directory.

date

The current date and time.

uname -a

System info - kernel name, version, architecture.

Each of these is a separate command - a small program. The terminal lets you run them one after another.

What just happened

The terminal is a shell - a program that reads what you type, runs a corresponding command, and prints the result. The default shell on most Linux distributions is bash; on modern macOS it's zsh. They're 95% compatible for what we'll do.

When you typed echo "hello, world": - echo is a command (a small program). - "hello, world" is an argument (the input). - The shell ran echo with that argument. - echo printed its argument; the shell printed the output to your terminal.

The general shape: command argument1 argument2 ....

Try changing things

  1. Run echo hello (no quotes). Same output? (Usually - quotes are needed when the argument contains spaces or special characters.)

  2. Run ls -l. What's different? (Long listing - shows more info per file.)

  3. Run ls -a. (Includes "hidden" files - those starting with .)

  4. Run ls -la. (Both options combined.)

  5. Run ls /etc. (List the /etc folder specifically.)

You've now seen options (also called flags): the things starting with - that modify a command's behavior.

A note on errors

Try this:

lz

You'll see:

bash: lz: command not found

That's an error. The shell looked for a command called lz, didn't find one, told you. Errors aren't scary; they're feedback.

Try this:

cat nofile.txt

You'll see something like:

cat: nofile.txt: No such file or directory

Another error. Again, just feedback.

Reading errors is most of the skill. They almost always tell you exactly what's wrong.

Command history

Press the up arrow in your terminal. Your previous command appears. Press up again for the one before. This is history - the terminal remembers what you've typed.

Press Ctrl-R to search history: type a few characters and the shell finds matching past commands.

history (the command) prints a numbered list. !42 re-runs command #42. !! re-runs the last command.

You'll use history constantly.

Tab completion

Type:

cd /e

Then press Tab. The shell completes it to cd /etc/ (the only thing starting with /e). Try cd /us then Tab - completes to /usr/.

If multiple things match, press Tab twice to see them all.

Tab completion is the single biggest productivity feature of the shell. Use it. Always.

Clearing the screen

Lots of output? Type clear or press Ctrl-L. Fresh screen.

Exiting the terminal

exit (the command). Or close the terminal window. Or Ctrl-D on an empty line.

Exercise

In your terminal, run each of these. Look at the output. Note anything that confuses you.

whoami
hostname
pwd
ls
ls -l
ls -la /
date
uname -a
echo "I made it through page 01"

Try at least one wrong command (whodoami, say) and one with a wrong argument (cat nofile). Read the errors.

What you might wonder

"Why is ls not list?" Old Unix tradition. Commands are short to type. Two-letter names for the most-used. Three- or four-letter for the next tier. Full words for less-used. You get used to it.

"What's the difference between bash and zsh?" Same shape. zsh has extras (better completion, better prompts, more configurability). For our purposes, identical.

"Do I need to type these commands every time?" You'll learn shortcuts. Up arrow for last command, Tab to complete, Ctrl-R to search history. Eventually you re-type very little.

"Should I be worried about typing dangerous commands?" We'll be careful. The two dangerous commands to be conscious of: - rm -rf (recursive delete, no confirmation) - we'll cover safely in page 03. - Anything starting with sudo (page 06) - runs as the system admin user.

For now you can't break anything serious.

Done

You have: - A working Linux/macOS/WSL terminal. - Run your first commands. - Met options (flags) and history. - Used Tab completion.

Next page: navigate the filesystem.

Next: The shell - ls, cd, pwd →

02 - The Shell: ls, cd, pwd

What this session is

About 30 minutes. You'll learn to navigate the Linux filesystem - find where you are, move around, list what's there.

The filesystem is a tree

Linux organizes files in a tree. The top is / ("root"). Everything is under it.

/
├── bin/        commands available to all users
├── etc/        system configuration files
├── home/       user home directories
│   ├── alice/
│   └── bob/
├── tmp/        temporary files (cleaned periodically)
├── usr/        installed software
├── var/        variable data (logs, mail)
└── ...

(macOS uses /Users/alice instead of /home/alice - that's the main difference.)

Your home directory is where your personal files live. Always written ~ (tilde) as shorthand: ~/Documents = /home/yourname/Documents.

pwd: where am I?

pwd

Prints your current directory. Probably /home/yourname (your home) when you open a fresh terminal.

cd: change directory

cd /etc        # go to /etc
pwd            # /etc
cd ~           # back to home
pwd            # /home/yourname
cd /           # go to root
cd             # alone, also goes home

Special destinations: - cd ~ - your home. - cd / - the filesystem root. - cd .. - up one level. - cd - - back to the previous directory you were in.

Relative vs absolute paths: - Absolute start with /: /etc/hosts, /usr/bin/ls. - Relative don't, and are relative to your current directory: Documents/notes.txt means "from where I am, into Documents, then notes.txt."

Special relative shortcuts: - . - current directory. - .. - parent directory.

cd /etc
cd ..          # now in /
cd ./tmp       # same as cd /tmp from /

ls: list contents

ls             # files and folders in current directory
ls /etc        # contents of /etc
ls -l          # long format (permissions, owner, size, date)
ls -a          # include hidden files (start with .)
ls -h          # human-readable sizes (with -l: 4.0K, 1.2M)
ls -la         # combine: long format + hidden

You'll meet hidden files often. Anything starting with . is conventionally "config" or "internal": .bashrc, .gitignore, .vscode/. Not actually hidden - just convention-hidden.

A tour: walk through /

cd /
ls

You should see directories like bin, etc, home, tmp, var, usr. Each has a purpose:

Directory What lives here
/bin and /usr/bin Commands available to all users (ls, cat, grep, ...)
/sbin and /usr/sbin Commands for system administration (mount, ifconfig)
/etc System-wide configuration files
/home User home directories
/tmp Temporary files; often cleared on reboot
/var Variable data - logs (/var/log), mail spools, etc.
/usr Installed software (binaries, libraries, docs)
/opt Third-party software (sometimes)
/proc Virtual filesystem exposing kernel info
/sys Virtual filesystem for kernel objects
/dev Device files (disks, terminals)
/root Home directory of the root (admin) user

Most of this you don't need to touch as a beginner. Knowing the layout helps you predict where to find things.

File metadata: ls -l

ls -l ~

Output looks like:

drwxr-xr-x  3 alice alice 4096 May 17 10:23 Documents
-rw-r--r--  1 alice alice  128 May 16 14:11 notes.txt

Each row, left to right:

Column Meaning
drwxr-xr-x Type + permissions (page 06)
3 Number of links (usually 1 for files)
alice Owner
alice Group
4096 Size in bytes
May 17 10:23 Last modified
Documents Name

The first character is the type: - d - directory - - - regular file - l - symbolic link - b c s p - special files (you'll rarely see these)

The next 9 characters are permissions. We'll cover them in page 06.

Tab completion saves your life

Type:

cd ~/Doc

Press Tab. The shell completes to ~/Documents/ (if it exists). Saves typing; prevents typos.

If multiple things match (~/D and you have Documents and Downloads), pressing Tab twice shows both.

Exercise

In your terminal:

  1. pwd - note where you are.
  2. cd / - go to root.
  3. ls - see the top-level directories.
  4. cd /etc - go to etc.
  5. ls | head - see the first ~10 entries (the | and head are page 04 and 07; for now, just try).
  6. cd ~ - back home.
  7. ls -la - long-format listing including hidden files. Count how many of your home's items are hidden (start with .).
  8. cd ~/Documents (if it exists; else create it: mkdir ~/Documents and then cd).
  9. cd .. - back to home.
  10. cd - - back to Documents.

Spend 10 minutes wandering. cd somewhere, ls, pwd, cd ... Build muscle memory.

What you might wonder

"Why both /bin and /usr/bin?" Historical. The split made more sense when /usr was on a separate disk. Modern distros often merge them; the duplication is for compatibility.

"What's . and .. actually?" Every directory contains two implicit entries: . (itself) and .. (its parent). ls -a / shows them.

"Why is my home /Users/alice on macOS but /home/alice on Linux?" macOS does it differently. ~ works on both as the abbreviation.

"Should I cd by typing the full path or by hopping?" Whichever is faster. Tab completion + history makes long paths fast. Use absolute paths when you want certainty.

Done

  • Recognize the filesystem tree.
  • Use pwd, cd, ls (with -l, -a, -h).
  • Distinguish absolute and relative paths.
  • Use ., .., ~, - as shortcuts.
  • Read ls -l output.

Next: Files and directories →

03 - Files and Directories

What this session is

About 30 minutes. You'll learn to create, copy, move, rename, and delete files and directories.

Create a directory: mkdir

cd ~
mkdir practice
ls

mkdir creates a directory. mkdir -p path/to/nested creates intermediate directories as needed:

mkdir -p ~/practice/a/b/c

That creates all four (practice, a, b, c).

Create an empty file: touch

cd ~/practice
touch hello.txt
ls

touch creates an empty file if it doesn't exist. If it does exist, touch updates its modification time but doesn't change the content. Useful for "make sure this file exists."

Copy: cp

cp source destination

cp hello.txt hello2.txt           # copy file to file
cp hello.txt ~/Documents/         # copy file into a directory (keeps name)
cp -r mydir mydir2                # copy directory recursively

-r (recursive) is required when copying directories.

Move and rename: mv

mv is both "move" and "rename" - same command, depending on context:

mv old.txt new.txt                # rename
mv hello.txt ~/Documents/         # move into directory
mv ~/Documents/foo.txt .          # move from there to here ('.' = current)
mv dir1 dir2                      # rename or move directory

No -r needed for mv - directories are moved in place.

Delete: rm

rm hello.txt              # delete a file
rm file1 file2 file3      # delete multiple
rm -r mydir               # delete a directory and everything in it (recursive)
rm -f file                # force - no errors if file doesn't exist
rm -rf path               # recursive + force - combine carefully

The most dangerous command in Linux

rm -rf / (with a typo, or as root) deletes your entire filesystem. There's no undo, no trash bin. The shell does exactly what you tell it.

Read rm commands twice before pressing Enter. Be especially careful with variables: rm -rf $foo/ where $foo is empty becomes rm -rf / - disaster.

There's no trash for rm. Files are gone. Some distributions have trash-cli (trash-put instead of rm) - install if you want a safety net.

Wildcards (globs)

The shell expands * and ? into matching filenames before running the command:

ls *.txt              # all .txt files
ls *.tx?              # .txt, .tx1, .tx5, etc - ? matches one character
ls a*                 # all starting with 'a'
ls *2024*             # all containing '2024'
rm /tmp/*.log         # all .log files in /tmp

Two globs you'll meet: - * - zero or more characters (not crossing /). - ? - exactly one character. - ** (in some shells) - match across directories.

A few more handy ones

  • tree - show directory contents as a tree. Install if not present: sudo apt install tree / brew install tree.
  • du -sh dir - disk usage (size) of a directory, human-readable.
  • df -h - disk free across all mounted filesystems.
  • stat file - detailed metadata about a file.

Exercise

In ~/practice:

  1. mkdir -p projects/blog/posts - create nested dirs.
  2. cd projects/blog.
  3. touch index.html style.css script.js - create three files.
  4. mkdir posts/2026 and touch posts/2026/first-post.md posts/2026/second-post.md.
  5. ls -la - see the whole tree.
  6. cp index.html backup.html - copy.
  7. mv backup.html index.html.bak - rename.
  8. cp -r posts/2026 posts/2027 - copy a directory.
  9. ls posts/ - verify.
  10. rm index.html.bak - delete the backup.
  11. rm -r posts/2027 - delete the copied directory.
  12. Bonus: tree . (install tree if not present). See the final structure.

Don't actually rm -rf ~/practice - leave it for the next page.

What you might wonder

"Why no trash?" Unix tradition: be explicit. The convenience of "I can recover" creates the bad habit of "I'll just delete and check later." Be deliberate; verify before deleting.

"What's the difference between cp and cp -r?" cp only copies files. cp -r recursively copies directories and everything in them. Tools like cp won't operate on directories without the explicit recursive flag - safety feature.

"What does the shell do with *?" The shell expands it to a list of matching files before running the command. rm *.log becomes rm a.log b.log c.log (the shell does the substitution, then rm runs with those arguments).

"What if I delete something important?" If you're using a filesystem with snapshots (ZFS, btrfs with snapshots, Time Machine on macOS), you might recover. Otherwise: gone. Backups exist for this reason.

Done

  • Create directories (mkdir, mkdir -p).
  • Create empty files (touch).
  • Copy files and directories (cp, cp -r).
  • Move/rename (mv).
  • Delete (rm, rm -r) - carefully.
  • Use globs (*, ?).

Next: Reading files →

04 - Reading Files

What this session is

About 30 minutes. You'll learn to read text files in the terminal - without opening an editor.

cat: show a whole file

cat ~/.bashrc

cat (short for "concatenate") prints a file's contents to your terminal. For small files, perfect. For large ones, it scrolls off the top - useful tools for that below.

You can cat multiple files at once:

cat file1.txt file2.txt          # both, in order

For files with binary content (images, executables), cat produces gibberish. Stick to text.

less: page through a file

For files too big to view all at once:

less /etc/services

Inside less:

  • Space - next page.
  • b - previous page.
  • Arrow keys - line by line.
  • /pattern - search forward.
  • ?pattern - search backward.
  • n / N - next/previous match.
  • g - top.
  • G - bottom.
  • q - quit.

less is the file viewer in Linux. Use it for anything over ~50 lines.

(Memory note: there's also more, which is older and inferior. Use less.)

head and tail: first/last N lines

head file.txt            # first 10 lines
head -n 5 file.txt       # first 5 lines

tail file.txt            # last 10 lines
tail -n 20 file.txt      # last 20 lines
tail -f file.log         # follow - print new lines as they appear (great for log files)

tail -f is one of the most-used commands for watching log files in real time. Ctrl-C to stop.

wc: count lines/words/bytes

wc file.txt          # lines, words, bytes
wc -l file.txt       # just lines
wc -w file.txt       # just words
wc -c file.txt       # just bytes

Useful in combinations: "how many lines in this log?" wc -l app.log. We'll see it piped with other commands in page 07.

file: what type is this?

file mystery.bin

Tells you what kind of file it is - ASCII text, PNG image, ELF 64-bit LSB executable, etc. Useful when you've downloaded something and aren't sure what you got.

xxd / hexdump: see bytes

For looking at binary files:

xxd file.bin | head

Shows hex + ASCII side by side. Rarely needed at this level; mentioned for recognition.

A note on encoding

Most text files are UTF-8. Occasionally you'll meet files with different encodings (Windows-1252, etc.) that look corrupted in your terminal. Use iconv to convert, or set LANG / LC_ALL for your shell session. Beyond beginner scope.

Exercise

  1. Open a long config file with less:

    less /etc/services
    
    Scroll, search (type /tcp and press Enter), navigate with arrows, quit with q.

  2. Show the last 20 lines of your bash history:

    tail -n 20 ~/.bash_history       # may not exist if you've never used bash
    

  3. Count the lines in /etc/services:

    wc -l /etc/services
    

  4. What kind of file is /usr/bin/ls?

    file /usr/bin/ls
    

  5. Show the first 5 lines of /etc/passwd:

    head -n 5 /etc/passwd
    
    What's in that file? (Each line is a user account.)

  6. Bonus: if you have a system log accessible (/var/log/syslog on Debian/Ubuntu, /var/log/system.log on macOS), follow it with tail -f. Watch it for a minute (sometimes you'll see new log lines appear). Ctrl-C to stop.

What you might wonder

"Why use less instead of an editor?" - Faster - opens instantly for huge files. - Doesn't accidentally modify anything. - Available on every Linux/Unix system.

You'll meet less constantly in real work - man (page 05) uses it; git log uses it; many tools pipe through it.

"What's the difference between tail -f and just tail?" Plain tail shows the last N lines and exits. tail -f keeps the terminal open and shows new lines as they're added. Use -f for live monitoring.

"cat file | less vs less file - which?" less file directly. The piped form works but creates a useless extra process. However, command | less is common when paging through any command's output that's too long.

Done

  • Show files with cat (small) or less (any size).
  • Navigate less (Space, /, q).
  • Grab first/last lines with head and tail.
  • Follow logs with tail -f.
  • Count with wc.
  • Identify file types with file.

Next: Searching →

05 - Searching

What this session is

About 30 minutes. You'll learn grep (search inside files), find (search the filesystem), locate (fast filename search), and a couple of modern alternatives.

grep: search inside files

grep "pattern" file.txt           # lines containing "pattern"
grep "error" /var/log/syslog      # lines containing "error" in a log file
grep -i "error" file.txt          # case-insensitive
grep -n "error" file.txt          # show line numbers
grep -r "pattern" /etc            # recursive (search all files in /etc)
grep -v "pattern" file.txt        # inverse - lines NOT containing
grep -c "pattern" file.txt        # count of matching lines

grep is the most-used search tool in Linux. Memorize it.

You can pass multiple files:

grep "TODO" *.md                  # search all .md files in current directory

Patterns are regular expressions ("regex"). Simple cases (literal text) just work. For real regex (., *, [], ^, $), look it up when you need it.

Useful flag combo: -rni = recursive, line numbers, case-insensitive. Probably what you want when looking for something in a codebase.

Modern alternative: ripgrep (rg)

ripgrep is grep rewritten in Rust - much faster, smarter defaults (ignores hidden + gitignored files), more readable output. Install:

  • macOS: brew install ripgrep
  • Linux: sudo apt install ripgrep or sudo dnf install ripgrep
rg "pattern"            # search recursively from current directory
rg "pattern" path/      # search in a specific directory
rg -i "pattern"         # case-insensitive
rg -t py "TODO"         # search only Python files

Use rg over grep in interactive use; learn grep because it's universally available.

find: search the filesystem

find /path -name "pattern"            # files matching name
find . -name "*.txt"                  # all .txt files under current dir
find ~ -type d -name "Documents"      # directories named Documents in your home
find /var/log -type f -mtime -1       # files modified in the last 1 day
find . -size +10M                     # files larger than 10MB
find . -empty                         # empty files or directories

find is powerful and verbose. The basic shape: find <where> <criteria> [<action>].

Common criteria: - -name "pattern" - filename matches. - -type f / -type d - files / directories only. - -size +N / -size -N - bigger / smaller than N (with k/M/G suffixes). - -mtime -N / -mtime +N - modified less than / more than N days ago.

You can combine:

find ~/Documents -type f -name "*.pdf" -size +1M

That finds PDFs larger than 1MB in ~/Documents.

Common action: -delete (be careful) or -exec command {} \;:

find /tmp -type f -name "*.log" -mtime +7 -delete       # delete old log files
find . -name "*.bak" -exec rm {} \;                     # same idea, more flexible

The {} is replaced with each matching file. The \; ends the -exec command.

find searches in real time. locate searches an indexed database (much faster but might be stale).

locate filename.txt
locate -i Filename                 # case-insensitive

Install if not present: sudo apt install plocate (or mlocate). Run sudo updatedb to refresh the index.

Use locate for quick "where is that file again?"; use find for criteria-based searches or anything in directories you've just modified.

Modern alternative: fd

fd is to find what rg is to grep - Rust rewrite, faster, friendlier defaults.

fd pattern                # find files matching pattern (basename)
fd -e txt                 # find .txt files
fd -t d Documents         # find directories

Install: brew install fd / sudo apt install fd-find (called fdfind on Debian/Ubuntu).

Putting it together

A real session: find every file mentioning "TODO" in your code project's Python files:

cd ~/projects/myapp
grep -rn "TODO" --include="*.py"
# or with rg:
rg -t py "TODO"

Find log files older than a week:

find /var/log -type f -mtime +7

Find where the python3 binary lives:

which python3
# or
locate python3 | head

Exercise

  1. In ~/practice (from page 03 - recreate if needed), create some files with content:

    echo "this has TODO inside" > a.txt
    echo "no special marker here" > b.txt
    echo "another TODO here" > c.txt
    mkdir nested
    echo "TODO in nested" > nested/d.txt
    

  2. Find all files containing "TODO":

    grep -rn "TODO" .
    
    You should see 3 results across 3 files.

  3. Find all .txt files in your home directory:

    find ~ -type f -name "*.txt"
    

  4. Find empty directories:

    find ~ -type d -empty
    

  5. Search a system file: find every line in /etc/services mentioning port 22:

    grep "22/" /etc/services
    

  6. Bonus: if you have ripgrep installed, run rg "TODO" from your ~/practice. Compare speed and output to grep.

What you might wonder

"When should I use grep -r vs find + grep?" - grep -r works for simple "find pattern in any file under here." - find + grep for "files matching criteria AND content matching pattern": find . -name "*.py" -mtime -7 -exec grep -l "TODO" {} +.

"What's which?" which command shows the path to the executable that would run when you type command. which python3/usr/bin/python3 typically.

"Are regex patterns the same in grep as in Python?" Mostly. Basic regex is universal. Some advanced features differ; grep -E uses extended regex (closer to Perl/Python). When in doubt, test.

Done

  • Search inside files with grep (-r, -n, -i, -v).
  • Search the filesystem with find (by name, type, size, mtime).
  • Quick name lookups with locate.
  • Recognize modern alternatives rg and fd.

Next: Permissions and users →

06 - Permissions and Users

What this session is

About 45 minutes. You'll learn Linux's user system, file permissions, and sudo - the "run as administrator" mechanism.

Users and groups

Linux is multi-user. Every process runs as some user. Every file is owned by a user and a group.

whoami            # your username
id                # full info: UID, GID, group memberships
groups            # groups you belong to

The special user root (UID 0) can do anything - modify any file, kill any process, install software. Most operations should not run as root; you stay logged in as yourself and elevate when needed (via sudo).

File permissions

Every file has three sets of permissions, for three classes: - Owner (the user who owns it). - Group (the group it's assigned to). - Others (everyone else).

Each set has three permissions: - Read (r) - can read the contents. - Write (w) - can modify or delete. - Execute (x) - can run it (for binaries) or cd into it (for directories).

ls -l shows them:

-rwxr-xr-- 1 alice staff 1234 May 17 10:23 script.sh

The first character is the file type (- regular, d directory, l link).

The next 9 characters are permissions in three groups of 3: - rwx (owner) - read, write, execute. - r-x (group) - read, execute, no write. - r-- (others) - read only.

So alice can do anything; members of staff can read and execute but not modify; everyone else can only read.

Octal notation

Each rwx set is a 3-bit number: - r = 4 - w = 2 - x = 1

Sum them per set: rwx = 7, r-x = 5, r-- = 4.

So rwxr-xr-- is 754. You'll see permissions written this way constantly.

Common permission patterns:

Octal Symbolic Use case
755 rwxr-xr-x Executable scripts and binaries
644 rw-r--r-- Regular files (default for new files)
700 rwx------ Private directory or executable
600 rw------- Private file (SSH keys, sensitive data)
777 rwxrwxrwx World-writable - almost always wrong

chmod: change permissions

chmod 644 file.txt              # set octal
chmod +x script.sh              # add execute for everyone
chmod -w file.txt               # remove write for everyone
chmod u+x script.sh             # add execute for owner (u=user) only
chmod g-r file.txt              # remove read for group
chmod o= file.txt               # remove all permissions for others
chmod -R 755 directory/         # recursive

Symbolic form: [ugo][+-=][rwx]. - u user (owner), g group, o others, a all. - + add, - remove, = set exactly. - r, w, x for read/write/execute.

For making a script executable: chmod +x script.sh.

chown: change owner

sudo chown bob file.txt              # change owner to bob
sudo chown bob:staff file.txt        # change owner and group
sudo chown -R bob ~/shared           # recursive

You typically need sudo to change ownership (you can give files away or take them - risky without privilege).

Directories: special meanings

Permissions on directories are slightly different: - r - list contents (ls). - w - create, delete, rename files inside. - x - enter the directory (cd) and access files inside.

You can have x without r: enter the directory, access named files, but not list. (Useful for "private" directories where users know the file name but can't browse.)

sudo: run as root

Most beginner mistakes come from sudo. Use it deliberately.

sudo command           # run command as root (asks for your password the first time)
sudo apt update        # update package lists (needs root)
sudo nano /etc/hosts   # edit a system file

After typing your password, sudo remembers for ~5 minutes by default. Be careful with anything you run as root - there's no protection from yourself.

sudo -i or sudo su - gives you a root shell. Useful for many root commands in a row; risky because you're a typo away from disaster. Avoid unless necessary.

Common patterns

Make a script executable:

chmod +x ~/bin/myscript.sh

Lock down an SSH private key (required by SSH):

chmod 600 ~/.ssh/id_rsa

Recursively fix permissions in a directory:

chmod -R 755 mydir       # directories rwxr-xr-x, files rwxr-xr-x
# Often you actually want:
find mydir -type d -exec chmod 755 {} \;     # dirs rwxr-xr-x
find mydir -type f -exec chmod 644 {} \;     # files rw-r--r--

That handles the "dirs need execute but files probably don't" case.

A real example: read a system file

Try:

cat /etc/shadow

You'll get Permission denied. That file stores password hashes; only root can read it.

sudo cat /etc/shadow

Now it works. Don't actually do this in normal life - but it demonstrates the privilege system.

Exercise

  1. Create a script:

    cd ~/practice
    echo '#!/bin/bash' > greet.sh
    echo 'echo "Hello, $(whoami)!"' >> greet.sh
    ls -l greet.sh
    
    Note the permissions - probably -rw-r--r--. Not executable yet.

  2. Try to run it:

    ./greet.sh
    
    Permission denied - because no execute bit.

  3. Make it executable:

    chmod +x greet.sh
    ls -l greet.sh
    
    Now -rwxr-xr-x. Try running again:
    ./greet.sh
    
    Should print Hello, <your username>!.

  4. Change to owner-only execute:

    chmod 700 greet.sh
    ls -l greet.sh
    
    Now -rwx------. Still works for you; nobody else can read or run.

  5. Try sudo: read a root-only file:

    sudo cat /etc/shadow | head -n 3       # first 3 lines
    
    Type your password. Read the structure (username:password-hash:other-fields).

  6. Don't do this: sudo rm -rf / would wipe your system. We're not running this. The point: sudo is a sharp tool.

What you might wonder

"Why three classes (user, group, others)?" Historical Unix design: a user, a team, everyone else. Modern systems sometimes use ACLs (setfacl / getfacl) for finer control. The 3-class system is enough for 95% of cases.

"What's the setuid bit (s)?" A 4th permission bit that makes a binary run with its owner's permissions instead of the caller's. Used by passwd (changes /etc/shadow, owned by root). Recognize when you see it; don't set it without care - security risk.

"What's umask?" The default permissions stripped from newly created files. umask 022 (typical) means new files get 666 - 022 = 644 (rw-r--r--). You rarely need to change this.

"How do I share files with someone else?" Add them to a group, set the group on the file (chown :groupname file), give group read/write (chmod 664 file). For collaboration directories, the setgid bit (chmod g+s dir) makes new files inherit the directory's group. Beyond beginner; mentioned for awareness.

Done

  • Read ls -l permissions.
  • Use octal (755) and symbolic (u+x) chmod.
  • Distinguish user, group, others.
  • Use chown to change ownership.
  • Use sudo deliberately.

Next: Pipes and redirection →

07 - Pipes and Redirection

What this session is

About 45 minutes. You'll learn pipes (|) and redirection (>, >>, <) - the most powerful feature of the Unix shell. Once you internalize them, you can compose small commands into solutions to problems no individual command would solve.

The Unix philosophy

Write programs that do one thing well. Write programs to work together. Write programs to handle text streams.

Every command has three default channels: - stdin (standard input) - where the command reads from. - stdout (standard output) - where it writes results. - stderr (standard error) - where it writes errors.

By default, stdin is your keyboard; stdout and stderr are your terminal. Pipes and redirection let you wire them differently.

Pipe: |

The pipe sends one command's stdout into another's stdin:

ls | wc -l
  • ls produces a list of files, one per line.
  • wc -l counts lines.
  • Result: the number of files in the current directory.

You can chain as many pipes as you want:

ps aux | grep python | wc -l
  • ps aux - list all running processes.
  • grep python - keep only lines containing "python".
  • wc -l - count them.
  • Result: how many Python processes are running.

Read pipelines left to right. Each | is "and then send through."

Redirecting output: > and >>

Send stdout to a file:

ls > files.txt              # write list to file (overwrites existing)
ls >> files.txt             # append to file (creates if needed)
echo "hello" > greeting.txt
date >> log.txt             # append today's date to log.txt

> overwrites - if files.txt existed, it's now replaced. >> appends - keeps existing content, adds to the end.

For commands that produce a lot of output you want to save:

find / -name "*.log" > all-logs.txt 2>/dev/null

2>/dev/null redirects stderr to "nothing" - suppresses error messages from directories you can't read.

Redirecting input: <

Send a file's contents to a command's stdin:

sort < unsorted.txt > sorted.txt

sort reads from stdin (here, unsorted.txt) and writes sorted lines to stdout (here, sorted.txt).

In practice you rarely use < because most commands accept a filename argument too: sort unsorted.txt > sorted.txt works the same. But < is useful when a command only reads stdin.

tee: split output

tee writes to a file AND to stdout, so the pipeline continues:

ls -la | tee files.txt | wc -l
  • ls -la lists files.
  • tee files.txt writes the list to a file AND passes it on.
  • wc -l counts.
  • Result: file count, and files.txt also has the listing.

Useful when you want to save intermediate output without breaking the pipeline.

Combining: real examples

Count how many .py files are in a directory tree:

find . -name "*.py" | wc -l

Find the 5 largest files in your home:

du -ah ~ | sort -h | tail -n 5
  • du -ah ~ - disk usage for each file under home (human-readable).
  • sort -h - sort by human-readable size.
  • tail -n 5 - keep the last 5 (largest).

Find the most-used commands in your shell history:

history | awk '{print $2}' | sort | uniq -c | sort -rn | head -n 10
  • history - your command history.
  • awk '{print $2}' - second word of each line (the command itself).
  • sort - sort alphabetically.
  • uniq -c - collapse duplicates and count.
  • sort -rn - sort numerically, reversed (biggest first).
  • head -n 10 - top 10.

You don't need to understand every piece yet. Notice: a complex task is solved by piping simple tools together. That's the Unix way.

Save all warnings from a log file to a separate file:

grep "WARNING" /var/log/syslog > warnings.txt

Watch log files for errors as they happen:

tail -f /var/log/syslog | grep -i error

stderr vs stdout

Some commands write errors separately. Compare:

ls /nonexistent

Says "ls: cannot access '/nonexistent'." This is stderr.

ls /nonexistent > out.txt

You still see the error in your terminal! That's because > only redirects stdout. The file is empty.

To redirect stderr:

ls /nonexistent 2> err.txt

2> is "redirect file descriptor 2 (stderr)." 1> is stdout (same as >).

To redirect both to the same place:

ls /nonexistent > out.txt 2>&1
# or, more readable:
ls /nonexistent &> out.txt

2>&1 is "send stderr to where stdout is going." &> is shorthand for both.

To discard one or both:

command 2>/dev/null            # discard errors only
command > /dev/null 2>&1       # discard both
command &>/dev/null            # same, shorter

/dev/null is the "nothing" file - anything written there is discarded.

Useful text-processing pipes

A small zoo you'll see constantly:

sort                # sort alphabetically
sort -n             # sort numerically
sort -r             # reverse
sort -u             # sort + unique
uniq                # collapse adjacent duplicates (often paired with sort)
uniq -c             # count occurrences
cut -d, -f2         # field 2, comma-separated
cut -c1-10          # characters 1-10
awk '{print $1}'    # print first whitespace-separated field
sed 's/foo/bar/g'   # substitute foo with bar (everywhere)
tr 'a-z' 'A-Z'      # translate (here, lowercase to upper)
head / tail         # first/last N lines (page 04)

You don't need to memorize them all. Recognize them when you see them; look up specifics when you have a task.

Exercise

  1. Count files in your home:

    ls ~ | wc -l
    

  2. List the 5 largest directories under your home:

    du -h ~/* | sort -h | tail -n 5
    

  3. How many lines in your bash history are unique?

    history | awk '{$1=""; print $0}' | sort -u | wc -l
    
    (Strips the history number, sorts unique, counts.)

  4. Save the output of ls -la /etc to a file:

    ls -la /etc > etc-listing.txt
    wc -l etc-listing.txt
    

  5. Append the date to a log file:

    echo "Started: $(date)" >> mylog.txt
    echo "Did stuff" >> mylog.txt
    echo "Ended: $(date)" >> mylog.txt
    cat mylog.txt
    

  6. Discard errors from a find of /:

    find / -name "*.log" 2>/dev/null | head
    

  7. Bonus: print the top 5 most-used commands from your history:

    history | awk '{print $2}' | sort | uniq -c | sort -rn | head -n 5
    

What you might wonder

"How do I read stdin in a script?" With read (one line at a time) or by reading the whole thing: input=$(cat). Useful when writing filter scripts.

"Why use awk if I have cut?" cut is simpler for fixed delimiters. awk is a small programming language - handles complex columns, conditional output, math. Learn awk '{print $N}' for "print the Nth field"; that covers 80% of awk use.

"What about variables?" Coming in page 09 (shell scripting).

"What's sed for?" sed (stream editor) is for find-and-replace in streams: sed 's/foo/bar/g' file.txt replaces every foo with bar. Powerful for log processing, file editing in scripts.

Done

  • Pipe with |.
  • Redirect stdout with > (overwrite) and >> (append).
  • Redirect stderr with 2>, both with &>.
  • Discard output with /dev/null.
  • Use tee to split output.
  • Recognize the standard text-processing pipes.

Next: Processes →

08 - Processes

What this session is

About 45 minutes. You'll learn what a process is, how to see what's running, how to kill misbehaving programs, and how to manage background jobs.

What's a process

Every running program is a process with: - A PID (process ID) - a unique number. - A user - the user who started it. - Resources - open files, memory, network sockets. - A state - running, sleeping, waiting, zombie.

When you run a command, the shell creates a child process to execute it. When the command finishes, the process exits.

See what's running: ps

ps                       # processes in your current terminal
ps aux                   # ALL processes, with detailed info
ps aux | grep python     # only Python processes

ps aux is the most-used form. Columns:

Column Meaning
USER who started it
PID process ID
%CPU CPU usage
%MEM memory usage
VSZ / RSS virtual / resident memory (KB)
TTY terminal it's attached to (? if none)
STAT state (R running, S sleeping, Z zombie, T stopped)
START / TIME when started / cumulative CPU time
COMMAND what's running

Live view: top and htop

top is the classic interactive process viewer:

top

Press q to quit. M to sort by memory. P by CPU. 1 to show per-CPU breakdown.

htop is the modern, prettier alternative. Install: - sudo apt install htop - brew install htop

htop

F10 or q to quit. Use arrows to scroll; F9 to kill (with menu). Much easier than top.

btop (newer) is similar - colorful, more visual. Try it: sudo apt install btop / brew install btop.

Killing a process

kill PID                # send the default signal (TERM - polite request to exit)
kill -9 PID             # send SIGKILL - force kill (process can't ignore)
killall name            # kill all processes named "name"
pkill name              # similar

The two signals to know: - SIGTERM (15) - the default. "Please exit cleanly." The process can save state, close files, then exit. - SIGKILL (9) - "Die now." The kernel terminates the process immediately. No cleanup. Use only when SIGTERM doesn't work.

kill 1234          # SIGTERM to PID 1234
kill -TERM 1234    # same
kill -9 1234       # SIGKILL - use as last resort

Running things in the background

Add & to run a command in the background, freeing your terminal:

sleep 60 &
[1] 12345          # the shell tells you job # and PID

Your terminal returns. The process runs.

To see your background jobs in this shell:

jobs

To bring a background job to the foreground:

fg              # bring last
fg %1           # bring job #1

To send a foreground job to the background: - Press Ctrl-Z to suspend (pause it). - Type bg to continue it in the background.

To kill a job:

kill %1         # kill job #1

Long-running processes that survive logout

Jobs started with & die when you log out. For things that should survive:

nohup ./long-job.sh &      # ignore hangup signal

Output goes to nohup.out by default.

The modern alternative: tmux or screen - terminal multiplexers. Start a session, run things in it, detach, log out, come back hours later, reattach. Out of scope here; install and learn one - tmux is the more popular.

For production services: use systemd units (/etc/systemd/system/myservice.service). Beyond beginner; mentioned for recognition.

Process tree: pstree

pstree
pstree -p          # include PIDs
pstree alice       # only alice's processes

Shows parent-child relationships visually. Useful for understanding what spawned what.

Why a process won't die

Sometimes kill PID doesn't work: 1. The process is in uninterruptible sleep (waiting on disk or kernel - state D in ps). Wait it out; reboot if persistent. 2. The process is a child of another, and the parent ignores SIGCHLD. Kill the parent. 3. You don't own the process. sudo kill PID if you must.

Last resort always: sudo kill -9 PID.

Exercise

  1. List your processes:

    ps -u $USER
    

  2. Count Python processes on your system:

    ps aux | grep python | wc -l
    
    (Note: this counts the grep itself too. ps aux | grep python | grep -v grep | wc -l to exclude.)

  3. Start a sleep in the background:

    sleep 120 &
    jobs
    
    Note the PID. Bring it back to the foreground:
    fg
    
    Press Ctrl-Z to suspend, then bg to continue background, then kill it:
    kill %1
    

  4. Launch htop (or top). Sort by memory (in htop, F6 → MEM%; in top, press M). Find the biggest process. Quit.

  5. Bonus: find what process is using a given file:

    lsof | grep filename       # may need to install lsof
    
    Or what's listening on a port:
    ss -tlnp                   # TCP listening sockets, with process info
    

What you might wonder

"What's a zombie process?" A process that has finished but whose exit status hasn't been reaped by its parent. It still has a PID but no resources. Mostly harmless; reflects a buggy parent. Reboot clears them.

"Why does kill need a number? What are all the signals?" kill -l shows them all. The common ones: - 1 SIGHUP - terminal hangup; often triggers reload in daemons. - 2 SIGINT - what Ctrl-C sends. - 9 SIGKILL - uncatchable. - 15 SIGTERM - polite termination. - 19 SIGSTOP / 18 SIGCONT - pause / resume.

"How do I run a job on a schedule?" cron (Linux/macOS) or systemd timers. crontab -e to edit your scheduled jobs. Out of scope here; useful to know it exists.

"What's the relationship between processes and threads?" A process can have multiple threads (lighter-weight units of execution within the process, sharing memory). For most beginner tasks, this distinction doesn't matter. ps -L shows threads if you need to see them.

Done

  • Inspect processes with ps aux, top, htop.
  • Kill processes with kill, killall, pkill.
  • Distinguish SIGTERM (15) from SIGKILL (9).
  • Run jobs in the background (&, jobs, fg, bg).
  • Keep jobs alive past logout (nohup, tmux).

Next: Shell scripting basics →

09 - Shell Scripting Basics

What this session is

About an hour. You'll learn to write shell scripts - files containing a sequence of commands. Variables, conditionals, loops, command-line arguments. Enough to automate routine tasks.

Your first script

Create a file greet.sh:

#!/bin/bash
echo "Hello, $USER!"
echo "Today is $(date)"
echo "You are in $(pwd)"

Make it executable and run:

chmod +x greet.sh
./greet.sh

You should see something like:

Hello, alice!
Today is Sat May 17 14:23:45 BST 2026
You are in /home/alice/practice

What's new:

  • #!/bin/bash - the shebang line. Tells the OS this file should be run by bash. Always the first line of a shell script.
  • $USER - a variable ("interpolation" - bash substitutes the value).
  • $(date) - command substitution. The output of date is inserted here.

Variables

name="Alice"
greeting="Hello, $name"
echo "$greeting"            # Hello, Alice
echo "$greeting!"           # Hello, Alice!

Rules: - name=value - assignment. No spaces around = (it's not a comparison). - $name - read the variable. - Quote variables when they might contain spaces: echo "$path" not echo $path.

You can do command substitution:

files=$(ls)
count=$(ls | wc -l)
echo "There are $count files: $files"

Math (integer only in plain bash):

a=5
b=3
sum=$((a + b))
echo "$sum"                 # 8

The $(( ... )) is arithmetic expansion.

Command-line arguments

When you run ./script.sh foo bar, the script can access the arguments:

  • $1, $2, ... - first, second, ... argument.
  • $0 - script name.
  • $# - number of arguments.
  • $@ - all arguments (each as a separate word).
#!/bin/bash
echo "Script: $0"
echo "First arg: $1"
echo "Second arg: $2"
echo "Argument count: $#"
echo "All args: $@"

Run as ./script.sh alice bob:

Script: ./script.sh
First arg: alice
Second arg: bob
Argument count: 2
All args: alice bob

Conditionals

if [ "$1" = "hello" ]; then
    echo "you said hello"
else
    echo "you didn't say hello"
fi

Notes: - if condition; then ... fi - fi ends the block. (Yes, "fi" - "if" backward. Bash quirk.) - [ ... ] is the test command (literally a command named [). Spaces matter: [ "$x" = "y" ] works; ["$x"="y"] doesn't. - For strings: = (or ==), !=. - For integers: -eq, -ne, -lt, -le, -gt, -ge. - Negation: if ! [ ... ]; then ....

There's a modern [[ ... ]] form:

if [[ "$x" == "hello" ]]; then ...

[[ is bash-specific; supports more (regex, glob patterns). Use it for new scripts; [ for maximum portability.

File tests:

if [ -f "$file" ]; then echo "exists and is a regular file"; fi
if [ -d "$dir" ]; then echo "exists and is a directory"; fi
if [ -e "$path" ]; then echo "exists (any type)"; fi
if [ -r "$file" ]; then echo "readable"; fi
if [ -w "$file" ]; then echo "writable"; fi
if [ -x "$file" ]; then echo "executable"; fi

Loops

for name in alice bob chioma; do
    echo "Hello, $name"
done

Loop over a glob:

for file in *.txt; do
    echo "Processing $file"
done

C-style:

for ((i=1; i<=5; i++)); do
    echo "$i"
done

While loop:

n=10
while [ $n -gt 0 ]; do
    echo "$n"
    n=$((n - 1))
done

Reading user input

echo "What's your name?"
read name
echo "Hello, $name"

read reads one line from stdin.

Functions

greet() {
    echo "Hello, $1"
}

greet "Alice"
greet "Bob"

Inside the function: - $1, $2, ... - function arguments (not script arguments). - return N - exit code 0-255 (not a value). For returning a string, echo it and capture with $(...).

add() {
    echo $(($1 + $2))
}

sum=$(add 3 4)
echo "$sum"        # 7

A real script: clean temp files older than 7 days

#!/bin/bash
# clean-temp.sh - delete /tmp/myapp/*.log files older than 7 days

TARGET_DIR="/tmp/myapp"

if [ ! -d "$TARGET_DIR" ]; then
    echo "Directory $TARGET_DIR doesn't exist"
    exit 1
fi

count=$(find "$TARGET_DIR" -name "*.log" -mtime +7 | wc -l)
echo "Found $count old log files in $TARGET_DIR"

if [ "$count" -gt 0 ]; then
    find "$TARGET_DIR" -name "*.log" -mtime +7 -delete
    echo "Deleted $count files."
fi

New things: - # comment - anything after # is a comment. - exit 1 - exit the script with status code 1 (nonzero = failure). 0 is success.

Robust shell scripts: best practices

Two lines you should put at the top of every script you write:

#!/bin/bash
set -euo pipefail
  • set -e - exit immediately if any command fails.
  • set -u - error if you use an undefined variable.
  • set -o pipefail - pipelines fail if any command in them fails (not just the last).

Together: "fail fast, fail loud" - exactly what you want in shell scripts.

Also: always quote variables. "$file" not $file. Spaces and special characters in filenames break unquoted variables in surprising ways.

#!/bin/bash
set -euo pipefail

# Always quote
for file in "$@"; do
    if [ -f "$file" ]; then
        echo "Processing: $file"
    fi
done

Exercise

Write a script ~/practice/info.sh:

#!/bin/bash
set -euo pipefail

echo "User: $USER"
echo "Host: $(hostname)"
echo "Date: $(date)"
echo "Uptime: $(uptime)"
echo "Disk free:"
df -h | head -n 2

Make it executable: chmod +x info.sh. Run: ./info.sh. Read the output.

Stretch 1: modify it to accept an optional argument - a username - and report whether that user exists on the system (id "$1" >/dev/null 2>&1 returns 0 if exists, nonzero if not).

Stretch 2: write backup.sh PATH that copies the given file/directory to a timestamped backup:

#!/bin/bash
set -euo pipefail

src="$1"
dest="${src}.backup.$(date +%Y%m%d-%H%M%S)"
cp -r "$src" "$dest"
echo "Backed up to $dest"
Run as ./backup.sh ~/practice/info.sh. Check the backup file appears.

What you might wonder

"Should I learn bash or sh (POSIX shell)?" Bash for daily use. POSIX sh (run via /bin/sh) is more portable (works on systems without bash) but more limited. For shipping scripts: shebang #!/usr/bin/env bash and target bash.

"What about Python instead of bash?" For anything more than ~50 lines, use Python (or Go, or any "real" language). Bash is great for "string commands together"; it's a poor general-purpose language. The rule of thumb: if you need an array, switch to Python.

"What's ${var} vs $var?" Same thing. ${var} is the unambiguous form; useful when the next character would confuse the parser: ${name}_id (vs $name_id which would look up name_id).

"How do I debug a script?" Run with bash -x script.sh - prints each command before executing. Or add set -x inside the script to enable tracing partway. Disable with set +x.

Done

  • Write a shebang script.
  • Use variables and command substitution.
  • Read command-line arguments.
  • Use if, for, while.
  • Define functions.
  • Apply set -euo pipefail for safer scripts.
  • Always quote variables.

Next: Editing in the terminal →

10 - Editing in the Terminal

What this session is

About 30 minutes. You'll learn nano (beginner-friendly terminal editor) and just enough vim to survive - because vim is the default editor on many systems and you'll eventually be dropped into it without warning.

nano: the friendly editor

nano file.txt

That opens nano. You see the file's contents (empty for a new file) and a help bar at the bottom showing the key shortcuts.

Editing: just type. Arrow keys move the cursor. Backspace deletes.

The shortcuts (where ^ means "Ctrl"): - ^O - write (save). Hit Enter to confirm filename. - ^X - exit. Asks to save if there are unsaved changes. - ^K - cut current line. - ^U - paste. - ^W - search. Type query, Enter. - ^G - help.

That's it. nano is friendly because the help is always on-screen.

Use nano whenever you need to edit a file in the terminal and don't want to think about it.

vim: the unavoidable editor

vim is everywhere. Many systems set EDITOR=vim by default. git commit opens vim if you forget -m. Servers often only have vim installed. Learn just enough to survive.

Open a file:

vim file.txt

You're now in normal mode. Pressing letters doesn't insert text - it runs commands. (This is why beginners panic.)

The four modes

Vim has modes. The basics:

  • Normal mode (where you start) - navigate, run commands.
  • Insert mode - actually type text.
  • Visual mode - select text.
  • Command-line mode - for commands like save, quit.

Survival commands

When you open vim, you're in normal mode. To type text, press i to enter insert mode. To go back to normal, press Esc.

Minimal vocabulary:

Press What happens
i enter insert mode (insert before cursor)
a insert mode, after cursor
o new line below, enter insert mode
Esc back to normal mode
:w save (Enter to confirm)
:q quit
:wq save and quit
:q! quit without saving (force)
u undo
Ctrl-r redo
dd delete current line
yy "yank" (copy) current line
p paste after cursor
/pattern search (then n for next, N for previous)
gg go to top
G go to bottom
:N go to line N

A minimal workflow

  1. vim file.txt - open.
  2. Press i - now you're typing.
  3. Type your content.
  4. Press Esc - back to normal.
  5. Type :wq - save and quit.

If you're stuck and want to leave without changes: Esc, then :q!.

Why people love (or hate) vim

Vim's command language is composable. d3w deletes 3 words. 5dd deletes 5 lines. >} indents the next paragraph. Once you internalize it, you edit at near-thought speed. Getting there takes 2-4 weeks of daily use.

If you're going to spend time at the terminal, learn enough vim to be unfrustrated. vimtutor (a command - try it) is the official 30-minute interactive guide. Run it. Once.

Editor preference: EDITOR env var

Many commands (git commit, crontab -e, visudo) open whatever editor $EDITOR is set to.

echo $EDITOR              # see current
export EDITOR=nano        # for this session

Persistent: add export EDITOR=nano to ~/.bashrc or ~/.zshrc (the shell startup file - page 11 has a bit on these). New terminals will pick it up.

VS Code's code -w works too: export EDITOR='code -w'. The -w makes code wait for you to close the window before returning.

VS Code from the terminal

If you have VS Code installed, you can:

code file.txt           # open file in VS Code
code .                  # open current directory
code -d a.txt b.txt     # diff two files

(Setup: on macOS, install the code shell command via the VS Code Command Palette → "Shell Command: Install 'code' command in PATH".)

The hybrid workflow that many engineers use: terminal for navigation, building, running, debugging; VS Code (or another editor) for serious code editing; nano/vim for quick edits to one file.

Dotfiles: your shell's configuration

Your shell reads startup files from your home:

  • ~/.bashrc - read for each interactive bash shell.
  • ~/.bash_profile or ~/.profile - read for login shells.
  • ~/.zshrc - read for each zsh shell (the macOS default).

What goes in them: aliases, env vars, custom prompt, PATH additions.

A small .bashrc addition you might add:

# Custom prompt with current directory
PS1='\u@\h:\w$ '

# Useful aliases
alias ll='ls -la'
alias gs='git status'
alias gd='git diff'

# Add ~/bin to PATH for your own scripts
export PATH="$HOME/bin:$PATH"

After editing .bashrc, either open a new terminal or run source ~/.bashrc to apply changes.

People often manage these files as a git repo called "dotfiles" - version-controlled, sync across machines. A common first OSS contribution: improve someone's dotfiles repo (or your own).

Exercise

  1. nano:

    nano hello.txt
    
    Type some content. Save with ^O (Enter to confirm name), exit with ^X.

  2. vim survival:

    vim hello2.txt
    
    Press i. Type a few lines. Press Esc. Type :wq. You just used vim.

  3. vimtutor (highly recommended):

    vimtutor
    
    30 minutes. Best vim intro ever made. Even if you'll never use vim daily, it makes you not-panic when you land in it.

  4. Set up your shell:

  5. Find your startup file (~/.bashrc for bash, ~/.zshrc for zsh).
  6. Add an alias: alias ll='ls -la' (or pick your own).
  7. Add a PATH entry: export PATH="$HOME/bin:$PATH" (create ~/bin if it doesn't exist).
  8. Open a new terminal. Run ll. Confirm it's an alias for ls -la.

  9. Bonus - fish: if you find bash awkward, try fish shell. Auto-suggestions, syntax highlighting out of the box. Different syntax for scripting though (fish is its own shell language, not bash).

What you might wonder

"Should I learn vim or use VS Code?" Both. VS Code for serious editing; vim for "I'm SSH'd into a remote server and need to fix one line." Don't fight a vim-vs-emacs holy war; pick what helps.

"What about emacs?" Powerful editor with a different model (everything's a keybinding involving Ctrl/Meta). Some people love it. Less common than vim for terminal-only work. Try M-x butterfly.

"What's the difference between ~/.bashrc and ~/.bash_profile?" .bashrc is read for interactive non-login shells (new terminals after you're already logged in). .bash_profile is read for login shells (SSH, console login). Many setups source .bashrc from .bash_profile so the distinction doesn't matter.

"What about tmux/screen?" Terminal multiplexers - run multiple shells in one terminal window, detach and reattach. Hugely useful for SSH sessions. Try tmux; the basic commands are tmux new -s name, Ctrl-b d to detach, tmux attach -t name to reattach.

Done

  • Use nano comfortably.
  • Survive vim: i, type, Esc, :wq.
  • Know that vimtutor exists (and ideally run it).
  • Set EDITOR.
  • Add aliases and PATH entries to your shell config.

Next: Package managers →

11 - Package Managers

What this session is

About 30 minutes. You'll learn how to install software on Linux/macOS. Every distribution has a package manager - a tool that downloads software, manages dependencies, and updates everything.

Why package managers

In Windows world, you download installers per app. On Linux you don't - instead, a central package manager lets you:

  • Install software with one command.
  • Get automatic updates for everything.
  • Resolve dependencies automatically.
  • Verify package integrity.
  • Uninstall cleanly.

Every Linux distribution has one. macOS has third-party ones (Homebrew is most popular).

apt (Debian, Ubuntu)

sudo apt update              # refresh the package list (do this first)
sudo apt upgrade             # upgrade everything installed
sudo apt install <package>   # install
sudo apt remove <package>    # uninstall (keep config files)
sudo apt purge <package>     # uninstall + remove config
sudo apt search <pattern>    # find packages matching
sudo apt show <package>      # info about a package
sudo apt list --installed    # what's installed

apt needs root for install/upgrade/remove (it modifies the system).

Common things you might install:

sudo apt install git curl wget tree htop tmux build-essential
sudo apt install python3 python3-pip python3-venv
sudo apt install nodejs npm
sudo apt install postgresql redis

build-essential brings in a C compiler and basic build tools - needed if you'll compile any software.

dnf (Fedora, RHEL, CentOS)

sudo dnf update
sudo dnf install <package>
sudo dnf remove <package>
sudo dnf search <pattern>
sudo dnf info <package>

Same shape as apt, different distros. Older Fedoras used yum; dnf is the modern replacement.

pacman (Arch)

sudo pacman -Syu              # sync + upgrade everything
sudo pacman -S <package>      # install
sudo pacman -R <package>      # remove
sudo pacman -Ss <pattern>     # search
sudo pacman -Qi <package>     # info

Arch is power-user oriented. Mentioned for completeness.

brew (macOS, also Linux)

Homebrew is macOS's most popular package manager. Doesn't need root.

brew install <package>
brew uninstall <package>
brew upgrade                  # upgrade everything
brew update                   # refresh package list (do first)
brew search <pattern>
brew info <package>
brew list                     # what's installed

Common installs:

brew install git node python tmux ripgrep fd htop
brew install --cask visual-studio-code        # GUI apps via "cask"

Install Homebrew on macOS: brew.sh. One-line installer.

What gets installed where

apt puts files in: - /usr/bin - executables. - /usr/lib - libraries. - /etc/ - config files. - /usr/share/doc/<package>/ - documentation.

brew puts everything under /opt/homebrew/ (Apple Silicon) or /usr/local/ (Intel Mac, also Homebrew on Linux).

Use which <command> to see where any installed command lives:

which python3              # /usr/bin/python3 (apt) or /opt/homebrew/bin/python3 (brew)

Language-specific package managers

For programming languages, the system package manager is usually NOT the right choice - they ship outdated versions and don't help you per-project.

Instead, use language-specific tools:

  • Python: pip (PyPI). Inside virtual environments (python -m venv .venv).
  • Node.js: npm or yarn (npm registry).
  • Rust: cargo install <crate> for Rust binaries; library deps via Cargo.toml.
  • Go: go install <package>@<version> for binaries; library deps via go.mod.
  • Ruby: gem install <pkg> (or bundler for per-project).

The pattern: system package manager for OS tools (git, curl, nano, the language itself); language package managers for libraries within your projects.

Searching for packages online

When you don't know the package name:

Or just apt search <keyword>.

A real session: set up a development environment

A typical "I just installed Linux, what do I install" sequence:

# Update everything first
sudo apt update && sudo apt upgrade -y

# Essential CLI tools
sudo apt install -y git curl wget tree htop tmux build-essential

# Language runtimes
sudo apt install -y python3 python3-pip python3-venv nodejs npm

# Better terminal tools (optional)
sudo apt install -y ripgrep fd-find bat zoxide

# Editor (if not VS Code)
sudo apt install -y vim neovim

On macOS:

brew install git curl wget tree htop tmux
brew install python node
brew install ripgrep fd bat zoxide
brew install --cask visual-studio-code

Configuration files in /etc

Most installed software puts configuration in /etc/<package>/:

/etc/nginx/                 # nginx config
/etc/postgresql/            # PostgreSQL config
/etc/ssh/sshd_config        # SSH server config
/etc/hosts                  # local DNS overrides
/etc/passwd                 # user database (not passwords)
/etc/shadow                 # password hashes (root only)
/etc/fstab                  # filesystem mounts
/etc/crontab                # system-wide scheduled jobs

Knowing where things live is half the battle. When troubleshooting, "where's the config for X?" → /etc/X/.

To edit (as root): sudo nano /etc/<package>/<file>.

Exercise

  1. Update your package lists and upgrade installed packages:

    sudo apt update && sudo apt upgrade -y     # Linux
    # or
    brew update && brew upgrade                 # macOS
    

  2. Install tree if not present, then visualize a directory:

    sudo apt install -y tree                   # or brew install tree
    tree ~/practice
    

  3. Install htop and run it:

    sudo apt install -y htop
    htop
    
    Quit with q.

  4. Search for a package by topic:

    apt search "syntax highlighting"           # or brew search
    

  5. Bonus: install one of the modern CLI replacements:

  6. ripgrep (rg) - faster grep.
  7. fd - friendlier find.
  8. bat - cat with syntax highlighting.
  9. eza - ls with colors and icons.

Try bat README.md after install. Compare to cat README.md.

What you might wonder

"What's a .deb / .rpm?" The package file format for apt / dnf respectively. You can manually install one with sudo dpkg -i pkg.deb (Debian) or sudo rpm -i pkg.rpm. Usually you don't - apt install pkgname handles everything.

"What's snap / flatpak / AppImage?" Alternative cross-distro package formats. Snaps and flatpaks bundle dependencies and run in sandboxes. AppImage is a single-file portable executable. Some software (mostly GUI apps) ships this way. For learning Linux, stick with your distro's native package manager.

"How do I install something not in the package manager?" Often the project provides a tarball or installer. Always read instructions; never run untrusted curl ... | bash from a random source.

"Why don't I need sudo for brew?" By design - Homebrew installs to a user-writable directory (/opt/homebrew or /usr/local). Avoids the "everything's root" problem of system package managers. Tradeoff: separate trees per user.

Done

  • Use your system's package manager (apt, dnf, brew).
  • Install, remove, search, update, upgrade.
  • Know where files end up and which dir holds config.
  • Distinguish system package managers from language-specific ones.

Next: Networking essentials →

12 - Networking Essentials

What this session is

About 45 minutes. You'll learn the network commands every Linux user eventually needs - fetch URLs, SSH to remote machines, copy files, see what's listening on ports.

Fetch a URL: curl

curl https://example.com                     # print to terminal
curl -o page.html https://example.com        # save to file
curl -I https://example.com                  # HEAD request (headers only)
curl -L https://bit.ly/something             # follow redirects
curl -X POST -d "name=alice" https://api.example.com    # POST request

curl is the universal HTTP client. Read its man page once; the flag inventory is huge but you'll use 5-10 of them regularly.

For JSON APIs:

curl -s https://api.github.com/users/octocat | jq

jq is a JSON processor. Install: sudo apt install jq / brew install jq. Pretty-prints and filters JSON. Pair with curl constantly.

wget is a simpler alternative for "just download this":

wget https://example.com/file.zip

ssh: log into remote machines

ssh user@host                # log in
ssh user@host "command"      # run one command and exit
ssh -p 2222 user@host        # custom port (default 22)

The remote shell prompt is yours. Whatever you type runs on the remote machine.

First time connecting to a host: SSH asks you to verify the host's fingerprint. Say yes (after, ideally, verifying out-of-band). The fingerprint is stored in ~/.ssh/known_hosts.

SSH keys: passwordless login

Type a password every time? Use a key pair instead.

Generate:

ssh-keygen -t ed25519 -C "your_email@example.com"

Saves ~/.ssh/id_ed25519 (private - keep secret, never share) and ~/.ssh/id_ed25519.pub (public - fine to share).

Copy your public key to the remote:

ssh-copy-id user@host

After: ssh user@host logs you in without a password.

Permissions matter: - ~/.ssh must be 700. - ~/.ssh/id_* private keys must be 600. - ~/.ssh/id_*.pub public keys can be 644.

Wrong permissions and SSH refuses to use the keys.

Copy files: scp and rsync

scp (secure copy):

scp file.txt user@host:/path/to/dest/        # local to remote
scp user@host:/remote/file.txt local-name    # remote to local
scp -r mydir user@host:/dest/                # recursive (directories)

rsync is much smarter - incremental, resumable, efficient over slow links:

rsync -avh source/ user@host:/dest/          # sync directory contents
rsync -avh --delete src/ dest/               # also delete dest files not in src
rsync -avh --dry-run src/ dest/              # show what WOULD change

-a = archive (preserves permissions, recursion, etc.), -v = verbose, -h = human-readable sizes.

The trailing / on the source matters: - rsync src/ dest/ - copy contents of src into dest. - rsync src dest/ - copy src itself into dest (as dest/src).

Use rsync for everything except trivial single-file copies.

What's listening on what port: ss

ss -tlnp                # TCP, Listening, Numeric, Process info
ss -tunlp               # also UDP

Shows which programs are listening on which ports.

Older command: netstat -tlnp. Same idea, deprecated in favor of ss.

sudo ss -tlnp           # needs sudo to show process info for other users' processes

What process owns a port: lsof -i

sudo lsof -i :8080      # what's on port 8080
sudo lsof -i tcp        # all TCP usage

Useful when "port already in use" - lsof tells you who's holding it.

DNS lookup: dig and nslookup

dig example.com
dig +short example.com           # just the IP(s)
dig example.com MX               # mail exchanger records
nslookup example.com             # older alternative

dig is the modern, scriptable tool. nslookup is older and still around.

Ping and traceroute

ping example.com                 # send ICMP echo; press Ctrl-C to stop
traceroute example.com           # show the route packets take

Useful for "is this host reachable?" and "where does the path break?"

On modern systems some of these are restricted; use mtr (combo of ping + traceroute, interactive) if installed.

Firewall: ufw (Ubuntu)

sudo ufw status                  # what rules exist
sudo ufw allow 22/tcp            # allow SSH
sudo ufw allow http              # allow HTTP (port 80)
sudo ufw enable                  # turn on the firewall
sudo ufw deny 23                 # block telnet

Beyond beginner; mentioned for awareness. Most desktop users don't manage their firewall manually.

A real session: SSH into a server, sync a directory

# One-time setup: generate key, copy to remote
ssh-keygen -t ed25519
ssh-copy-id alice@my-server.example.com

# Now SSH passwordless
ssh alice@my-server.example.com
# ... do stuff on remote ...
exit

# Sync a local dir to the server
rsync -avh --delete ~/projects/myapp/ alice@my-server.example.com:/srv/myapp/

# Or fetch a file from the server
scp alice@my-server.example.com:/var/log/app.log ./

A few useful patterns

Test a webhook endpoint:

curl -X POST https://example.com/webhook \
  -H "Content-Type: application/json" \
  -d '{"event":"test"}'

Download a tar archive and extract:

curl -L https://example.com/foo.tar.gz | tar -xz

The tar -xz extracts a gzipped tar from stdin.

Stream output from a remote command:

ssh user@host "tail -f /var/log/app.log"

tail -f on the remote, output streams to your local terminal.

Exercise

  1. Fetch a URL:

    curl -s https://api.github.com/users/octocat
    
    Then pipe to jq if installed:
    curl -s https://api.github.com/users/octocat | jq
    

  2. DNS lookup:

    dig +short github.com
    

  3. See what's listening on your machine:

    ss -tlnp 2>/dev/null
    
    What ports does your computer expose?

  4. Generate an SSH key (if you don't have one):

    ls ~/.ssh/                              # check first
    ssh-keygen -t ed25519                   # if no id_ed25519 exists
    cat ~/.ssh/id_ed25519.pub               # your public key
    
    Copy the public key - you'll need it for GitHub (page 15) and any servers.

  5. Add your key to GitHub: GitHub Settings → SSH and GPG keys → New SSH key → paste your public key. After: ssh -T git@github.com should respond with your username.

  6. Bonus - rsync a folder to itself with --dry-run to see what would change:

    rsync -avh --dry-run --delete src/ dest/
    
    Useful before destructive syncs.

What you might wonder

"What's tmux for in this context?" SSH sessions die when your local connection drops. Run things inside tmux on the remote and they survive - reconnect with tmux attach. Indispensable for any remote work.

"What about nc (netcat)?" Low-level "make/accept TCP connections, send/receive bytes." Useful for testing services, transferring files when other tools aren't available. Niche but powerful.

"How do I serve a local directory over HTTP for quick sharing?"

python3 -m http.server 8000
Serves the current directory on port 8000. Open http://localhost:8000 in a browser. Great for sharing files on a LAN or testing.

"VPN, proxies, tunnels?" SSH itself can do port forwarding (ssh -L 8080:dest:80 user@host creates a tunnel). Beyond beginner; useful to know exists.

Done

  • Fetch URLs with curl (and maybe wget).
  • SSH to remote machines, with keys for passwordless.
  • Copy files with scp and rsync.
  • See listening ports with ss.
  • Look up DNS with dig.

You've now covered the core CLI skills. Remaining pages: how to apply this to OSS contribution.

Next: Picking a project →

13 - Picking a Project

What this session is

About 30 minutes plus browsing. What "Linux-adjacent open source" looks like for someone with terminal skills but not a programming language background - and how to evaluate one.

What kinds of project welcome you

You've learned command-line Linux but not a programming language. The OSS projects that fit your skills are:

  • Dotfiles repos. Configuration files for shells, editors, terminal tools. People share their setups.
  • Shell scripts and CLI utilities. Small bash/python/shell-based tools.
  • Documentation for any project. Docs are read more than code; improvements are always welcome.
  • Distro / package configuration. Helping with Debian package metadata, Arch AUR packages, NixOS configs.
  • Ansible roles, Docker images, Kubernetes manifests. Config-as-code.
  • Translations. Many projects need help translating their UI or docs.

Many of these are perfect first-contribution territory because they don't require deep programming.

What "manageable" means

  1. Small. Under 5k lines of any code/config.
  2. Active. Recent commits, responsive maintainers.
  3. Friendly tone. Welcoming README, code-of-conduct, polite issue interactions.
  4. You actually use or care about the project. Motivation matters.

10-minute evaluation

Same checks as the other beginner paths: - Stars: 100-10000. - Last commit: within a month. - Open issues: many, with labels. - PR merge time: under 14 days. - CONTRIBUTING.md exists.

Candidates

Tier 1 - dotfiles, shell tools, small CLI utilities

  • mathiasbynens/dotfiles - a famous, well-organized dotfiles repo. Issues range from "improve a script" to "add a new alias I find useful."
  • thoughtbot/dotfiles - opinionated dev setup. Active.
  • Your own dotfiles. Start one. Publish to GitHub. Contributors will come.
  • junegunn/fzf - fuzzy finder, written in Go. The user community contributes scripts and docs heavily.
  • tldr-pages/tldr - community-curated command examples (like a friendlier man). Adding/improving entries is a perfect first contribution. Highly recommended for a first PR.
  • tj/git-extras - extra git commands. Mostly shell scripts.

Tier 2 - sysadmin tools, infra config

  • ansible-collections/... - modular Ansible content. Each role is small.
  • prometheus-community/... - exporters, dashboards, alerts. Configuration repos.
  • Various distro package repos - Debian, Arch AUR, Nixpkgs. Mostly metadata work.
  • asdf-vm/asdf - version manager. Plugins are individual repos, often small.

Tier 3 - docs and translation

  • Most popular open-source projects - Linux distributions, Kubernetes, Docker, etc. - have separate docs/ repos or i18n/ directories where docs-only contributions are very welcome.
  • kubernetes/website - Kubernetes documentation. Active translation effort; English clarifications welcome.
  • docker/docs - Docker docs.
  • mozilla-l10n/... - Mozilla's localization repos.

Tier 4 - the big stuff, don't start here

  • The Linux kernel itself (C, complex, slow review).
  • Major web browsers.
  • Anything with a big-company CLA process.

tldr-pages: a near-perfect first contribution

tldr-pages deserves special mention. It's: - A community-curated set of example-driven man pages. - Written in Markdown. - Small, well-organized. - Welcoming to first-time contributors. - Has dozens of "good first issue" tickets at any time (typos, missing examples, missing commands).

If you finish this path and want one specific recommendation for a first PR: tldr-pages. Fix a typo. Add a missing example. Translate an entry. Submit. Five-minute PR; real-world workflow.

How to find issues

Project's Issues tab → Labels. Look for: - good first issue - help wanted - documentation - easy

Read 5-10. Pick one where: - Description is clear. - Fix is contained. - Nobody has claimed it. - It hasn't been sitting open for a year.

Comment: "I'd like to work on this; can you confirm it's still wanted?" Wait for the maintainer.

What counts as a contribution

For Linux-adjacent OSS:

  • Fix a typo in a README.
  • Improve the docs for a command-line flag.
  • Add a missing example to a tldr-pages entry.
  • Add a missing alias to a dotfiles repo.
  • Improve a shell script's error message.
  • Translate a doc page.
  • Add a missing test for a script.
  • Improve an Ansible role's variable defaults.

All real. All count.

Exercise

  1. Browse three projects from Tier 1.
  2. Run the 10-minute eval on each.
  3. Pick the most responsive.
  4. Read its CONTRIBUTING.md.
  5. Clone:
    git clone https://github.com/<owner>/<repo>
    cd <repo>
    
    Browse the file structure. Run any tests/checks they document.
  6. Browse the good first issue list. Pick two candidates - don't claim yet.

If you can't decide: pick tldr-pages. It's a guaranteed positive experience.

What you might wonder

"I don't really know any programming language. Am I going to be useful?" Yes. Plenty of OSS work isn't programming - docs, configs, scripts, infrastructure, translations. The contribution workflow (git, PRs, code review) is the same regardless of what you're contributing. After this path you have those skills.

"What if I want to get into 'real' Linux work?" Move to the "Linux Kernel" path next (which assumes a C-and-Unix background). Or pick up a programming language first - the "Go from scratch" or "Python from scratch" paths are good companions.

"Should I publish my own dotfiles?" Yes. Even a tiny dotfiles repo on your GitHub is useful - it's your config, version-controlled, syncable. Plus it teaches you git in a low-stakes way.

Done

  • Understand the kinds of Linux-adjacent OSS that fit terminal skills.
  • Run the 10-minute eval.
  • Have specific candidate projects in mind.
  • Have a tentative issue.

Next: Anatomy of a small project →

14 - Anatomy of a Small Linux-Adjacent Project

What this session is

About 45 minutes. Walk through the file structure of typical Linux-adjacent OSS projects - dotfiles repos, shell-script tools, infrastructure-as-code repos, doc projects.

These look different from programming-language projects. There's no pom.xml, no Cargo.toml, no package.json. Files are mostly configs and scripts.

Typical dotfiles repo

my-dotfiles/
├── README.md
├── LICENSE
├── install.sh
├── .bashrc
├── .vimrc
├── .gitconfig
├── .config/
│   ├── nvim/
│   │   └── init.vim
│   └── tmux/
│       └── tmux.conf
├── scripts/
│   ├── setup-mac.sh
│   ├── setup-linux.sh
│   └── helpers/
└── .gitignore

Roles: - README.md - what this is, how to use it, what platforms it supports. - install.sh - the entry point. Usually creates symlinks from ~/.bashrc etc. to the files in the repo. - Top-level dotfiles (.bashrc, .vimrc) - the configs themselves. - .config/ - for tools that follow XDG (Linux convention for per-user config under ~/.config/). - scripts/ - helpers for setup / system administration.

How they're "installed":

# Symlink approach (common)
ln -sf ~/code/my-dotfiles/.bashrc ~/.bashrc

When you edit ~/.bashrc, you're actually editing the file in the repo. Commit. Push. Sync to other machines by cloning + running install.sh.

Tools like stow automate the symlinking. Many dotfile repos use chezmoi or yadm for fancier sync.

Typical shell-script tool

For something like tj/git-extras:

git-extras/
├── README.md
├── LICENSE
├── CONTRIBUTING.md
├── Makefile                  (install / uninstall / lint)
├── bin/
│   ├── git-back
│   ├── git-changelog
│   ├── git-effort
│   └── ... (each a shell script)
├── etc/
│   └── git-extras-completion.zsh
├── man/                      (man-page sources)
│   ├── git-back.1.ronn
│   └── ...
├── test/
│   └── tests.bats
└── .github/workflows/

Roles: - bin/ - the actual scripts. Each is an executable file named git-<subcommand>. When installed (usually to /usr/local/bin/), git finds them and runs them as git back, git changelog, etc. - man/ - man-page source (in markdown-like ronn format here; compiled to nroff for man to display). - etc/ - extra files (shell completions, sample configs). - test/tests.bats - bats is a bash testing framework. Yes, you can unit-test bash. - Makefile - install/uninstall targets.

A first PR might be: fix a typo in bin/git-back, add a new option to git-changelog, improve the man page for one of them.

Typical Ansible role

ansible-role-nginx/
├── README.md
├── LICENSE
├── meta/
│   └── main.yml              (role metadata, dependencies)
├── defaults/
│   └── main.yml              (default variable values)
├── vars/
│   └── main.yml              (role-internal variables)
├── tasks/
│   ├── main.yml              (entry point - runs the tasks)
│   └── install.yml
├── handlers/
│   └── main.yml              (notifications, e.g., "restart nginx")
├── templates/
│   ├── nginx.conf.j2         (Jinja2 templates)
│   └── site.conf.j2
├── files/
│   └── ...                   (static files)
├── molecule/                 (testing infrastructure)
│   └── default/
│       └── molecule.yml
└── .github/workflows/

Ansible roles automate configuration of remote systems. Mostly YAML and templates; no compiled code. Very approachable for terminal-fluent beginners.

A first contribution: improve a default value, add a missing variable, fix a template that doesn't work on a specific OS, improve the README.

Typical docs project (e.g., kubernetes/website)

kubernetes-website/
├── README.md
├── LICENSE
├── content/                  (the actual docs)
│   ├── en/
│   │   ├── docs/
│   │   │   ├── concepts/
│   │   │   ├── tasks/
│   │   │   └── tutorials/
│   │   └── ...
│   ├── de/                   (German)
│   ├── es/                   (Spanish)
│   └── ja/                   (Japanese)
├── layouts/                  (templates)
├── static/                   (CSS, JS, images)
├── hugo.toml                 (Hugo static-site config)
└── .github/workflows/

Docs sites typically use a static-site generator (Hugo, MkDocs, Sphinx, Docusaurus). The content is Markdown; the generator turns it into HTML.

Contributing: edit a Markdown file under content/en/docs/.... PR. The CI builds the site to preview your changes.

Typical tldr-pages contribution

The tldr-pages repo is laid out as:

tldr/
├── README.md
├── CONTRIBUTING.md
├── pages/                    (English)
│   ├── common/
│   │   ├── git.md
│   │   ├── ls.md
│   │   └── ...
│   ├── linux/
│   ├── osx/
│   └── windows/
├── pages.fr/                 (French translations)
├── pages.es/                 (Spanish)
├── pages.de/                 (German)
└── ...

Each .md file is one command's tldr page, with example-driven entries. The format is strict but simple. Adding a missing command or example is a 5-minute PR.

CI: what your PR will be measured against

Open .github/workflows/. For these projects, CI typically runs:

  • For dotfiles: shellcheck (lints shell scripts).
  • For shell-script tools: shellcheck + bats tests.
  • For Ansible: ansible-lint + molecule tests.
  • For docs: the static-site builder (Hugo / MkDocs) builds without errors; link checker.

Install shellcheck (sudo apt install shellcheck / brew install shellcheck) and use it locally:

shellcheck install.sh

Catches common bash bugs. Use before submitting.

Exercise

Pick your candidate project from page 13:

  1. Clone it locally.
  2. Walk the file structure. Map each file/folder to a category.
  3. Read CONTRIBUTING.md end to end.
  4. Find the CI workflow YAML. Identify the commands it runs.
  5. Run those commands locally:
  6. For shell projects: shellcheck path/to/scripts/*.sh.
  7. For Ansible: ansible-lint ..
  8. For docs: read the build instructions; build locally with their tool.
  9. Look at the issue you tentatively picked. Identify which file(s) it touches.

You're ready for the actual contribution.

What you might wonder

"How do I know if a project is using Hugo, MkDocs, or something else?" Look at the root. hugo.toml / config.toml = Hugo. mkdocs.yml = MkDocs. docusaurus.config.js = Docusaurus. conf.py in a docs/ dir = Sphinx.

"What's .bats? Is that really a thing?" Yes. bats-core is a TAP-compliant testing framework for bash. Common in shell-tool projects (git-extras, bashly, etc.). Read a .bats file once; the format is intuitive.

"What if I want to contribute code, not docs/scripts?" Pick up a programming language. The "Go from scratch", "Python from scratch", "Java from scratch", "Rust from scratch" paths on this site are sized for that.

Done

  • Recognize the layouts of dotfiles, shell-tool, Ansible, and docs projects.
  • Find and read CI workflows.
  • Run the same checks locally before pushing.

Next: Your first contribution →

15 - Your First Contribution

What this session is

The whole thing. We walk through making a real contribution to a real Linux-adjacent OSS project, end to end.

By the end you'll have submitted a pull request. When it merges (days or weeks later), you'll be an open-source contributor - a small, real one.

The workflow

Same as every other language path:

  1. Fork on GitHub.
  2. Clone your fork.
  3. Add upstream as a remote.
  4. Branch off main.
  5. Set up dev environment, install any required tools.
  6. Change the file(s); test if applicable.
  7. Run lints / tests locally (same commands as CI).
  8. Push to your fork; open the PR.

Prerequisites: git installed and configured

sudo apt install git              # Linux
brew install git                  # macOS

Configure once per machine:

git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
git config --global pull.rebase false

For GitHub SSH auth, you should already have a key from page 12. If not, generate one and add to GitHub (page 12 covered this).

Test:

ssh -T git@github.com

Should respond with your username. If "permission denied," your key isn't on GitHub.

Step 1: Fork

GitHub → Fork (top right). Creates github.com/<you>/<project>.

Step 2: Clone

git clone git@github.com:<you>/<project>.git
cd <project>

(Use the git@github.com: URL for SSH clone - works with your SSH key. The https:// form requires a personal access token for pushes.)

Step 3: Add upstream

git remote add upstream git@github.com:<owner>/<project>.git
git fetch upstream
git remote -v

You should see origin (your fork) and upstream.

Step 4: Branch

git checkout -b fix/typo-in-readme

Step 5: Set up the dev environment

Read CONTRIBUTING.md. Install any tools it mentions:

  • shellcheck (for shell projects): sudo apt install shellcheck.
  • pre-commit (some projects use it): pip install pre-commit && pre-commit install.
  • Tool-specific: Hugo / MkDocs for docs builds, Ansible for Ansible projects, etc.

Run their test command. Confirm green before changing anything.

Step 6: Make the change

Open the file in your editor. Make the change. Save.

Keep it small, focused, conventional:

  • A docs PR fixing a typo: one line changed.
  • A dotfiles PR adding an alias: one line added.
  • A tldr-pages PR fixing an example: one or two lines changed.
  • A shell-script PR improving error handling: a small block.

Step 7: Run lints / tests locally

For shell projects:

shellcheck path/to/script.sh

For tldr-pages (which has its own linter):

# they have a Markdown linter; the CONTRIBUTING tells you
npm install
npm run test       # or similar

For docs sites (e.g., MkDocs):

mkdocs serve       # builds locally; serves at localhost:8000

If anything fails, fix locally. Don't push red.

Step 8: Commit and push

git add <files>
git commit -m "docs: fix typo in installation section"

Commit message conventions vary by project. tldr-pages, for example, expects: <page>: <action> like git checkout: add new example.

Push:

git push origin fix/typo-in-readme

GitHub prints a URL - click it to open the PR page.

Step 9: Open the PR

On the upstream repo, you'll see "Compare & pull request" banner.

  • Title. Short, descriptive.
  • Description. What changed and why. Reference the issue: Closes #123.
  • Checklist. Address every item in the PR template.

Submit. CI runs. Wait. Fix anything red by pushing more commits.

What happens next: review

Same as any OSS project:

  1. "LGTM, merging." Done.
  2. "Could you change these?" Most common. Address comments.
  3. "Not what we want, but thanks." Rare for good-first-issue work.
  4. Silence. Polite check-in after 1 week.

For tldr-pages and similar high-volume welcoming projects, PRs often merge within a day.

After the merge

  • Update your fork's main:
    git fetch upstream
    git checkout main
    git merge upstream/main
    git push origin main
    
  • Delete the branch.
  • Take a screenshot.

Worked example: a tldr-pages fix

Suppose you noticed tldr ls doesn't have an example for ls -lh /path. You decide to add one.

git clone git@github.com:<you>/tldr.git
cd tldr
git remote add upstream git@github.com:tldr-pages/tldr.git
git fetch upstream

git checkout -b ls-add-human-readable-example

# Edit pages/common/ls.md, add an example
nano pages/common/ls.md

Add a section like:

- List in long format with human-readable file sizes:

`ls {{[-lh|--long --human-readable]}} {{path/to/directory}}`

Save. Test (read the CONTRIBUTING for tldr-pages's linter):

npm install                # one-time
npm run test                # or: just the linter they mention

If green:

git add pages/common/ls.md
git commit -m "ls: add human-readable size example"
git push origin ls-add-human-readable-example

Open the PR. Address review (often "please reword to match our style guide"). Merged.

You're now a tldr-pages contributor.

After your first PR: what next

  1. Pick another issue in the same project. Each PR is faster than the last.
  2. After 3-5 PRs, become a regular. Review others' PRs (you don't need to be a maintainer to leave helpful comments).
  3. Build your own dotfiles / scripts repo. Use git for version control. Publish on GitHub.
  4. Pick a programming language. This path got you to "I can contribute config and docs." Pick up Go, Python, or Rust to contribute code.

Recommended next paths on this site:

What you might wonder

"My PR sat for weeks?" Polite check-in. Some projects are slow; some abandoned. Pick another.

"Maintainer was rude?" Disengage. Try another project.

"I'm not actually programming. Does this count as 'real' OSS contribution?" Yes. Docs and configs are as load-bearing as code. Maintainers value docs PRs heavily - they fix them less themselves.

"What about contributing to the Linux kernel itself?" Different path, requires C + deep Unix knowledge. The "Linux Kernel" path on this site is the on-ramp.

Done with this path

You've: - Installed Linux/WSL/macOS terminal access. - Navigated the filesystem confidently. - Managed files, permissions, processes. - Written shell scripts. - Used package managers and networking tools. - Read a real Linux-adjacent OSS project. - Picked, prepared, and submitted a PR.

What you should not do: claim you "know Linux" now. You know enough to use it daily and contribute small fixes. There's much more - the kernel, the deep parts of /proc and /sys, performance work, security hardening, distributed systems on Linux.

What you should do: keep using the terminal. Daily. Even when it's slower than a GUI. Familiarity compounds; in a year you'll do many things faster in the terminal than you ever could in a GUI.

Congratulations. You are no longer a beginner.