2022-10-18

Command Line Guide

A Practical Guide to Files, Pipes, Permissions, Processes, and Shell Scripts

Table of Contents

The command line is a text interface for controlling a computer. Instead of clicking through folders and menus, you enter commands to move between directories, inspect and modify files, connect programs, manage processes, and automate repeated work.

On macOS, open Terminal.app. Most Linux distributions provide a terminal application as well. macOS uses Zsh as its default interactive shell, while Bash is common on Linux. The basic commands in this guide work in both unless a section is marked as Bash-specific.

Warning

Commands such as rm, chmod, and chown can remove data or make it inaccessible. Check the current directory with pwd, inspect the target with ls, and preview wildcard matches with echo before running a destructive command.

Keyboard Shortcuts

These shortcuts make an interactive terminal much faster to use.

Process and Screen Control

ShortcutDescription
Ctrl + CInterrupt the foreground process
Ctrl + ZSuspend the foreground process
Ctrl + DSend end-of-file; at an empty prompt, exit the shell
Ctrl + LClear the screen
Ctrl + SPause terminal output in terminals that use flow control
Ctrl + QResume output paused by Ctrl + S

Cursor Movement and Editing

ShortcutDescription
Ctrl + AMove to the beginning of the line
Ctrl + EMove to the end of the line
Alt + B / Alt + FMove backward or forward by one word
Ctrl + TSwap the current character with the previous character
Alt + TSwap the current word with the previous word
Ctrl + WCut the word before the cursor
Ctrl + UCut from the cursor to the beginning of the line
Ctrl + KCut from the cursor to the end of the line
Ctrl + YPaste the most recently cut text
Ctrl + _Undo the last edit

On many macOS terminal configurations, Esc, then B or F, can be used when Option + B or Option + F is not configured as Alt.

Command History

ShortcutDescription
Up / DownMove through previous and next commands
Ctrl + RSearch command history backward
Ctrl + GCancel the history search

Paths and Special Symbols

The filesystem is a hierarchy that begins at the root directory /.

SymbolMeaningExample
/Root directorycd /
.Current directoryls .
..Parent directorycd ..
~Current user's home directorycd ~
-Previous working directory when used with cdcd -

An absolute path begins at /. A relative path begins at the current directory.

/Users/username/Documents/report.txt   # Absolute path on macOS
Documents/report.txt                   # Relative path from the home directory
../report.txt                           # A file in the parent directory

Quote or escape paths that contain spaces:

cd "My Projects"
cd My\ Projects

pwd: Show the Current Directory

pwd
# /Users/username/Documents

cd: Change Directory

cd ~/Documents       # Go to Documents in the home directory
cd ..                # Go up one level
cd ../..             # Go up two levels
cd -                 # Return to the previous directory
cd                   # Go to the home directory

ls: List Directory Contents

ls                   # List the current directory
ls -l                # Long format: permissions, owner, size, and time
ls -a                # Include hidden names that begin with .
ls -lh               # Use human-readable file sizes
ls -ltr              # Sort by modification time, oldest first
ls -AhlS             # Combine options: almost all, long, readable, by size

Most short options can be combined: ls -A -h -l -S and ls -AhlS are equivalent.

tree: Display a Directory Hierarchy

The tree command may need to be installed first:

brew install tree                 # macOS with Homebrew
sudo apt install tree             # Debian or Ubuntu
tree                              # Show the full hierarchy
tree -L 2                         # Limit output to two levels
tree -d                           # Show directories only
tree -a                           # Include hidden entries
tree -I 'node_modules|*.pyc'      # Ignore matching names
tree --dirsfirst                  # Put directories before files
tree -h                           # Use human-readable file sizes

Example output:

project/
├── src/
│   ├── main.py
│   └── utils.py
├── tests/
│   └── test_main.py
└── README.md

Creating Files and Directories

mkdir: Create Directories

mkdir project                     # Create one directory
mkdir dir1 dir2                   # Create several directories
mkdir -p project/src/components   # Create missing parent directories

touch: Create a File or Update Its Timestamp

touch file.txt                    # Create an empty file if it does not exist
touch existing.txt                # Update its access and modification times
touch file{1..3}.txt              # Create file1.txt, file2.txt, and file3.txt

rmdir: Remove Empty Directories

rmdir empty-directory
rmdir -p dir/subdir/leaf          # Also remove empty parent directories

rmdir fails if a target directory contains anything. Use this behavior when you want to avoid removing files accidentally.

Viewing and Measuring Files

cat, less, head, and tail

cat file.txt                      # Print the entire file
cat file1.txt file2.txt           # Print several files in order
less largefile.txt                # View a file page by page; press q to quit
head file.txt                     # Print the first 10 lines
head -n 5 file.txt                # Print the first 5 lines
tail file.txt                     # Print the last 10 lines
tail -f application.log           # Follow new lines as they are written

file: Identify a File Type

file document.pdf
# document.pdf: PDF document, version 1.4

wc: Count Lines, Words, and Bytes

wc file.txt                       # Lines, words, and bytes
wc -l file.txt                    # Lines only
wc -w file.txt                    # Words only
wc -c file.txt                    # Bytes only

du: Measure Disk Usage

du -h file.txt                    # Human-readable file size
du -sh directory/                 # Total size of a directory
du -h -d 1 .                      # Size of each item one level below, on macOS
du -h --max-depth=1 .             # GNU/Linux equivalent

Copying, Moving, and Removing

cp: Copy Files and Directories

cp source.txt destination.txt     # Copy a file
cp -i source.txt destination.txt  # Ask before overwriting
cp -v source.txt destination.txt  # Print the copied names
cp file1 file2 target/            # Copy several files into a directory
cp -R source-dir destination-dir  # Copy a directory recursively

The meaning of the destination depends on whether it already exists:

  • cp source.txt destination.txt replaces destination.txt if it exists. Add -i to request confirmation.
  • If destination-dir does not exist, cp -R source-dir destination-dir creates it as a copy of source-dir.
  • If destination-dir exists, the result is usually destination-dir/source-dir/....

mv: Move or Rename

mv old.txt new.txt                # Rename a file
mv -i old.txt new.txt             # Ask before overwriting
mv file.txt directory/            # Move a file into a directory
mv dir1 dir2                      # Rename dir1, or move it into dir2 if dir2 exists

rm: Remove Files and Nonempty Directories

rm file.txt                       # Remove one file
rm -i file.txt                    # Ask before removing
rm -v file.txt                    # Print removed names
rm -r directory/                  # Remove a directory recursively
rm -ri directory/                 # Recursively remove with confirmation
rm -rf directory/                 # Force recursive removal without confirmation
Warning

rm does not provide a built-in undo operation. Treat rm -rf as a last resort. Never paste it without first checking the exact expanded targets, for example with echo directory/* or find directory -maxdepth 1.

Creating Text and Redirecting Output

echo and printf

echo "Hello, world"               # Print a line
echo "$PATH"                      # Print an environment variable
printf '%s\n' "Hello, world"      # Print using an explicit format

printf is preferable when formatting matters because its behavior is more predictable across shells.

Standard Streams

Every command can work with three standard streams:

StreamNumberDefault destination
Standard input (stdin)0Keyboard
Standard output (stdout)1Terminal
Standard error (stderr)2Terminal

Redirection Operators

command > file                    # Write stdout to file, replacing it
command >> file                   # Append stdout to file
command < file                    # Read stdin from file
command 2> errors.log             # Write stderr to file
command > output.log 2>&1         # Write stdout and stderr to one file
command &> output.log             # Bash and Zsh shorthand for the line above
command 2>/dev/null               # Discard stderr
echo "first line" > notes.txt
echo "second line" >> notes.txt
cat notes.txt
# first line
# second line

Pipes

A pipe, |, sends the standard output of the command on its left to the standard input of the command on its right.

history | tail -20                # Last 20 history entries
grep "ERROR" application.log | wc -l
ps aux | grep '[p]ython'
du -h ./* | sort -h | tail -10

Each stage should do one job: select data, transform it, sort it, count it, or save it.

tee: Display and Save Output

tee copies its input both to the terminal and to a file, which is useful when you want to continue a pipeline while keeping a record.

ls / | tee root-contents.txt | head
grep "ERROR" application.log | tee errors.txt | wc -l
command | tee -a history.log      # Append instead of replacing

Text Processing

grep: Search Lines

grep uses regular expressions to select matching lines.

grep 'word' file.txt              # Lines containing word
grep -i 'word' file.txt           # Ignore letter case
grep -n 'word' file.txt           # Include line numbers
grep -w 'word' file.txt           # Match a whole word
grep -v 'word' file.txt           # Select nonmatching lines
grep -r 'word' directory/         # Search recursively
grep -l 'word' *.txt              # Print matching filenames only
grep -c 'word' file.txt           # Count matching lines
grep -o '[0-9]\+' file.txt        # Print only matching parts with basic regex
grep -E '[0-9]+' file.txt         # Use extended regular expressions

Common options:

OptionMeaning
-iIgnore case
-wMatch whole words
-vSelect nonmatching lines
-nInclude line numbers
-H / -hShow or suppress filename prefixes
-rSearch directories recursively
-RSearch recursively and follow symbolic links
-l / -LList files with or without matches
-cCount matching lines
-m NUMBERStop after NUMBER matches per file
-oPrint only the matched text

sed: Transform Text

sed 's/old/new/' file.txt          # Replace the first match on each line
sed 's/old/new/g' file.txt         # Replace every match
sed '2d' file.txt                  # Delete the second line in the output
sed -i '' 's/old/new/g' file.txt   # Edit in place on macOS
sed -i 's/old/new/g' file.txt      # Edit in place with GNU sed on Linux

Without -i, sed prints the transformed text and leaves the original file unchanged.

awk: Process Records and Columns

awk '{print $1}' file.txt          # Print the first whitespace-separated field
awk '{print $NF}' file.txt         # Print the last field
awk '/pattern/ {print $0}' file.txt
awk -F, '{print $2}' data.csv      # Use a comma as the field separator

sort, uniq, cut, and tr

sort names.txt                     # Sort lines
sort names.txt | uniq              # Remove adjacent duplicates
sort names.txt | uniq -c           # Count each distinct line
cut -d, -f2 data.csv               # Print the second comma-separated field
tr '[:lower:]' '[:upper:]' < file.txt

Finding Files and Commands

find: Search the Filesystem

The basic form is find PATH CONDITIONS ACTIONS.

find . -name '*.txt'               # Names ending in .txt
find . -iname '*.TXT'              # Case-insensitive name match
find . -type f                     # Regular files only
find . -type d                     # Directories only
find . -empty                      # Empty files or directories

find . -mtime -7                   # Modified during the last 7 days
find . -mmin -60                   # Modified during the last 60 minutes
find . -atime +30                  # Accessed more than 30 days ago

find . -size +10M                  # Larger than 10 MiB
find . -size +30k -size -1M        # Between 30 KiB and 1 MiB

find . -name '*.log' -mtime +30    # Combine conditions with implicit AND
find . \( -name '*.py' -o -name '*.sh' \) -type f
find . -not -path './node_modules/*' -type f

Quote patterns such as '*.txt'. Otherwise, the shell may expand them before find receives them.

Run a Command on Search Results

find . -name '*.txt' -exec grep -n 'word' {} +
find . -name '*.tmp' -exec rm -i {} +
find . -name '*.tmp' -delete

{} is replaced with the found paths. The final + groups as many paths as possible into each command invocation; \; runs the command once per path.

Warning

Place -delete only after confirming the same find expression without it. find descends into subdirectories unless you restrict it with options such as -maxdepth.

find with xargs

Use a null character as the separator so that filenames containing spaces, quotes, or newlines remain intact.

find . -type f -name '*.txt' -print0 | xargs -0 grep -n 'word'
find . -type f -name '*.tmp' -print0 | xargs -0 rm -i

When possible, find ... -exec command {} + is simpler and provides the same safe filename handling.

Find an Executable

command -v python                  # Portable: show how a command resolves
type python                        # Explain whether it is an alias, function, or file
which python                       # Common, but less informative than type
whereis python                     # Binary, source, and manual locations on many Linux systems
locate filename                    # Fast database search when locate is installed

Shell Globbing and Brace Expansion

Globs Match Filenames

The shell expands a glob into matching filenames before it runs the command.

PatternMeaning
*Zero or more characters
?Exactly one character
[abc]One character from the set
[a-z]One character from the range
[!abc]One character not in the set
ls *.txt                           # Every .txt file
ls file?.txt                       # file1.txt, fileA.txt, and similar names
ls [abc]*.txt                      # Names beginning with a, b, or c
ls [0-9][0-9].txt                  # Names with exactly two digits
mv *.{py,sh} scripts/              # Python and shell files

Preview a glob without performing the intended operation:

echo rm temp*
printf '%s\n' temp*
Note

Globs and regular expressions are different languages. The shell expands *.txt as a glob, while grep -E '^[0-9]+$' interprets its quoted pattern as a regular expression.

Extended Globs in Bash

Enable extended pattern matching in Bash:

shopt -s extglob
PatternMeaning
*(pattern)Zero or more occurrences
+(pattern)One or more occurrences
?(pattern)Zero or one occurrence
@(pattern)Exactly one occurrence
!(pattern)Anything except the pattern
ls
# file1 file2 file3 file11 file123

ls !(file1)
# file2 file3 file11 file123

ls !(file1|file2)
# file3 file11 file123

ls file+([0-9])
# file1 file2 file3 file11 file123

These examples are Bash-specific. Zsh provides its own extended glob syntax and options.

Brace Expansion Generates Strings

Brace expansion does not inspect the filesystem; it generates strings.

echo {1,2,3}                       # 1 2 3
echo file{1..5}.txt                # file1.txt through file5.txt
echo {a..z}                        # a through z
echo {A,B}{1,2}                    # A1 A2 B1 B2

mkdir -p project/{src,tests,docs}
touch file{1..10}.txt
cp file.txt{,.backup}              # Expands to: cp file.txt file.txt.backup
mv /path/{foo,bar,baz}.txt dir/

Combining Commands and Expansions

Command Lists

command1 ; command2                # Run command2 regardless of command1
command1 && command2               # Run command2 only if command1 succeeds
command1 || command2               # Run command2 only if command1 fails
command1 & command2                # Start command1 in the background, then command2

Practical examples:

mkdir project && cd project
make && make test
cd /tmp || echo "Could not enter /tmp"
long_running_command & echo "Started in the background"

Command Substitution: $(...)

Command substitution runs a command and inserts its standard output into another command.

echo "Today is $(date +%F)"
touch "report_$(date +%Y%m%d).txt"
ls "$(dirname "$(command -v python)")"

Prefer $(...) to legacy backticks because it is easier to read and nest.

Parameter Expansion: ${...}

Parameter expansion reads or transforms the value of a shell variable. It does not run the contents as a command.

animal=cat
echo "${animal}s"                  # cats
echo "${animal}_food"              # cat_food
echo "${HOME}/Documents"

The braces separate the variable name from adjacent text. This is the essential difference:

$(date)                            # Output of the date command
${HOME}                            # Value of the HOME variable

xargs: Build Commands from Input

printf '%s\n' file1 file2 file3 | xargs echo
printf '%s\n' file1 file2 file3 | xargs -n 1 echo "Processing:"
printf '%s\0' *.txt | xargs -0 -I {} cp {} backup/

Use -0 with null-delimited input whenever filenames are involved.

Environment, Aliases, and History

Environment Variables

echo "$HOME"                       # Home directory
echo "$PATH"                       # Directories searched for executables
echo "$USER"                       # Current username

export PROJECT_ROOT="$HOME/project" # Set and export a variable

Changes made at the prompt normally last only for the current shell session. Add persistent settings to ~/.zshrc for Zsh or ~/.bashrc for Bash, then start a new shell or reload the file.

source ~/.zshrc                    # Reload Zsh configuration
source ~/.bashrc                   # Reload Bash configuration

Aliases and Functions

alias ll='ls -lah'
alias ..='cd ..'
unalias ll

Aliases are suited to simple command substitutions. Use a function when you need arguments or multiple commands:

mkcd() {
    mkdir -p "$1" && cd "$1"
}

mkcd new-project

Command History

history                            # Show history
history 20                         # Show recent entries in many shells
!!                                 # Repeat the previous command
!123                               # Run history entry 123
!$                                 # Last argument of the previous command

History expansion depends on the shell and its configuration. Use Ctrl + R when you want to find and inspect a command before running it.

Permissions and Ownership

Permissions control who may read, modify, or execute files and who may list, modify, or enter directories.

Inspect Permissions

ls -l script.sh
# -rwxr-xr--  1 user  group  1234 Aug  4 15:18 script.sh

The first character is the file type: - for a regular file, d for a directory, and l for a symbolic link. The next nine characters are three permission groups:

rwx r-x r--
│   │   └── others
│   └────── group
└────────── owner (user)

For a regular file:

  • r allows reading the contents.
  • w allows modifying the contents.
  • x allows executing the file as a program.

For a directory:

  • r allows reading its list of names.
  • w allows creating, renaming, or removing entries, subject to other permissions.
  • x allows entering the directory and accessing entries whose names are known.

Symbolic Permissions with chmod

SymbolMeaning
uUser or owner
gGroup
oOthers
aEveryone
OperatorMeaning
=Set exact permissions
+Add permissions
-Remove permissions
chmod u+x script.sh               # Add execute for the owner
chmod g-w file.txt                # Remove group write access
chmod o-r secret.txt              # Remove read access for others
chmod a+r file.txt                # Add read access for everyone
chmod u=rwx,g=rx,o=r file.txt     # Set each class explicitly

Numeric Permissions with chmod

Each digit is the sum of 4 for read, 2 for write, and 1 for execute. The three digits apply to owner, group, and others.

ModeSymbolic formTypical use
755rwxr-xr-xPublicly executable script or traversable directory
700rwx------Private executable or directory
644rw-r--r--Common document or configuration file
600rw-------Private file
777rwxrwxrwxFull access for everyone; rarely appropriate
chmod 755 script.sh
chmod 644 document.txt
chmod 600 private-key
chmod -R u+rwX,go-rwx private-directory/
Note

New files often begin from mode 666 and new directories from 777, but the shell's umask removes permissions. Run umask to inspect the current mask. The execute bit is omitted from the base mode for regular files so that ordinary new files do not become executable automatically.

Warning

Apply recursive permission changes carefully. chmod -R 755 directory/ also marks every regular file executable and readable by everyone. Symbolic mode u+rwX uses uppercase X to add execute only to directories and to files that are already executable.

Change Owner and Group

chown user file.txt                 # Change owner
chown user:group file.txt           # Change owner and group
chown -R user:group directory/      # Apply recursively

Changing ownership commonly requires administrator privileges, for example sudo chown ....

A symbolic link is a small filesystem entry that stores a path to another file or directory.

ln -s /path/to/original /path/to/link
ln -s ~/Documents/project ~/Desktop/project-link
ls -l ~/Desktop/project-link
# project-link -> /Users/username/Documents/project

Useful options:

ln -sf original link              # Replace an existing destination
ln -s ~/Documents/notes.txt .     # Create the link in the current directory
  • A symbolic link may point to a file or directory.
  • If its target is removed or moved, the link may break.
  • Relative links can be more portable within a directory tree; absolute links are independent of the current working directory.
Note

A macOS Finder alias is not the same as a symbolic link. Command-line tools understand symbolic links directly, while Finder aliases use macOS-specific metadata.

ln original.txt hardlink.txt

Hard-linked filenames refer to the same underlying file data. Removing one name does not remove the data while another hard link remains. Hard links normally cannot cross filesystems or refer to directories.

Process and Job Management

Inspect Processes

ps                                 # Processes associated with this terminal
ps aux                             # All processes in BSD-style output
ps aux | grep '[p]ython'           # Find a process without matching grep itself
pgrep -fl python                   # Find process IDs and command lines
top                                # Interactive system process viewer
htop                               # Alternative viewer, if installed

Foreground and Background Jobs

long_running_command &             # Start in the background
jobs                               # List jobs belonging to this shell
fg %1                              # Bring job 1 to the foreground
bg %1                              # Resume suspended job 1 in the background

Press Ctrl + Z to suspend a foreground process, then use bg to continue it in the background or fg to bring it back.

Stop Processes

kill PID                           # Request a graceful termination
kill -TERM PID                     # Same signal, written explicitly
killall process_name               # Signal processes by name
kill -9 PID                        # Force termination with SIGKILL

Use kill -9 only when normal termination does not work. It prevents the process from cleaning up files or other resources.

Writing Bash Scripts

A shell script stores a sequence of commands in a text file so that it can be repeated reliably.

A Minimal Script

Create hello.sh:

#!/usr/bin/env bash

set -euo pipefail

name=${1:-world}
printf 'Hello, %s!\n' "$name"

Run it with Bash, or make it executable and run it directly:

bash hello.sh Kensei

chmod u+x hello.sh
./hello.sh Kensei

The first line is the shebang and selects the interpreter. The options in set -euo pipefail are a common defensive starting point for Bash scripts:

  • -e exits when an unhandled command fails.
  • -u reports an unset variable as an error.
  • -o pipefail makes a pipeline fail when any stage fails.

These options affect control flow, so understand their behavior before adding them to a large existing script.

Script Arguments and Special Parameters

ParameterMeaning
$0Script or function name
$1 through $9Individual positional arguments
${10} and abovePositional arguments that require braces
"$@"All arguments, preserved as separate strings
"$*"All arguments combined into one string
$#Number of positional arguments
$$Process ID of the current shell
$?Exit status of the previous command
$!Process ID of the most recent background command

Use "$@" when forwarding arguments:

backup() {
    cp -v -- "$@" backup/
}

backup "first file.txt" second.txt

Quoting "$@" preserves "first file.txt" as one argument.

Conditional Statements

Bash and Zsh support the versatile [[ ... ]] conditional syntax:

file=$1

if [[ -f $file ]]; then
    echo "$file is a regular file"
elif [[ -d $file ]]; then
    echo "$file is a directory"
else
    echo "$file does not exist"
fi

Useful tests include:

TestMeaning
-e pathPath exists
-f pathRegular file exists
-d pathDirectory exists
-r pathPath is readable
-w pathPath is writable
-x pathPath is executable or traversable
-z stringString is empty
string1 == string2Strings are equal inside [[ ... ]]

The older [ ... ] form is specified by POSIX and is more portable to /bin/sh. It has stricter quoting and operator rules. Use [[ ... ]] in scripts that explicitly select Bash or Zsh, and [ ... ] when writing portable sh scripts.

Functions

find_large_files() {
    local pattern=${1:-'*'}
    local size_mb=${2:-10}
    find . -type f -name "$pattern" -size "+${size_mb}M" -exec ls -lh {} +
}

find_large_files '*.pdf' 20

Functions share the current shell environment, so they can change directories and variables in the calling shell. Quote every expansion that should remain one argument.

Help and Good Habits

Learn What a Command Does

man command                        # Full manual page
command --help                     # Common GNU-style quick help
help command                       # Help for a Bash builtin
type command                       # Alias, function, builtin, or executable
command -v command                 # Show how the command resolves

Option syntax and behavior can differ between macOS/BSD tools and GNU/Linux tools. Check the manual on the system where the command will run.

Work Safely

pwd                                # Confirm your location
ls -la target/                     # Inspect the target
echo rm target/*.tmp               # Preview a wildcard expansion
cp important.txt{,.backup}         # Make a quick backup
rm -i file.txt                     # Request confirmation

Aliases such as alias rm='rm -i' can add a useful interactive guard, but scripts and other environments may not load the alias. Do not rely on it as the only safety measure.

Work Efficiently

cd -                               # Return to the previous directory
pushd /path/to/project             # Save the current directory and move
popd                               # Return to the saved directory
nano file.txt                      # Beginner-friendly terminal editor
vim file.txt                       # Modal terminal editor
open -e file.txt                   # Open in TextEdit on macOS

Practice Exercises

Create and Inspect a Project

mkdir -p myproject/{src,tests,docs}
touch myproject/src/{main.py,utils.py}
touch myproject/README.md
tree myproject

Find and Count Files

find myproject -type f -name '*.py' | wc -l

Search and Transform Text

printf '%s\n' INFO ERROR INFO ERROR > application.log
grep -n 'ERROR' application.log | tee errors.log | wc -l

Write a Small Script

Create a script that accepts a directory as $1, checks that it exists with [[ -d ... ]], and prints its five largest entries using du, sort, and tail.

Quick Reference

TaskCommands and operators
Navigatepwd, cd, ls, tree
Createmkdir, touch
Viewcat, less, head, tail, file
Copy, move, removecp, mv, rm, rmdir
Measurewc, du
Searchgrep, find, command -v, type
Transform textsed, awk, sort, uniq, cut, tr
Redirect and pipe>, >>, <, 2>, `
Combine commands;, &, &&, `
Permissionschmod, chown, umask
Linksln, ln -s
Processes and jobsps, top, pgrep, kill, jobs, fg, bg
Helpman, --help, help, type

Related posts