Last active
August 30, 2022 19:13
-
-
Save bokwoon95/6b92d03bb500fb20e217e0513ad5ac67 to your computer and use it in GitHub Desktop.
Search (and Replace) in multiple files using ag
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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