Manipulate markdown header levels while preserving code blocks.
sudo curl -fsSL https://raw.githubusercontent.com/Open-Technology-Foundation/mdheaders/main/mdheaders -o /usr/local/bin/mdheaders && sudo chmod +x /usr/local/bin/mdheadersmdheaders up README.md # Upgrade: # → ##
mdheaders down README.md # Downgrade: ## → #
mdheaders norm -s 2 doc.md # Normalize to start at H2sudo curl -fsSL https://raw.githubusercontent.com/Open-Technology-Foundation/mdheaders/main/mdheaders -o /usr/local/bin/mdheaders && sudo chmod +x /usr/local/bin/mdheadersgit clone https://github.com/Open-Technology-Foundation/mdheaders.git
cd mdheaders
# Install script
sudo cp mdheaders /usr/local/bin/
# Install manpage (optional)
sudo cp mdheaders.1 /usr/share/man/man1/
# Install bash completion (optional)
sudo cp mdheaders.bash_completion /etc/bash_completion.d/mdheaders| Command | Alias | Action |
|---|---|---|
upgrade |
up |
Increase header levels (# → ##) |
downgrade |
down |
Decrease header levels (## → #) |
normalize |
norm |
Auto-adjust all headers to target level |
| Option | Description |
|---|---|
-l, --levels=N |
Number of levels to shift (default: 1) |
-s, --start-level=N |
Target starting level for normalize (default: 1) |
-o, --output=FILE |
Output file (default: stdout) |
-i, --in-place |
Modify file in-place |
-b, --backup[=SUFFIX] |
Create backup before in-place edit (default: .bak) |
--skip-errors |
Skip invalid headers with warning (default) |
--stop-on-error |
Abort on first invalid header |
-q, --quiet |
Suppress warnings and progress messages |
-v, --verbose |
Show detailed processing information (default) |
-h, --help |
Show help message |
-V, --version |
Show version |
# Upgrade all headers by 1 level
mdheaders upgrade README.md
# Downgrade all headers by 1 level
mdheaders downgrade README.md
# Upgrade by 2 levels
mdheaders upgrade -l 2 doc.md
# Normalize to start at H2
mdheaders normalize --start-level=2 doc.md# Modify file in-place
mdheaders up -i doc.md
# In-place with backup (.bak)
mdheaders down -i -b doc.md
# In-place with custom backup suffix
mdheaders up -i -b.orig README.md
# Bundled short options
mdheaders up -ib doc.md# Output to stdout (default)
mdheaders upgrade doc.md
# Save to new file
mdheaders upgrade -o NEW.md OLD.md
# Quiet mode (suppress warnings)
mdheaders down -q doc.md# Skip invalid headers with warning (default)
mdheaders up --skip-errors doc.md
# Abort on first invalid header
mdheaders up --stop-on-error doc.md- Upgrade headers: Increase header levels (e.g.,
#→##) - Downgrade headers: Decrease header levels (e.g.,
##→#) - Normalize headers: Auto-detect minimum level and normalize to target
- Code block awareness: Preserves fenced code blocks (``` and ~~~)
- Flexible output: stdout, file, or in-place modification
- Safety features: Validates H1-H6 boundaries, optional backups, numeric option validation; in-place edits preserve inode, mode, owner, symlinks and hardlinks
- Single-file: Self-contained script with no dependencies
The tool uses a state machine to track whether it's inside a code block:
- Track code fences: Detects ``` and ~~~ fences
- Match fence types: Ensures closing fence matches opening type
- Process headers: Only modifies headers outside code blocks
- Preserve content: Maintains exact formatting and whitespace
The normalize command uses a two-pass approach:
- First pass: Scan document to detect minimum header level
- Calculate delta: Determine shift needed (target - current_min)
- Second pass: Apply delta to all headers
Example:
- Document has: H1, H2, H3, H4
- Run:
mdheaders normalize --start-level=2 doc.md - Result: H2, H3, H4, H5 (all shifted up by 1)
- Downgrade: Cannot go below H1 (
#) - Upgrade: Cannot exceed H6 (
######) - Invalid headers: Skipped by default (use
--stop-on-errorto abort)
- Code blocks (fenced with ``` or ~~~)
- Code comments containing
# - Inline code with backticks
- Exact whitespace after
#symbols - All non-header content
- Bash 5.0+ (uses
${var@Q}quoting,local --) - Standard coreutils (
mktemp,cp,mv,cat)
# Run everything: ShellCheck lint gate + all suites (87 tests)
./tests/run_all.sh
# Individual suites
./tests/test_basic.sh # 12 - upgrade/downgrade
./tests/test_normalize.sh # 14 - normalize
./tests/test_errors.sh # 10 - error paths and edge cases
./tests/test_options.sh # 10 - options and command aliases
./tests/test_audit.sh # 41 - security/regression (injection, data loss, in-place safety)Test fixtures are in tests/fixtures/.
| Code | Meaning |
|---|---|
| 0 | Success (including a no-op when nothing needs shifting) |
| 1 | Processing error (file not found, no headers, write failure, abort) |
| 2 | Invalid arguments |
- Only handles fenced code blocks (``` and ~~~), not indented code blocks
- Only ATX headers (
#) are processed; setext underlines (===/---) are left unchanged - Doesn't process inline code spans for headers
- Assumes well-formed markdown (unclosed fences may produce unexpected results)
GPL-3. See LICENSE.
See CHANGELOG.md for release history.
Biksu Okusi Okusi Group