Bash script to search file contents (by file extension) for the specified search term. Uses grep under the hood.
#!/usr/bin/env bash
# shellcheck disable=SC2034,SC2086,SC2155,SC2001,SC2048
#
# Search file contents (by file extension) for the specified search term.
#
# grep options:
#
# −r Recursively search subdirectories listed.
# −I Ignore binary files.
# −s Nonexistent and unreadable files are ignored (i.e. their error messages are suppressed).
# -l Only the names of files containing selected lines are written to standard output.
# -o Print only the matched (non-empty) parts of a matching line, with each such part on a separate output line.
# -w The expression is searched for as a word.
#
# The following directories are ALWAYS ignored:
# node_modules, .git, packages, bin, obj, .idea, .vs, and .vscode
#
# Author.....: Jon LaBelle
# Date.......: December 24, 2019
# Homepage...: <https://jonlabelle.com/snippets/view/shell/search-file-content>
#
set -e
set -o pipefail
readonly SCRIPT_NAME=$(basename "${0}")
SEARCH_TERM=
FILE_EXTENSION=
FILES_WITH_MATCHES=false
IGNORE_CASE=""
VERBOSE=false
log_verbose() {
if [ "$VERBOSE" = "true" ]; then
echo "${1}"
fi
}
show_usage() {
echo "Usage: ${SCRIPT_NAME} -t <term> -e <file_extension> [options]"
echo
echo "Search options:"
echo
echo " -t, --search-term <term> the term to search in files for (case insensitive)."
echo " -e, --file-extension <extension> only files with this extension will be searched"
echo " -l, −−files-with-matches only print the matched (relative) file path to stdout."
echo " paths are listed only once per file searched."
echo " -i, −−ignore-case perform case insensitive matching."
echo
echo "NOTE: The following directories are ALWAYS ignored:"
echo "'node_modules', '.git', 'packages', 'bin', 'obj', '.idea', '.vs' and '.vscode'."
echo
echo "Other options:"
echo
echo " -v, --verbose useful for debugging and seeing what's going on under the hood."
echo " -h, --help show this message and exit."
echo
echo "Examples:"
echo
echo " To search C# files containing the term 'thread':"
echo " ${SCRIPT_NAME} -t thread -e .cs"
echo
echo " To search C# files containing the term 'thread' and print debug information (-v):"
echo " ${SCRIPT_NAME} -v -t thread -e .cs"
echo
echo " To search C# files containing the term 'Thread()' and only print file name matches (-l):"
echo " ${SCRIPT_NAME} -t 'Thread()' -e .cs -l"
echo
}
search() {
log_verbose "> Searching '${FILE_EXTENSION}' files for the term '${SEARCH_TERM}' located in '$(pwd)'..."
if [ "$FILES_WITH_MATCHES" = "true" ]; then
find . \
-path "*.git/*" -prune -o \
-path "*.idea/*" -prune -o \
-path "*.vs/*" -prune -o \
-path "*.vscode/*" -prune -o \
-path "*bin/*" -prune -o \
-path "*node_modules/*" -prune -o \
-path "*obj/*" -prune -o \
-path "*packages/*" -prune -o \
-iname '*.'${FILE_EXTENSION} \
-type f \
-exec grep ${IGNORE_CASE} -I -r -o -s -w -l ''${SEARCH_TERM}'' '{}' \;
else
find . \
-path "*.git/*" -prune -o \
-path "*.idea/*" -prune -o \
-path "*.vs/*" -prune -o \
-path "*.vscode/*" -prune -o \
-path "*bin/*" -prune -o \
-path "*node_modules/*" -prune -o \
-path "*obj/*" -prune -o \
-path "*packages/*" -prune -o \
-iname '*.'${FILE_EXTENSION} \
-type f \
-exec grep ${IGNORE_CASE} -I -r -o -s -w ''${SEARCH_TERM}'' '{}' \;
fi
}
main() {
if [[ "$1" = "-h" || "$1" = "--help" || "$1" = "help" || "$1" = "--version" ]]; then
show_usage
exit 0
fi
while (($# > 0)); do
if [[ "$1" = "-t" || "$1" = "--search-term" ]]; then
log_verbose "> Setting search term filter to: '$2'"
SEARCH_TERM=$2
shift 2
elif [[ "$1" = "-e" || "$1" = "--file-extension" ]]; then
# Normalize the file extension by remove the last period/dot.
FILE_EXTENSION="${2##*.}"
log_verbose "> Normalized file extension '$FILE_EXTENSION'"
shift 2
elif [[ "$1" = "-i" || "$1" = "--ignore-case" ]]; then
IGNORE_CASE="--ignore-case"
log_verbose "> Case insensitive search enabled."
shift 1
elif [[ "$1" = "-v" || "$1" = "--verbose" ]]; then
VERBOSE=true
log_verbose "> Verbose mode enabled."
shift 1
elif [[ "$1" = "-l" || "$1" = "−−files-with-matches" ]]; then
FILES_WITH_MATCHES=true
log_verbose "> Only file matches will be printed to stdout."
shift 1
else
shift 1
fi
done
if [[ -z "$FILE_EXTENSION" || -z "$SEARCH_TERM" ]]; then
echo "Values for both '--search-term' and '--file-extension' are required."
echo "Type '${SCRIPT_NAME} --help' for help information, including usage and examples."
exit 1
fi
log_verbose "> The following directories will be ignored:"
log_verbose "> .git"
log_verbose "> .idea"
log_verbose "> .vs"
log_verbose "> .vscode"
log_verbose "> bin"
log_verbose "> node_modules"
log_verbose "> obj"
log_verbose "> packages"
search $FILE_EXTENSION $SEARCH_TERM
exit 0
}
if [ "$#" -eq 0 ]; then
show_usage
exit 1
else
main $*
fi