#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# bat-extras | Copyright (C) 2019 eth-p | MIT License
#
# Repository: https://github.com/eth-p/bat-extras
# Issues:     https://github.com/eth-p/bat-extras/issues
# -----------------------------------------------------------------------------
# shellcheck disable=SC1090
# --- BEGIN LIBRARY FILE: print.sh ---
printc(){
printf "$(sed "$_PRINTC_PATTERN" <<<"$1")" "${@:2}"
}
printc_init(){
case "$1" in
true)_PRINTC_PATTERN="$_PRINTC_PATTERN_ANSI";;
false)_PRINTC_PATTERN="$_PRINTC_PATTERN_PLAIN";;
"[DEFINE]"){
_PRINTC_PATTERN_ANSI=""
_PRINTC_PATTERN_PLAIN=""
local name
local ansi
while read -r name ansi;do
if [[ -z $name && -z $ansi ]]||[[ ${name:0:1} == "#" ]];then
continue
fi
ansi="${ansi/\\/\\\\}"
_PRINTC_PATTERN_PLAIN="${_PRINTC_PATTERN_PLAIN}s/%{$name}//g;"
_PRINTC_PATTERN_ANSI="${_PRINTC_PATTERN_ANSI}s/%{$name}/$ansi/g;"
done
if [[ -t 1 && -z ${NO_COLOR+x} ]];then
_PRINTC_PATTERN="$_PRINTC_PATTERN_ANSI"
else
_PRINTC_PATTERN="$_PRINTC_PATTERN_PLAIN"
fi
}
esac
}
print_warning(){
printc "%{YELLOW}[%s warning]%{CLEAR}: $1%{CLEAR}\n" "batgrep" "${@:2}" 1>&2
}
print_error(){
printc "%{RED}[%s error]%{CLEAR}: $1%{CLEAR}\n" "batgrep" "${@:2}" 1>&2
}
printc_init "[DEFINE]" <<END
	CLEAR	\x1B[0m
	RED		\x1B[31m
	GREEN	\x1B[32m
	YELLOW	\x1B[33m
	BLUE	\x1B[34m
	MAGENTA	\x1B[35m
	CYAN	\x1B[36m

	DEFAULT \x1B[39m
	DIM		\x1B[2m
END
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: pager.sh ---
is_pager_less(){
[[ "$(pager_name)" == "less" ]]
return $?
}
is_pager_bat(){
[[ "$(pager_name)" == "bat" ]]
return $?
}
is_pager_disabled(){
[[ -z "$(pager_name)" ]]
return $?
}
pager_name(){
_detect_pager 1>&2
echo "$_SCRIPT_PAGER_NAME"
}
pager_version(){
_detect_pager 1>&2
echo "$_SCRIPT_PAGER_VERSION"
}
pager_exec(){
if [[ -n $SCRIPT_PAGER_CMD ]];then
"$@"|pager_display
return $?
else
"$@"
return $?
fi
}
pager_display(){
if [[ -n $SCRIPT_PAGER_CMD ]];then
if [[ -n $SCRIPT_PAGER_ARGS ]];then
"${SCRIPT_PAGER_CMD[@]}" "${SCRIPT_PAGER_ARGS[@]}"
return $?
else
"${SCRIPT_PAGER_CMD[@]}"
return $?
fi
else
cat
return $?
fi
}
_detect_pager(){
if [[ $_SCRIPT_PAGER_DETECTED == "true" ]];then return;fi
_SCRIPT_PAGER_DETECTED=true
if [[ -z ${SCRIPT_PAGER_CMD[0]} ]];then
_SCRIPT_PAGER_VERSION=0
_SCRIPT_PAGER_NAME=""
return
fi
local output
local output1
output="$("${SCRIPT_PAGER_CMD[0]}" --version 2>&1)"
output1="$(head -n 1 <<<"$output")"
if [[ $output1 =~ ^less[[:blank:]]([[:digit:]]+) ]];then
_SCRIPT_PAGER_VERSION="${BASH_REMATCH[1]}"
_SCRIPT_PAGER_NAME="less"
elif [[ $output1 =~ ^bat(cat)?[[:blank:]]([[:digit:]]+) ]];then
__BAT_VERSION="${BASH_REMATCH[2]}"
_SCRIPT_PAGER_VERSION="${BASH_REMATCH[2]}"
_SCRIPT_PAGER_NAME="bat"
else
_SCRIPT_PAGER_VERSION=0
_SCRIPT_PAGER_NAME="$(basename "${SCRIPT_PAGER_CMD[0]}")"
fi
}
_configure_pager(){
SCRIPT_PAGER_ARGS=()
if [[ -n ${PAGER+x} ]];then
SCRIPT_PAGER_CMD=($PAGER)
else
SCRIPT_PAGER_CMD=("less")
fi
if [[ -n ${BAT_PAGER+x} ]];then
SCRIPT_PAGER_CMD=($BAT_PAGER)
SCRIPT_PAGER_ARGS=()
return
fi
if is_pager_bat;then
SCRIPT_PAGER_CMD=("less")
SCRIPT_PAGER_ARGS=()
fi
if is_pager_less;then
SCRIPT_PAGER_CMD=("${SCRIPT_PAGER_CMD[0]}" -R --quit-if-one-screen)
if [[ "$(pager_version)" -lt 500 ]];then
SCRIPT_PAGER_CMD+=(--no-init)
fi
fi
}
if [[ -t 1 ]];then
_configure_pager
else
SCRIPT_PAGER_CMD=()
SCRIPT_PAGER_ARGS=()
fi
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: opt.sh ---
SHIFTOPT_HOOKS=()
SHIFTOPT_SHORT_OPTIONS="VALUE"
setargs(){
_ARGV=("$@")
_ARGV_LAST="$((${#_ARGV[@]}-1))"
_ARGV_INDEX=0
_ARGV_SUBINDEX=1
}
getargs(){
if [[ $1 == "-a" || $1 == "--append" ]];then
if [[ $_ARGV_INDEX -ne "$((_ARGV_LAST+1))" ]];then
eval "$2=(\"\${$2[@]}\" $(printf '%q ' "${_ARGV[@]:_ARGV_INDEX}"))"
fi
else
if [[ $_ARGV_INDEX -ne "$((_ARGV_LAST+1))" ]];then
eval "$1=($(printf '%q ' "${_ARGV[@]:_ARGV_INDEX}"))"
else
eval "$1=()"
fi
fi
}
resetargs(){
setargs "${_ARGV_ORIGINAL[@]}"
}
_shiftopt_next(){
_ARGV_SUBINDEX=1
((_ARGV_INDEX++))||true
}
shiftopt(){
[[ $_ARGV_INDEX -gt $_ARGV_LAST ]]&&return 1
OPT="${_ARGV[$_ARGV_INDEX]}"
unset OPT_VAL
if [[ $OPT =~ ^-[a-zA-Z0-9_-]+=.* ]];then
OPT_VAL="${OPT#*=}"
OPT="${OPT%%=*}"
fi
if [[ $OPT =~ ^-[^-]{2,} ]];then
case "$SHIFTOPT_SHORT_OPTIONS" in
PASS)_shiftopt_next;;
CONV)\
OPT="-$OPT"
_shiftopt_next
;;
VALUE){
OPT="${_ARGV[$_ARGV_INDEX]}"
OPT_VAL="${OPT:2}"
OPT="${OPT:0:2}"
_shiftopt_next
};;
SPLIT){
OPT="-${OPT:_ARGV_SUBINDEX:1}"
((_ARGV_SUBINDEX++))||true
if [[ $_ARGV_SUBINDEX -gt ${#OPT} ]];then
_shiftopt_next
fi
};;
*)printf "shiftopt: unknown SHIFTOPT_SHORT_OPTIONS mode '%s'" \
"$SHIFTOPT_SHORT_OPTIONS" \
1>&2
_shiftopt_next
esac
else
_shiftopt_next
fi
local hook
for hook in "${SHIFTOPT_HOOKS[@]}";do
if "$hook";then
shiftopt
return $?
fi
done
return 0
}
shiftval(){
if [[ -n ${OPT_VAL+x} ]];then
return 0
fi
if [[ $_ARGV_SUBINDEX -gt 1 && $SHIFTOPT_SHORT_OPTIONS == "SPLIT" ]];then
OPT_VAL="${_ARGV[$((_ARGV_INDEX+1))]}"
else
OPT_VAL="${_ARGV[$_ARGV_INDEX]}"
_shiftopt_next
fi
if [[ $OPT_VAL =~ -.* ]];then
printc "%{RED}%s: '%s' requires a value%{CLEAR}\n" "batgrep" "$ARG"
exit 1
fi
}
setargs "$@"
_ARGV_ORIGINAL=("$@")
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: opt_hook_color.sh ---
hook_color(){
SHIFTOPT_HOOKS+=("__shiftopt_hook__color")
__shiftopt_hook__color(){
case "$OPT" in
--no-color)OPT_COLOR=false;;
--color){
case "$OPT_VAL" in
"")OPT_COLOR=true;;
always|true)OPT_COLOR=true;;
never|false)OPT_COLOR=false;;
auto)return 0;;
*)printc "%{RED}%s: '--color' expects value of 'auto', 'always', or 'never'%{CLEAR}\n" "batgrep"
exit 1
esac
};;
*)return 1
esac
printc_init "$OPT_COLOR"
return 0
}
if [[ -z $OPT_COLOR ]];then
if [[ -t 1 ]];then
OPT_COLOR=true
else
OPT_COLOR=false
fi
printc_init "$OPT_COLOR"
fi
}
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: opt_hook_pager.sh ---
hook_pager(){
SHIFTOPT_HOOKS+=("__shiftopt_hook__pager")
__shiftopt_hook__pager(){
case "$OPT" in
--no-pager)shiftval
SCRIPT_PAGER_CMD=''
;;
--paging){
shiftval
case "$OPT_VAL" in
auto):;;
always):;;
never)SCRIPT_PAGER_CMD='';;
*)printc "%{RED}%s: '--paging' expects value of 'auto', 'always', or 'never'%{CLEAR}\n" "batgrep"
exit 1
esac
};;
--pager){
shiftval
{
SCRIPT_PAGER_CMD=($OPT_VAL)
PAGER_ARGS=()
}
};;
*)return 1
esac
}
}
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: opt_hook_version.sh ---
hook_version(){
SHIFTOPT_HOOKS+=("__shiftopt_hook__version")
__shiftopt_hook__version(){
if [[ $OPT == "--version" ]];then
printf "%s %s\n\n%s\n%s\n" \
"batgrep" \
"2022.07.29" \
"Copyright (C) 2019-2021 eth-p | MIT License" \
"https://github.com/eth-p/bat-extras"
exit 0
fi
return 1
}
}
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: opt_hook_width.sh ---
term_width(){
local width="$({ stty size 2>/dev/null||echo "22 80";}|cut -d ' ' -f2)"
if [[ $width -ne 0 ]];then
echo "$width"
else
echo "80"
fi
return 0
}
term_clear(){
printf "\x1B[3J\x1B[2J\x1B[H"
}
hook_width(){
SHIFTOPT_HOOKS+=("__shiftopt_hook__width")
__shiftopt_hook__width(){
case "$OPT" in
--terminal-width)shiftval
OPT_TERMINAL_WIDTH="$OPT_VAL"
;;
*)return 1
esac
return 0
}
OPT_TERMINAL_WIDTH="$(term_width)"
}
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: version.sh ---
bat_version(){
if [[ -z $__BAT_VERSION ]];then
__BAT_VERSION="$(command "/usr/bin/bat" --version|cut -d ' ' -f 2)"
fi
echo "$__BAT_VERSION"
}
version_compare(){
local version="$1"
local compare="$3"
if ! [[ $version =~ \.$ ]];then
version="$version."
fi
if ! [[ $compare =~ \.$ ]];then
compare="$compare."
fi
version_compare__recurse "$version" "$2" "$compare"
return $?
}
version_compare__recurse(){
local version="$1"
local operator="$2"
local compare="$3"
local v_major="${version%%.*}"
local c_major="${compare%%.*}"
local v_minor="${version#*.}"
local c_minor="${compare#*.}"
if [[ -z $v_minor && -z $c_minor ]];then
[ "$v_major" $operator "$c_major" ]
return $?
fi
if [[ -z $v_minor ]];then
v_minor="0."
fi
if [[ -z $c_minor ]];then
c_minor="0."
fi
case "$operator" in
-eq)[[ $v_major -ne $c_major ]]&&return 1;;
-ne)[[ $v_major -ne $c_major ]]&&return 0;;
-ge|-gt)[[ $v_major -lt $c_major ]]&&return 1
[[ $v_major -gt $c_major ]]&&return 0
;;
-le|-lt)[[ $v_major -gt $c_major ]]&&return 1
[[ $v_major -lt $c_major ]]&&return 0
esac
version_compare__recurse "$v_minor" "$operator" "$c_minor"
}
# --- END LIBRARY FILE ---
# -----------------------------------------------------------------------------
# Init:
# -----------------------------------------------------------------------------
hook_color
hook_pager
hook_version
hook_width
# -----------------------------------------------------------------------------
# Options:
# -----------------------------------------------------------------------------
RG_ARGS=()
BAT_ARGS=()
PATTERN=""
FILES=()
OPT_CASE_SENSITIVITY=''
OPT_CONTEXT_BEFORE=2
OPT_CONTEXT_AFTER=2
OPT_FOLLOW=true
OPT_SNIP=""
OPT_HIGHLIGHT=true
OPT_SEARCH_PATTERN=false
OPT_FIXED_STRINGS=false
OPT_NO_SEPARATOR=false
BAT_STYLE="${BAT_STYLE:-header,numbers}"

# Set options based on the bat version.
if version_compare "$(bat_version)" -gt "0.12"; then
	OPT_SNIP=",snip"
fi

# Parse RIPGREP_CONFIG_PATH.
if [[ -n "$RIPGREP_CONFIG_PATH" && -e "$RIPGREP_CONFIG_PATH" ]]; then
	# shellcheck disable=SC2046
	setargs $(cat "$RIPGREP_CONFIG_PATH")
	while shiftopt; do
		case "$OPT" in
			-A | --after-context)  shiftval; OPT_CONTEXT_AFTER="$OPT_VAL" ;;
			-B | --before-context) shiftval; OPT_CONTEXT_BEFORE="$OPT_VAL" ;;
			-C | --context)
				shiftval
				OPT_CONTEXT_BEFORE="$OPT_VAL"
				OPT_CONTEXT_AFTER="$OPT_VAL"
				;;
		esac
	done
fi

# Parse arguments.
shopt -s extglob   # Needed to handle -u

# First handle -u specially - it can be repeated multiple times in a single
# short argument, and repeating it 1, 2, or 3 times causes different effects.
resetargs
SHIFTOPT_SHORT_OPTIONS="PASS"
while shiftopt; do
	case "$OPT" in
		[-]+(u) )
			RG_ARGS+=("$OPT")
			;;
	esac
done
resetargs
SHIFTOPT_SHORT_OPTIONS="VALUE"
while shiftopt; do
	case "$OPT" in

	# ripgrep options
	[-]+([u]) ) ;;   # Ignore - handled in first loop.
	--unrestricted)
		RG_ARGS+=("$OPT")
		;;
	-i | --ignore-case)              OPT_CASE_SENSITIVITY="--ignore-case" ;;
	-s | --case-sensitive)           OPT_CASE_SENSITIVITY="--case-sensitive" ;;
	-S | --smart-case)               OPT_CASE_SENSITIVITY="--smart-case" ;;

	-A | --after-context)  shiftval; OPT_CONTEXT_AFTER="$OPT_VAL" ;;
	-B | --before-context) shiftval; OPT_CONTEXT_BEFORE="$OPT_VAL" ;;
	-C | --context)
		shiftval
		OPT_CONTEXT_BEFORE="$OPT_VAL"
		OPT_CONTEXT_AFTER="$OPT_VAL"
		;;

	-F | --fixed-strings)
		OPT_FIXED_STRINGS=true
		RG_ARGS+=("$OPT")
		;;

	-U | --multiline | \
	-P | --pcre2 | \
	-z | --search-zip | \
	-w | --word-regexp | \
	--one-file-system | \
	--multiline-dotall | \
	--ignore | --no-ignore | \
	--crlf | --no-crlf | \
	--hidden | --no-hidden)
		RG_ARGS+=("$OPT")
		;;

	-E | --encoding | \
	-g | --glob | \
	-t | --type | \
	-T | --type-not | \
	-m | --max-count | \
	--max-depth | \
	--iglob | \
	--ignore-file)
		shiftval
		RG_ARGS+=("$OPT" "$OPT_VAL")
		;;

	# bat options

	# Script options
	--no-follow)           OPT_FOLLOW=false ;;
	--no-snip)             OPT_SNIP="" ;;
	--no-highlight)        OPT_HIGHLIGHT=false ;;
	-p | --search-pattern) OPT_SEARCH_PATTERN=true ;;
	--no-search-pattern)   OPT_SEARCH_PATTERN=false ;;
	--no-separator)        OPT_NO_SEPARATOR=true ;;

	# Option forwarding
	--rg:*) {
		if [[ "${OPT:5:1}" = "-" ]]; then
			RG_ARGS+=("${OPT:5}")
		else
			RG_ARGS+=("--${OPT:5}")
		fi
		if [[ -n "$OPT_VAL" ]]; then
			RG_ARGS+=("$OPT_VAL")
		fi
	} ;;

	# --
	--) getargs -a FILES; break ;;

	# ???
	-*) {
		printc "%{RED}%s: unknown option '%s'%{CLEAR}\n" "batgrep" "$OPT" 1>&2
		exit 1
	} ;;

	# Search
	*) FILES+=("$OPT") ;;

	esac
done

# Use the first file as a pattern.
PATTERN="${FILES[0]}"
FILES=("${FILES[@]:1}")

if [[ -z "$PATTERN" ]]; then
	print_error "no pattern provided"
	exit 1
fi

# Generate separator.
SEP="$(printc "%{DIM}%${OPT_TERMINAL_WIDTH}s%{CLEAR}" | sed "s/ /─/g")"

# Append ripgrep and bat arguments.
if [[ -n "$OPT_CASE_SENSITIVITY" ]]; then
	RG_ARGS+=("$OPT_CASE_SENSITIVITY")
fi

if "$OPT_FOLLOW"; then
	RG_ARGS+=("--follow")
fi

if "$OPT_COLOR"; then
	BAT_ARGS+=("--color=always")
else
	BAT_ARGS+=("--color=never")
fi

if [[ "$OPT_CONTEXT_BEFORE" -eq 0 && "$OPT_CONTEXT_AFTER" -eq 0 ]]; then
	OPT_SNIP=""
	OPT_HIGHLIGHT=false
fi

# Handle the --search-pattern option.
if "$OPT_SEARCH_PATTERN"; then
	if is_pager_less; then
		if "$OPT_FIXED_STRINGS"; then
			# This strange character is a ^R, or Control-R, character. This instructs
			# less to NOT use regular expressions, which is what the -F flag does for
			# ripgrep. If we did not use this, then less would match a different pattern
			# than ripgrep searched for. See man less(1).
			SCRIPT_PAGER_ARGS+=(-p $'\x12'"$PATTERN")
		else
			SCRIPT_PAGER_ARGS+=(-p "$PATTERN")
		fi
	elif is_pager_disabled; then
		print_error "%s %s %s" \
			"The -p/--search-pattern option requires a pager, but" \
			"the pager was explicitly disabled by \$BAT_PAGER or the" \
			"--paging option."
		exit 1
	else
		print_error "Unsupported pager '%s' for option -p/--search-pattern" \
			"$(pager_name)"
		exit 1
	fi
fi

# -----------------------------------------------------------------------------
# Main:
# -----------------------------------------------------------------------------
main() {
	# shellcheck disable=SC2034
	FOUND_FILES=()
	FOUND=0
	FIRST_PRINT=true
	LAST_LR=()
	LAST_LH=()
	LAST_FILE=''
	READ_FROM_STDIN=false
	NO_SEPARATOR="$OPT_NO_SEPARATOR"

	if [[ "$BAT_STYLE" = *grid* ]]; then
		NO_SEPARATOR=true
	fi

	
	# If we found no files being provided and STDIN to not be attached to a tty,
	# we capture STDIN to a variable. This variable will later be written to
	# the STDIN file descriptors of both ripgrep and bat.
	if ! [[ -t 0 ]] && [[ "${#FILES[@]}" -eq 0 ]]; then
		READ_FROM_STDIN=true
		IFS='' STDIN_DATA="$(cat)"
	fi
	
	do_ripgrep_search() {
		local COMMON_RG_ARGS=(
			--with-filename \
			--vimgrep \
			"${RG_ARGS[@]}" \
			--context 0 \
			--sort path \
			-- \
			"$PATTERN" \
			"${FILES[@]}" \
		)
		
		if "$READ_FROM_STDIN"; then
			"rg" "${COMMON_RG_ARGS[@]}" <<< "$STDIN_DATA"
			return $?
		else
			"rg" "${COMMON_RG_ARGS[@]}"
			return $?
		fi
	}

	do_print() {
		[[ -z "$LAST_FILE" ]] && return 0

		# Print the separator.
		if ! "$NO_SEPARATOR"; then
			"$FIRST_PRINT" && echo "$SEP"
		fi
		FIRST_PRINT=false

		# Print the file.
		"/usr/bin/bat" "${BAT_ARGS[@]}" \
			"${LAST_LR[@]}" \
			"${LAST_LH[@]}" \
			--style="${BAT_STYLE}${OPT_SNIP}" \
			--paging=never \
			--terminal-width="$OPT_TERMINAL_WIDTH" \
			"$LAST_FILE"

		# Print the separator.
		if ! "$NO_SEPARATOR"; then
			echo "$SEP"
		fi
	}
	
	do_print_from_file_or_stdin() {
		if [[ "$LAST_FILE" = "<stdin>" ]]; then
			# If the file is from STDIN, we provide the STDIN
			# contents to bat and tell it to read from STDIN.
			LAST_FILE="-"
			do_print <<< "$STDIN_DATA"
			return $?
		else
			do_print
			return $?	
		fi
	}

	# shellcheck disable=SC2034
	while IFS=':' read -r file line column text; do
		((FOUND++))

		if [[ "$LAST_FILE" != "$file" ]]; then
			do_print_from_file_or_stdin
			LAST_FILE="$file"
			LAST_LR=()
			LAST_LH=()
		fi

		# Calculate the context line numbers.
		line_start=$((line - OPT_CONTEXT_BEFORE))
		line_end=$((line + OPT_CONTEXT_AFTER))
		[[ "$line_start" -gt 0 ]] || line_start=''

		LAST_LR+=("--line-range=${line_start}:${line_end}")
		[[ "$OPT_HIGHLIGHT" = "true" ]] && LAST_LH+=("--highlight-line=${line}")
	done < <(do_ripgrep_search)
	do_print_from_file_or_stdin

	# Exit.
	if [[ "$FOUND" -eq 0 ]]; then
		exit 2
	fi
}

pager_exec main
exit $?
