#!/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, ## but I switched to ripgrep because its faster than GNU grep and it supports ## multiline replacements better than GNU sed. set -euo pipefail shopt -s inherit_errexit die() { echo -e "$(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 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 ---------------------------------------------------------------------- echo -e "\033[1m$file_path BEFORE\033[0m" echo "$matching_lines" echo ---------------------------------------------------------------------- echo -e "\033[1m$file_path AFTER\033[0m" rg --color=never -Ur "$replacement" "$pattern" <<< "$matching_lines" done echo ---------------------------------------------------------------------- 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