#!/usr/bin/env bash

## Search recursively for files with text matching a pattern and replace
## matches. Multiline replacements are supported.
##
## Usage: replace [-y] <pattern> <replacement> [<path>...]
##
## 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 "${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
(( $# > 2 )) && paths=("${@:3}") || paths=(.)
rg_opts=(-U --multiline-dotall)

# Ensure that there is at least one match.
rg "${rg_opts[@]}" -q "$pattern" "${paths[@]}" || die 'match not found'

# Find all matches, show preview of replacements and request confirmation.
if [[ ! -v assume_yes ]]; then
  rg "${rg_opts[@]}" -l0 "$pattern" "${paths[@]}" | while IFS= read -rd '' file; do
    matching_lines=$(rg "${rg_opts[@]}" -N "$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 "${rg_opts[@]}" -r "$replacement" "$pattern" <<< "$matching_lines"
    printf %"$COLUMNS"s | sed 's/ /─/g' # Horizontal line.
  done
  read -rp 'Replace text in files? '
  [[ $REPLY =~ ^[Yy] ]] || exit
fi

# Replace matches.
rg "${rg_opts[@]}" -l0 "$pattern" "${paths[@]}" | while IFS= read -rd '' file; do
  new_text=$(rg "${rg_opts[@]}" --passthru -Nr "$replacement" "$pattern" "$file")
  echo "$new_text" > "$file"
done
