Skip to content

Instantly share code, notes, and snippets.

@bokwoon95
Last active August 30, 2022 19:13
Show Gist options
  • Save bokwoon95/6b92d03bb500fb20e217e0513ad5ac67 to your computer and use it in GitHub Desktop.
Save bokwoon95/6b92d03bb500fb20e217e0513ad5ac67 to your computer and use it in GitHub Desktop.
Search (and Replace) in multiple files using ag
#!/bin/sh
#
# Provides 2 functions
# 1) agf (ag-find) : searches for a string
# 2) ragf (replace-ag-find) : searches for a string and replaces it; displays results
#
# WHY IS THIS NOT A SIMPLE ONE LINER?
# It is a convenience function with shorthand for the inclusion and exclusion list.
# $ ragf "^foo|bar$" "baz" file1 file2 *.csv somedirectory/ :: somedirectory/.gitignore somedirectory/*.log data/
# ┗━━━━━━━ included files ━━━━━━━┛ ┗━━━━━━━━━━━━━━━━ excluded files ━━━━━━━━━━━━━━━━┛
#
# Or you can specify nothing at all (defaults to current directory)
# $ ragf "^foo|bar$" "baz"
#
# Specify only included files,
# $ ragf "^foo|bar$" "baz" file1 file2 *.csv somedirectory/
# ┗━━━━━━━ included files ━━━━━━━┛
#
# Or specify only excluded files,
# $ ragf "^foo|bar$" "baz" :: somedirectory/.gitignore somedirectory/*.log data/
# ┗━━━━━━━━━━━━━━━━ excluded files ━━━━━━━━━━━━━━━━┛
#
# Place these functions in your .bashrc or .zshrc or similar to use them
# Run `agf` and `ragf` (without arguments) for more details on how to use
# This file is overcommented, you should remove the comments when copying it over
#
agf () {
if [ "$#" -ge 1 ]; then
# enable $variable splitting (needed for this function to work. only applies to zsh)
local shwordsplit="$(set -o | grep shwordsplit | awk '{print $2}')"
[ "$shwordsplit" != "" -a "$shwordsplit" = "off" ] && setopt SH_WORD_SPLIT && shwordsplit="ENABLED"
## GET PATTERN ##
##-------------##
PATTERN="$1"; shift
## GET INCLUDED/EXCLUDED FILES ##
##-----------------------------##
if [ "$#" -eq 0 ]; then
# if user did not provide $INCLUDED & $EXCLUDED arguments, initialize $INCLUDED to $(pwd)
INCLUDED="$(pwd)"
EXCLUDED=""
else
# get everything before the '::'
INCLUDED="$(echo "$@" | sed -n "s/\(.*\)::\(.*\)/\1/p" | xargs)"
# get everything after the '::'
EXCLUDED="$(echo "$@" | sed -n "s/\(.*\)::\(.*\)/\2/p" | xargs)"
# if both $INCLUDED & EXCLUDED are empty, there was no '::' present in $@. Initialize $INCLUDED to $@
[ "$INCLUDED" = "" -a "$EXCLUDED" = "" ] && INCLUDED="$@"
# if $INCLUDED is empty or '::', initialize it to $(pwd)
[ "$INCLUDED" = "" -o "$INCLUDED" = "::" ] && INCLUDED="$(pwd)"
fi
## DISPLAY RESULTS ##
##-----------------##
# --path-to-ignore only reads the contents of .ignore file.
# We don't want to create an entire file just for this, so we spoof a file with <(printf ...) process substitution
ag --context=3 --pager="less -RiMSFX -#4" "$PATTERN" $INCLUDED --path-to-ignore <(printf "$(echo $EXCLUDED | tr -s ' ' '\n')")
# disable $variable splitting
[ "$shwordsplit" = "ENABLED" ] && unsetopt SH_WORD_SPLIT
else
echo " Usage: agf <pattern> [INCLUDED...] [:: EXCLUDED...]"
echo
echo " Search for <pattern> within <INCLUDED> files, ignoring <EXCLUDED> files."
echo " <INCLUDED> files are separated from <EXCLUDED> files by a '::'."
echo " When omitted, <INCLUDED> is the current dir and <EXCLUDED> is empty."
echo
echo " Examples:"
echo " agf pattern"
echo " agf pattern *.py"
echo " agf pattern file1.txt folder1/ :: folder1/file2.txt"
echo " agf pattern :: file1.txt **/*.log"
fi
}
ragf () {
if [ "$#" -ge 2 ]; then
# enable $variable splitting (needed for this function to work. only applies to zsh)
local shwordsplit="$(set -o | grep shwordsplit | awk '{print $2}')"
[ "$shwordsplit" != "" -a "$shwordsplit" = "off" ] && setopt SH_WORD_SPLIT && shwordsplit="ENABLED"
## GET OLD/NEW ##
##-------------##
OLD="$1"; shift
NEW="$1"; shift
## GET INCLUDED/EXCLUDED FILES ##
##-----------------------------##
if [ "$#" -eq 0 ]; then
# if user did not provide $INCLUDED & $EXCLUDED arguments, initialize $INCLUDED to $(pwd)
INCLUDED="$(pwd)"
EXCLUDED=""
else
# get everything before the '::'
INCLUDED="$(echo "$@" | sed -n "s/\(.*\)::\(.*\)/\1/p" | xargs)"
# get everything after the '::'
EXCLUDED="$(echo "$@" | sed -n "s/\(.*\)::\(.*\)/\2/p" | xargs)"
# if both $INCLUDED & EXCLUDED are empty, there was no '::' present in $@. Initialize $INCLUDED to $@
[ "$INCLUDED" = "" -a "$EXCLUDED" = "" ] && INCLUDED="$@"
# if $INCLUDED is empty or '::', initialize it to $(pwd)
[ "$INCLUDED" = "" -o "$INCLUDED" = "::" ] && INCLUDED="$(pwd)"
fi
## SEARCH AND REPLACE ##
##--------------------##
# --files-with-matches prints the filenames that contain the matches and passes it to xargs perl
# ag -0 will delimit the filenames with NULL, and xargs -0 will read filenames delimited by NULL. This allows for reading filenames with spaces.
ag --files-with-matches -0 "$OLD" $INCLUDED -p <(printf "$(echo $EXCLUDED | tr -s ' ' '\n')") | xargs -0 perl -pi -e "s@$OLD@$NEW@g";
## DISPLAY RESULTS ##
##-----------------##
# --path-to-ignore only reads the contents of .ignore file.
# We don't want to create an entire file just for this, so we spoof a file with <(printf ...) process substitution
ag --context=3 --pager="less -RiMSFX -#4" "$NEW" $INCLUDED --path-to-ignore <(printf "$(echo $EXCLUDED | tr -s ' ' '\n')")
# disable $variable splitting
[ "$shwordsplit" = "ENABLED" ] && unsetopt SH_WORD_SPLIT
else
echo " Usage: ragf <old> <new> [INCLUDED...] [:: EXCLUDED...]"
echo
echo " Search and replace <old> with <new> in <INCLUDED> files, ignoring <EXCLUDED> files."
echo " <INCLUDED> files are separated from <EXCLUDED> files by a '::'."
echo " When omitted, <INCLUDED> is the current dir and <EXCLUDED> is empty."
echo " Note: If your regex uses '@', it must be escaped i.e. '\\@'"
echo
echo " Examples:"
echo " ragf old new"
echo " ragf old new *.py"
echo " ragf old new file1.txt folder1/ :: folder1/file2.txt"
echo " ragf old new :: file1.txt **/*.log"
fi
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment