#!/usr/bin/env bash ## Search recursively for files with text matching a pattern and replace ## matches. Multiline replacements are supported. ## ## Usage: replace [] [...] ## ## Options: ## -y Skip preview and confirmation prompt ## ## Dependencies: GNU coreutils, GNU sed, ripgrep ## ## This script previously used GNU grep for searching and GNU sed for replacing. ## I switched to ripgrep because GNU grep is slower, GNU sed's multiline ## replacements are less ergonomic, and GNU sed's regular expressions require ## more escaping. set -euo pipefail shopt -s inherit_errexit die() { echo "$(basename "$0"): $1" >&2; exit 1; } [[ " $* " =~ ' --help ' ]] && sed -n "s/^## \?//p" "$0" && exit while getopts y opt; do case $opt in y) assume_yes=true ;; *) exit 1 ;; esac done shift "$(( OPTIND - 1 ))" (( $# >= 2 )) || die 'missing argument' pattern=$1 replacement=$2 (( $# >= 3 )) && paths=("${@:3}") || paths=(.) # Ensure that there is at least one match. rg -Uq "$pattern" "${paths[@]}" || die 'match not found' # Find all matches, show preview of replacements, and request confirmation. if [[ ! -v assume_yes ]]; then rg -lU0 "$pattern" "${paths[@]}" | while IFS= read -rd '' file; do matching_lines=$(rg -UN "$pattern" "$file") file_path=$(realpath "$file") echo -e "\033[1m$file_path BEFORE\033[0m" echo "$matching_lines" echo -e "\033[1m$file_path AFTER\033[0m" rg --color=never -Ur "$replacement" "$pattern" <<< "$matching_lines" printf %"$COLUMNS"s | sed 's/ /─/g' # Horizontal line. done read -rp 'Replace text in files? ' input [[ $input =~ ^[Yy] ]] || exit fi # Replace matches. rg -lU0 "$pattern" "${paths[@]}" | while IFS= read -rd '' file; do new_text=$(rg --passthru -UNr "$replacement" "$pattern" "$file") echo "$new_text" > "$file" done