advanced-patterns.md 5.4 KB

sd Advanced Patterns

Advanced find-and-replace patterns using sd (simpler than sed).

Regex Patterns

Capture Groups

# Reorder parts
sd '(\w+)@(\w+)\.com' '$2/$1' emails.txt
# john@example.com → example/john

# Wrap in function
sd 'console\.log\((.*)\)' 'logger.info($1)' src/**/*.js
# console.log(msg) → logger.info(msg)

# Extract and transform
sd 'class (\w+)' 'export class $1' src/**/*.ts

Word Boundaries

# Match whole words only
sd '\bfoo\b' 'bar' file.txt
# "foo" matches, "foobar" doesn't

# Function rename without affecting variables
sd '\bgetUser\b' 'fetchUser' src/**/*.ts

Optional and Alternation

# Optional whitespace
sd 'function\s*\(' 'const fn = (' src/**/*.js

# Match alternatives
sd '(let|var)\s+' 'const ' src/**/*.js

Multiline Patterns

# Match across lines (-s flag)
sd -s 'start\n.*\nend' 'replacement' file.txt

# Remove multiline blocks
sd -s '\/\*\*[\s\S]*?\*\/' '' src/**/*.ts

Real-World Patterns

Import Transformations

# CommonJS to ES modules
sd "const (\w+) = require\('([^']+)'\)" "import $1 from '$2'" src/**/*.js

# Relative to absolute imports
sd "from '\.\./\.\./utils'" "from '@/utils'" src/**/*.ts

# Named imports reorganization
sd "import \{ (\w+), (\w+) \}" "import { $2, $1 }" src/**/*.ts

API Endpoint Updates

# Version bump
sd '/api/v1/' '/api/v2/' src/**/*.ts

# Domain migration
sd 'api\.old\.com' 'api.new.com' src/**/*.ts

# Path restructuring
sd '/users/(\d+)/posts' '/posts?user_id=$1' src/**/*.ts

Configuration Updates

# Environment variable rename
sd 'DATABASE_URL' 'DB_CONNECTION_STRING' .env* src/**/*.ts

# Port number change
sd 'port:\s*3000' 'port: 8080' **/*.yaml **/*.json

# Version strings
sd '"version":\s*"\d+\.\d+\.\d+"' '"version": "2.0.0"' package.json

Code Modernization

# Async/await from promises
sd '\.then\(\((\w+)\)\s*=>\s*\{' 'const $1 = await ' src/**/*.ts

# Template literals
sd '"\s*\+\s*(\w+)\s*\+\s*"' '${$1}' src/**/*.ts

# Optional chaining
sd '(\w+)\s*&&\s*\1\.(\w+)' '$1?.$2' src/**/*.ts

React/JSX Patterns

# className to class (or vice versa)
sd 'className="' 'class="' src/**/*.jsx
sd 'class="' 'className="' src/**/*.jsx

# Event handler rename
sd 'onClick=\{' 'onPress={' src/**/*.tsx

# Hook migration
sd 'componentDidMount\(\)' 'useEffect(() => {' src/**/*.tsx

Remove Code

# Console logs
sd 'console\.log\([^)]*\);?\s*\n?' '' src/**/*.ts

# Debug statements
sd '// DEBUG:.*\n' '' src/**/*.ts

# Commented code blocks
sd '//\s*[A-Za-z].*\n' '' src/**/*.ts  # Single line comments with code

# TODO comments
sd '// TODO:.*\n' '' src/**/*.ts

Batch Workflows

Safe Preview Pattern

# 1. List affected files
rg -l 'oldPattern' src/

# 2. Preview replacements (rg with -r)
rg 'oldPattern' -r 'newPattern' src/

# 3. Apply to found files
sd 'oldPattern' 'newPattern' $(rg -l 'oldPattern' src/)

# 4. Verify
rg 'oldPattern' src/  # Should return nothing

# 5. Review changes
git diff

With fd for File Selection

# TypeScript files only
fd -e ts -x sd 'old' 'new' {}

# Specific directories
fd -e js . src/ lib/ -x sd 'old' 'new' {}

# Exclude patterns
fd -e ts -E "*.test.ts" -E "*.spec.ts" -x sd 'old' 'new' {}

# By filename pattern
fd 'config' -e json -x sd '"dev"' '"prod"' {}

Multiple Replacements

# Chain with &&
sd 'pattern1' 'replacement1' file.txt && \
sd 'pattern2' 'replacement2' file.txt && \
sd 'pattern3' 'replacement3' file.txt

# Or use a script
#!/bin/bash
files=$(rg -l 'oldApi' src/)
for file in $files; do
    sd 'oldApi\.get' 'newApi.fetch' "$file"
    sd 'oldApi\.post' 'newApi.send' "$file"
    sd 'oldApi\.delete' 'newApi.remove' "$file"
done

Special Characters

Escaping Reference

Character Escape Example
. \. sd '1\.0' '2.0'
* \* sd '\*important\*' '**important**'
? \? sd 'what\?' 'what!'
[ ] \[ \] sd '\[x\]' '[✓]'
( ) \( \) sd 'func\(\)' 'func(arg)'
{ } \{ \} sd '\{0,1\}' '?'
$ \$ sd '\$100' '€100'
^ \^ sd '\^note' 'NOTE'
\ \\ sd '\\n' '\n'
\| \| sd 'a\|b' 'a or b'

Literal Mode

# When you have many special chars, consider preprocessing
# or use fixed-string replacement with rg first

# For checking matches
rg -F '[TODO]' src/

# sd doesn't have -F, so escape carefully
sd '\[TODO\]' '[DONE]' src/**/*.md

Platform-Specific

With Git

# Only changed files
sd 'old' 'new' $(git diff --name-only)

# Staged files only
sd 'old' 'new' $(git diff --cached --name-only)

# Files changed in last commit
sd 'old' 'new' $(git diff-tree --no-commit-id --name-only -r HEAD)

In Docker

# Inside container
docker exec -it container sd 'old' 'new' /app/config.json

# Or with volume mount
docker run -v $(pwd):/data alpine sh -c "apk add sd && sd 'old' 'new' /data/file.txt"

Tips and Best Practices

Tip Reason
Always preview with rg -r first Catch mistakes before applying
Use git before bulk changes Easy rollback with git checkout
Quote patterns Prevent shell interpretation
Use \b for word boundaries Avoid partial matches
Start specific, then broaden Easier to control scope
Test on single file first Verify pattern works
Use fd -x for file selection More precise than globs