Overview
The expand and unexpand commands are complementary utilities that convert between tab characters and spaces in text files. These tools are essential when dealing with files that mix tabs and spaces, when formatting code for specific standards, or when preparing files for systems that handle tabs differently.
expand - Convert Tabs to Spaces:
- Replaces tab characters with the appropriate number of spaces
- Default tab stop is every 8 columns
- Customizable tab stops for different formatting needs
- Preserves the visual alignment of text
- Useful for ensuring consistent display across editors
unexpand - Convert Spaces to Tabs:
- Replaces sequences of spaces with tab characters
- By default, only converts initial whitespace (leading spaces)
- Can convert all spaces with -a option
- Reduces file size (tabs are single bytes)
- Useful for standardizing indentation
Common Use Cases:
- Converting Python code to use spaces (PEP 8 standard)
- Converting Makefiles to use tabs (required by make)
- Preparing code for version control systems
- Ensuring consistent display in different editors
- Fixing alignment issues in configuration files
- Reducing file size by converting to tabs
A tab character (\t, ASCII 9, hex 0x09) is a single byte that tells the display system to advance to the next tab stop. The problem is that different systems and editors use different tab stop positions (every 4, 8, or even 2 columns). This causes the same file to look different in different editors. Spaces, on the other hand, are always one column wide, ensuring consistent appearance everywhere.
Common Options
expand Options:
| Option | Description |
|---|---|
| -t N | Set tab stops to N columns apart (default: 8) |
| -t N1,N2,N3 | Set specific tab stop positions |
| -i | Only convert initial tabs (at line start) |
| --first-only | Only convert leading tabs |
unexpand Options:
| Option | Description |
|---|---|
| -a, --all | Convert all whitespace, not just initial whitespace |
| -t N | Use tabs N columns apart (default: 8) |
| -t N1,N2,N3 | Set specific tab stop positions |
| --first-only | Only convert leading blanks (default behavior) |
Detailed Examples
Basic Tab to Space Conversion with expand
The most common use - converting all tabs in a file to spaces.
# Create a test file with tabs
cat > tabbed.txt <<'EOF'
Column1 Column2 Column3
Data1 Data2 Data3
Item1 Item2 Item3
EOF
# View the file with tabs visible
cat -A tabbed.txt
^I represents tab characters. Notice how they're single characters taking up variable amounts of space.
# Convert tabs to spaces (default 8-column tab stops)
expand tabbed.txt > spaced.txt
# View the converted file
cat -A spaced.txt
expand command replaced each tab character with the appropriate number of spaces to reach the next 8-column tab stop. The visual alignment is preserved, but now it's all spaces - no more tabs. The alignment looks the same but is now explicit rather than dependent on tab stop settings.
cat file | expand > output
Converting Spaces to Tabs with unexpand
The reverse operation - converting spaces back to tabs to save file size.
# Start with a file that has spaces for indentation
cat > spaced_code.txt <<'EOF'
function example() {
if (condition) {
doSomething();
doSomethingElse();
}
}
EOF
# Check file size
ls -lh spaced_code.txt
# Convert initial spaces to tabs
unexpand spaced_code.txt > tabbed_code.txt
# Check the converted file with tabs visible
cat -A tabbed_code.txt
# Check file size - should be smaller
ls -lh tabbed_code.txt
unexpand command (without options) only converts leading spaces - the indentation at the start of lines. Each set of spaces that aligns to a tab stop boundary is replaced with a tab character. File size decreased from 112 to 98 bytes because multiple space characters were replaced with single tab characters.
unexpand only converts leading whitespace (spaces at the beginning of lines). Use unexpand -a to convert ALL spaces to tabs.
Custom Tab Stops with -t Option
Using non-standard tab stops for specific formatting requirements.
# Create file with tabs
cat > data.txt <<'EOF'
Name Age City Status
Alice 25 NYC Active
Bob 30 LA Active
Charlie 35 Chicago Inactive
EOF
# Convert with default 8-column tabs
expand data.txt > expanded_8.txt
# Convert with 4-column tabs (common in many editors)
expand -t 4 data.txt > expanded_4.txt
# Convert with 2-column tabs (very tight spacing)
expand -t 2 data.txt > expanded_2.txt
# Compare the outputs
echo "=== 8-column tabs (default) ==="
cat expanded_8.txt
echo ""
echo "=== 4-column tabs ==="
cat expanded_4.txt
echo ""
echo "=== 2-column tabs ==="
cat expanded_2.txt
-t N option sets tab stops every N columns. With -t 4, tabs are replaced with spaces to reach the next multiple of 4. With -t 2, tabs reach the next multiple of 2. This shows how the same tab characters can represent different amounts of space depending on the tab stop setting.
-t option lets you convert to match these standards.
Converting All Whitespace with unexpand -a
Using the -a option to convert all spaces to tabs, not just leading ones.
# Create a file with spaces throughout
cat > spaced.txt <<'EOF'
First Second Third Fourth
One Two Three Four
Alpha Beta Gamma Delta
EOF
# Default unexpand (only leading spaces)
unexpand spaced.txt > unexpand_default.txt
# Convert ALL spaces with -a
unexpand -a spaced.txt > unexpand_all.txt
# Compare with cat -A
echo "=== Default unexpand (leading only) ==="
cat -A unexpand_default.txt
echo ""
echo "=== unexpand -a (all spaces) ==="
cat -A unexpand_all.txt
-a, unexpand only converts spaces at the beginning of lines. With -a, it converts ALL sequences of spaces that align to tab stops throughout the entire line. Notice in the second output how all the spaces between columns are now tabs (^I).
unexpand -a can have unexpected results if spaces aren't aligned to tab stop boundaries. Single spaces between words might not be converted, leading to inconsistent formatting. Test carefully!
Specific Tab Stop Positions
Setting exact column positions for tab stops instead of regular intervals.
# Create formatted output with specific columns
cat > report.txt <<'EOF'
ID Name Department Salary
1 Alice Engineering 95000
2 Bob Sales 75000
3 Charlie Marketing 68000
EOF
# Set specific tab stops at columns 10, 25, and 40
expand -t 10,25,40 report.txt > formatted.txt
# Display the result
cat formatted.txt
-t 10,25,40 sets tab stops at specific column positions: 10, 25, and 40. The first tab goes to column 10, second to 25, third to 40. This is useful for creating precisely formatted reports or tables where columns need specific widths.
Converting Python Code to Spaces (PEP 8 Compliance)
Making Python code compliant with PEP 8 style guide (spaces, not tabs).
# Create Python file with tabs
cat > script_tabs.py <<'EOF'
def calculate_total(items):
total = 0
for item in items:
if item['active']:
total += item['price']
return total
class DataProcessor:
def __init__(self, data):
self.data = data
def process(self):
results = []
for entry in self.data:
if self.validate(entry):
results.append(entry)
return results
EOF
# Check current status with cat -A
echo "=== Original (with tabs) ==="
cat -A script_tabs.py | head -5
# Convert to 4 spaces per tab (PEP 8 standard)
expand -t 4 script_tabs.py > script_spaces.py
# Verify conversion
echo ""
echo "=== Converted (with spaces) ==="
cat -A script_spaces.py | head -5
# Verify Python still works
python3 -c "from script_spaces import *; print('✓ Syntax valid')"
expand -t 4 command converts all tabs to 4 spaces, making the code PEP 8 compliant. The code remains functionally identical and syntactically valid.
find . -name "*.py" -exec expand -t 4 -i {} \; | sponge {}
Converting Makefile to Use Tabs (Required by make)
Makefiles MUST use tabs for recipe lines - spaces will cause errors.
# Create Makefile with spaces (WRONG!)
cat > Makefile_spaces <<'EOF'
# This Makefile has spaces instead of tabs - it won't work!
all: program
program: main.o utils.o
gcc -o program main.o utils.o
main.o: main.c
gcc -c main.c
utils.o: utils.c
gcc -c utils.c
clean:
rm -f *.o program
EOF
# Try to use it - will fail
make -f Makefile_spaces all 2>&1 | head -3
# Fix it by converting spaces to tabs
# Only convert leading spaces (recipe lines)
unexpand Makefile_spaces > Makefile_fixed
# Verify tabs are present
echo "=== Fixed Makefile (with tabs) ==="
cat -A Makefile_fixed | grep -A1 "program:"
# Now it works
make -f Makefile_fixed clean
unexpand command converted the leading spaces to tabs. Now the recipe lines start with ^I (tab), and make can parse the file correctly. This is one of the most common uses of unexpand - fixing Makefiles that were edited with space-inserting editors.
Batch Converting Multiple Files
Processing entire directories of files to standardize whitespace.
#!/bin/bash
# convert-to-spaces.sh - Convert all code files to spaces
TARGET_DIR="${1:-.}"
TAB_WIDTH="${2:-4}"
BACKUP_EXT=".bak"
echo "Converting files in: $TARGET_DIR"
echo "Tab width: $TAB_WIDTH spaces"
echo ""
# Counter
COUNT=0
# Find all relevant source files
find "$TARGET_DIR" -type f \( \
-name "*.py" -o \
-name "*.c" -o \
-name "*.h" -o \
-name "*.cpp" -o \
-name "*.java" -o \
-name "*.js" \
\) | while read file; do
# Check if file has tabs
if grep -q $'\t' "$file"; then
echo "Converting: $file"
# Create backup
cp "$file" "${file}${BACKUP_EXT}"
# Convert tabs to spaces
expand -t "$TAB_WIDTH" "$file" > "${file}.tmp"
mv "${file}.tmp" "$file"
((COUNT++))
else
echo "Skipping (no tabs): $file"
fi
done
echo ""
echo "Converted $COUNT file(s)"
echo "Backups saved with extension: $BACKUP_EXT"
- Finds all relevant source files recursively
- Checks each file for tabs before converting (efficiency)
- Creates backups before modifying
- Converts with specified tab width
- Reports progress and summary
Using expand/unexpand in Pipelines
Integrating expand and unexpand into command pipelines for text processing.
# Example 1: Process tab-separated data in pipeline
echo -e "Name\tAge\tCity\nAlice\t25\tNYC\nBob\t30\tLA" | \
expand -t 15 | \
grep "Alice" | \
awk '{print "Found:", $1, "aged", $2}'
# Example 2: Convert, process, and convert back
cat data_with_tabs.txt | \
expand -t 4 | \
sed 's/old/new/g' | \
unexpand -a | \
tee processed.txt
# Example 3: Pretty-print CSV as aligned columns
cat <<'EOF' | tr ',' '\t' | expand -t 20
Name,Department,Salary
Alice,Engineering,95000
Bob,Sales,75000
Charlie,Marketing,68000
EOF
# Example 4: Clean up whitespace in code
cat messy_code.c | \
expand -t 4 | \
sed 's/[[:space:]]*$//' | \
unexpand -t 4 > clean_code.c
- Example 1: Expand tabs for easier text processing with grep/awk
- Example 2: Convert to spaces, process, convert back to tabs
- Example 3: Convert CSV to nice aligned columns
- Example 4: Normalize whitespace in code files
expand → process → unexpand when you need consistent spacing for processing but want tabs in final output.
Complete Whitespace Normalization Script
A comprehensive script for cleaning and normalizing whitespace in source code.
#!/bin/bash
# normalize-whitespace.sh - Comprehensive whitespace cleanup
set -euo pipefail
# Configuration
readonly SCRIPT_NAME=$(basename "$0")
readonly TAB_WIDTH=4
# Color codes for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color
# Usage
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] file1 [file2 ...]
Normalize whitespace in source files:
- Convert tabs to spaces (default: $TAB_WIDTH spaces)
- Remove trailing whitespace
- Ensure single newline at end of file
- Remove multiple blank lines
OPTIONS:
-t N Tab width (default: $TAB_WIDTH)
-k Keep tabs (don't convert to spaces)
-b Create backup (.bak extension)
-v Verbose output
-h Show this help
EXAMPLES:
$SCRIPT_NAME -t 4 -b myfile.py
$SCRIPT_NAME -v *.c *.h
$SCRIPT_NAME -k Makefile
EOF
exit 1
}
# Parse options
TAB_SIZE=$TAB_WIDTH
KEEP_TABS=false
CREATE_BACKUP=false
VERBOSE=false
while getopts "t:kbvh" opt; do
case $opt in
t) TAB_SIZE="$OPTARG" ;;
k) KEEP_TABS=true ;;
b) CREATE_BACKUP=true ;;
v) VERBOSE=true ;;
h) usage ;;
*) usage ;;
esac
done
shift $((OPTIND-1))
if [ $# -eq 0 ]; then
echo "Error: No files specified"
usage
fi
# Logging functions
log_info() {
[ "$VERBOSE" = true ] && echo -e "${GREEN}[INFO]${NC} $*"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}
# Process a single file
process_file() {
local file="$1"
if [ ! -f "$file" ]; then
log_error "File not found: $file"
return 1
fi
if [ ! -w "$file" ]; then
log_error "File not writable: $file"
return 1
fi
echo "Processing: $file"
# Create backup if requested
if [ "$CREATE_BACKUP" = true ]; then
cp "$file" "${file}.bak"
log_info "Created backup: ${file}.bak"
fi
# Create temporary file
local tmpfile=$(mktemp)
trap "rm -f $tmpfile" RETURN
# Step 1: Convert tabs to spaces (unless -k specified)
if [ "$KEEP_TABS" = false ]; then
expand -t "$TAB_SIZE" "$file" > "$tmpfile"
log_info "Converted tabs to $TAB_SIZE spaces"
else
cat "$file" > "$tmpfile"
log_info "Keeping tabs (as requested)"
fi
# Step 2: Remove trailing whitespace from each line
sed -i 's/[[:space:]]*$//' "$tmpfile"
log_info "Removed trailing whitespace"
# Step 3: Remove multiple consecutive blank lines
sed -i '/^$/N;/^\n$/D' "$tmpfile"
log_info "Collapsed multiple blank lines"
# Step 4: Ensure single newline at end of file
sed -i -e '$a\' "$tmpfile"
log_info "Ensured final newline"
# Replace original file
mv "$tmpfile" "$file"
echo " ✓ Completed: $file"
return 0
}
# Process all files
echo "Whitespace Normalization"
echo "========================"
echo ""
PROCESSED=0
FAILED=0
for file in "$@"; do
if process_file "$file"; then
((PROCESSED++))
else
((FAILED++))
fi
echo ""
done
# Summary
echo "========================"
echo "Summary:"
echo " Processed: $PROCESSED"
echo " Failed: $FAILED"
if [ "$CREATE_BACKUP" = true ]; then
echo " Backups: Saved with .bak extension"
fi
[ $FAILED -eq 0 ] && exit 0 || exit 1
- Converts tabs to spaces with configurable width
- Removes trailing whitespace from lines
- Collapses multiple blank lines into one
- Ensures files end with a single newline
- Creates backups before modification
- Verbose logging for debugging
- Error handling and reporting
- Option to keep tabs for Makefiles
- Pre-commit hooks in version control
- Code review preparation
- Legacy code cleanup
- Enforcing coding standards across projects
- CI/CD pipeline validation
Comparison: expand vs unexpand
| Feature | expand | unexpand |
|---|---|---|
| Primary Function | Convert tabs to spaces | Convert spaces to tabs |
| Default Behavior | Converts all tabs in file | Converts only leading spaces |
| Tab Width | Default: 8 columns | Default: 8 columns |
| Common Use | Ensure consistent display, PEP 8 compliance | Reduce file size, Makefile compliance |
| Convert All Option | N/A (always converts all) | -a option converts all whitespace |
| File Size Impact | Usually increases size | Usually decreases size |
| Editor Independence | Output looks same everywhere | Output depends on editor tab settings |
Additional Information
# Check if file has tabs
if grep -q $'\t' filename; then
echo "File contains tabs"
fi
# Count tabs in file
echo "Tabs: $(grep -o $'\t' filename | wc -l)"
# Show lines with tabs
grep -n $'\t' filename
# Visualize tabs and spaces
cat -A filename | less
- Mixed tabs and spaces: Some files have both. Clean with expand then manually review.
- unexpand -a surprises: May not convert all spaces if they don't align to tab stops.
- Makefile conversion: Only use unexpand (spaces→tabs), never expand on Makefiles!
- Binary files: Never run expand/unexpand on binary files - corrupts them!
- In-place modification: These commands output to stdout - use redirection or temp files.
# Convert and clean trailing spaces
expand file.txt | sed 's/[[:space:]]*$//' > clean.txt
# Convert to spaces and format with pr
expand file.c | pr -t -n | less
# Convert CSV to aligned table
cat data.csv | tr ',' '\t' | expand -t 20
# Normalize indentation to 4 spaces
expand -t 4 code.py | sponge code.py
# Check if files would change
diff -u original.txt <(expand original.txt)
Most modern editors can be configured to handle tabs properly:
- Vim:
:set expandtab tabstop=4 shiftwidth=4 - Emacs:
(setq-default indent-tabs-mode nil tab-width 4) - VS Code: Settings → "Insert Spaces" + "Tab Size: 4"
- Sublime: Preferences → Settings → "translate_tabs_to_spaces": true
# Git pre-commit hook to convert tabs
#!/bin/bash
# .git/hooks/pre-commit
for file in $(git diff --cached --name-only | grep '\.py$'); do
expand -t 4 "$file" > "$file.tmp"
mv "$file.tmp" "$file"
git add "$file"
done
- Consistent display across all editors and viewers
- Web display (HTML collapses tabs differently)
- Terminal display consistency
- No ambiguity about indentation level
- Easier to work with in diff tools
- Many style guides mandate spaces (Python PEP 8, Google Style Guides)
Exception: Makefiles require tabs - this is non-negotiable!
Quick Reference
| Task | Command |
|---|---|
| Convert tabs to spaces (default 8) | expand file.txt |
| Convert tabs to 4 spaces | expand -t 4 file.txt |
| Convert leading spaces to tabs | unexpand file.txt |
| Convert all spaces to tabs | unexpand -a file.txt |
| Custom tab stops | expand -t 10,20,30 file.txt |
| In-place conversion | expand file.txt | sponge file.txt |
| Check for tabs | grep -q $'\t' file.txt |
| Show tabs visually | cat -A file.txt |
| Python PEP 8 conversion | expand -t 4 script.py > fixed.py |
| Fix Makefile | unexpand Makefile > Makefile.fixed |