⇄ The expand & unexpand Commands

Convert Between Tabs and Spaces

expand [OPTION]... [FILE]...
unexpand [OPTION]... [FILE]...

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
The Tab Character:

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

Example 1

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
Column1^IColumn2^IColumn3$ Data1^IData2^IData3$ Item1^IItem2^IItem3$
Tabs Visible: The ^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
Column1 Column2 Column3$ Data1 Data2 Data3$ Item1 Item2 Item3$
Explanation: The 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.
Pro Tip: By default, expand reads from standard input and writes to standard output, so you can use it in pipes: cat file | expand > output
Example 2

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
-rw-r--r-- 1 craig users 112 Jan 15 14:23 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
function example() {$ ^Iif (condition) {$ ^I^IdoSomething();$ ^I^IdoSomethingElse();$ ^I}$ }$
# Check file size - should be smaller ls -lh tabbed_code.txt
-rw-r--r-- 1 craig users 98 Jan 15 14:23 tabbed_code.txt
Explanation: The 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.
Default Behavior: By default, unexpand only converts leading whitespace (spaces at the beginning of lines). Use unexpand -a to convert ALL spaces to tabs.
Example 3

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
=== 8-column tabs (default) === Name Age City Status Alice 25 NYC Active Bob 30 LA Active Charlie 35 Chicago Inactive === 4-column tabs === Name Age City Status Alice 25 NYC Active Bob 30 LA Active Charlie 35 Chicago Inactive === 2-column tabs === Name Age City Status Alice 25 NYC Active Bob 30 LA Active Charlie 35 Chicago Inactive
Explanation: The -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.
Real-World Use: Different programming languages and style guides prefer different tab widths. Python PEP 8 recommends 4 spaces, some C code uses 2, Linux kernel style uses 8. The -t option lets you convert to match these standards.
Example 4

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
=== Default unexpand (leading only) === First Second Third Fourth$ One Two Three Four$ Alpha Beta Gamma Delta$ === unexpand -a (all spaces) === First^ISecond^IThird^IFourth$ One^ITwo^IThree^IFour$ Alpha^IBeta^IGamma^IDelta$
Explanation: Without -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).
Be Careful: Using 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!
Example 5

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
ID Name Department Salary 1 Alice Engineering 95000 2 Bob Sales 75000 3 Charlie Marketing 68000
Explanation: Instead of regular intervals, -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.
Real-World Use: Report generation where specific column alignments are required, creating fixed-width data files for import into databases, or formatting output for display on terminals with specific requirements.
Example 6

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')"
=== Original (with tabs) === def calculate_total(items):$ ^Itotal = 0$ ^Ifor item in items:$ ^I^Iif item['active']:$ ^I^I^Itotal += item['price']$ === Converted (with spaces) === def calculate_total(items):$ total = 0$ for item in items:$ if item['active']:$ total += item['price']$ ✓ Syntax valid
Explanation: PEP 8 (Python Enhancement Proposal 8), the official Python style guide, strongly recommends using 4 spaces per indentation level, never tabs. The expand -t 4 command converts all tabs to 4 spaces, making the code PEP 8 compliant. The code remains functionally identical and syntactically valid.
Automation: Add this to your pre-commit hook or CI/CD pipeline:
find . -name "*.py" -exec expand -t 4 -i {} \; | sponge {}
Example 7

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
Makefile_spaces:5: *** missing separator. Stop.
The Problem: Make requires recipe lines (the commands under targets) to start with a TAB character, not spaces. The error "missing separator" means make expected a tab but found spaces.
# 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
=== Fixed Makefile (with tabs) === program: main.o utils.o$ ^Igcc -o program main.o utils.o$ rm -f *.o program
Explanation: The 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.
Critical for Make: This is not optional - Makefiles MUST use tabs for recipe lines. Many editors default to inserting spaces when you press Tab, which breaks Makefiles. Always use unexpand to fix them or configure your editor to insert real tabs in Makefiles.
Example 8

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"
Converting files in: ./src Tab width: 4 spaces Converting: ./src/main.c Converting: ./src/utils.c Skipping (no tabs): ./src/config.c Converting: ./src/parser.py Skipping (no tabs): ./src/helpers.py Converted 3 file(s) Backups saved with extension: .bak
Explanation: This script demonstrates a safe, production-ready approach to batch converting files:
  • 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
Real-World Use: When adopting a new coding standard across a project, preparing code for version control, or cleaning up code from multiple contributors who used different editor settings.
Example 9

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}'
Found: Alice aged 25
# 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
Name Department Salary Alice Engineering 95000 Bob Sales 75000 Charlie Marketing 68000
# Example 4: Clean up whitespace in code cat messy_code.c | \ expand -t 4 | \ sed 's/[[:space:]]*$//' | \ unexpand -t 4 > clean_code.c
Explanation: These examples show how expand and unexpand integrate seamlessly into pipelines:
  • 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
Pipeline Pattern: Common pattern is: expand → process → unexpand when you need consistent spacing for processing but want tabs in final output.
Example 10

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
Whitespace Normalization ======================== Processing: messy_code.py ✓ Completed: messy_code.py Processing: config.txt ✓ Completed: config.txt Processing: script.sh ✓ Completed: script.sh ======================== Summary: Processed: 3 Failed: 0 Backups: Saved with .bak extension
Explanation: This production-quality script provides comprehensive whitespace normalization:
  • 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
Real-World Use:
  • 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

Detecting Tabs vs Spaces:
# 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
Common Pitfalls:
  • 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.
Useful Combinations:
# 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)
Editor Configuration:

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
Version Control Integration:
# 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
Why Spaces Are Generally Preferred:
  • 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