From a8eca1dfc784ca20520d2939aa19f7351834a614 Mon Sep 17 00:00:00 2001
From: LangLangBart <92653266+LangLangBart@users.noreply.github.com>
Date: Sat, 11 May 2024 18:12:10 +0200
Subject: [PATCH] Improve error handling, optimize history management, and
update utility commands (#4)
* feat: enhance error handling and debug logging
* fix: update grep command in remove_history to handle leading dashes
* refactor: replace 'tail -r' with a 'sed' command to reverse the order
* refactor: standardize debug and history limit variables
* chore: refactor history management and update documentation
* refactor: reorganize variable definitions and optimize history handling
---
gh-find-code | 517 +++++++++++++++++++++++++++++----------------------
readme.md | 85 +++++----
2 files changed, 349 insertions(+), 253 deletions(-)
diff --git a/gh-find-code b/gh-find-code
index 830ec9b..5a408fe 100755
--- a/gh-find-code
+++ b/gh-find-code
@@ -1,21 +1,82 @@
#!/usr/bin/env bash
-set -o allexport -o errexit -o nounset -o pipefail
+set -o allexport -o errexit -o errtrace -o nounset -o pipefail
# https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin
-# ====================== TODO =======================
+# TODO: lookout for Github adding regex support to the REST or GraphQL API
+# TODO: add tests - https://github.com/dodie/testing-in-bash
+# TODO: replace python with a more typical bash solution
-# There is no regex support currently for the REST API, look out if there are changes or
-# maybe the GraphQL API gets the ability to search code. Currently only ISSUE, REPOSITORY,
-# USER and DISCUSSION are supported.
+###############################################################################
+# Debugging and Error Handling Configuration
+###############################################################################
+
+GHFC_DEBUG_MODE=${GHFC_DEBUG_MODE:-0}
+if ((GHFC_DEBUG_MODE)); then
+ debug_directory=$(command mktemp -d)
+ store_all_debug="${debug_directory}/all_debug"
+ store_gh_api_debug="${debug_directory}/gh_api_debug"
+ store_gh_search_debug="${debug_directory}/gh_search_debug"
+ store_grep_extended_debug="${debug_directory}/grep_extended_debug"
+
+ # Redirect stdout and stderr to both the terminal and the debug file.
+ exec &> >(command tee -a "$store_all_debug")
+
+ # 'GH_DEBUG' is useful for understanding the reasons behind failed GitHub API calls.
+ export GH_DEBUG=api
+
+ # Redirect xtrace output to a file.
+ exec 6>>"$store_all_debug"
+ # Write the trace output to file descriptor 6.
+ BASH_XTRACEFD=6
+
+ # Use a more detailed execution trace prompt.
+ PS4='+$(date +%Y-%m-%d:%H:%M:%S) ${FUNCNAME[0]:-}:L${LINENO:-}: '
+ set -o xtrace
+ # Ensure xtrace is enabled in all child processes started by 'fzf'.
+ execution_shell="$(which bash) -o xtrace -c"
+fi
-# Check supported search types in the GraphQL API: gh api graphql --raw-field query=$'{
-# __type(name: "SearchType") { enumValues { name } }}' \
-# --jq '[.data.__type.enumValues[].name] | join(", ")'
+die() {
+ echo ERROR: "$*" >&2
+ exit 1
+}
-# TODO: add tests - https://github.com/dodie/testing-in-bash
-# TODO: replace python with a more typical bash solution
+BAT_THEME=${BAT_THEME:-Monokai Extended}
+bat_executable=""
+# Check for 'bat' early, as it is needed for the error_handler function.
+for value in bat batcat; do
+ if command -v $value >/dev/null; then
+ bat_executable="$value"
+ break
+ fi
+done
+builtin unset value
+[[ -z $bat_executable ]] && die "'bat' was not found."
-# ====================== set variables =======================
+# Enable 'errtrace' to ensure the ERR trap is inherited by functions, command substitutions and
+# commands executed in a subshell environment.
+error_handler() {
+ local lineno=$1 msg=$2 exit_code="${3:-1}"
+ {
+ echo
+ echo "ERROR TRACE: ${BASH_SOURCE[0]##*/}:$lineno command '$msg' exited with status $exit_code"
+ command "$bat_executable" \
+ --color always \
+ --highlight-line "$lineno" \
+ --language bash \
+ --line-range $((lineno - 3)):+7 \
+ --style numbers \
+ --terminal-width $((${COLUMNS:-$(tput cols)} - 4)) "${BASH_SOURCE[0]}" \
+ --wrap never |
+ command sed 's/^/ /;4s/ />>/'
+ } >&2
+ exit "$exit_code"
+}
+trap 'error_handler $LINENO "$BASH_COMMAND" $?' ERR
+
+###############################################################################
+# Set Variables
+###############################################################################
# define colors
COLOR_RESET='\033[0m'
@@ -31,22 +92,19 @@ WHITE_NORMAL='\033[0;97m'
WHITE_BOLD='\033[1;97m'
DARK_GRAY='\033[0;90m'
-debug_mode=false
+FZF_API_KEY=$(command head -c 32 /dev/urandom | command base64)
+GHFC_HISTORY_LIMIT=${GHFC_HISTORY_LIMIT:-500}
+GHFC_HISTORY_FILE=${GHFC_HISTORY_FILE:-${BASH_SOURCE%/*}/gh_find_code_history.txt}
+
open_in_editor=false
# Note: Using prompts of the same character length helps maintain user focus by avoiding shifts in
# the prompt's position. End the string with a color code, such as '%b', to preserve trailing
# whitespace during 'transform' actions.
-default_fzf_prompt="$(printf "%b❮❯ Code: %b" "$CYAN_NORMAL" "$COLOR_RESET")"
-fzf_prompt_failure="$(printf "%b!! Fail: %b" "$RED_NORMAL" "$COLOR_RESET")"
-fzf_prompt_fuzzyAB="$(printf "%b➤ Fuzzy:%b %b" "$CYAN_INVERT" "$CYAN_NORMAL" "$COLOR_RESET")"
-fzf_prompt_helpABC="$(printf "%b?? Help: %b" "$CYAN_NORMAL" "$COLOR_RESET")"
+default_fzf_prompt=$(printf "%b❮❯ Code: %b" "$CYAN_NORMAL" "$COLOR_RESET")
+fzf_prompt_failure=$(printf "%b!! Fail: %b" "$RED_NORMAL" "$COLOR_RESET")
+fzf_prompt_fuzzyAB=$(printf "%b➤ Fuzzy:%b %b" "$CYAN_INVERT" "$CYAN_NORMAL" "$COLOR_RESET")
+fzf_prompt_helpABC=$(printf "%b?? Help: %b" "$CYAN_NORMAL" "$COLOR_RESET")
-FZF_API_KEY="$(command head -c 32 /dev/urandom | command base64)"
-BAT_THEME=${BAT_THEME:-Monokai Extended}
-MAX_LINES_HISTORY=${MAX_LINES_HISTORY:-500}
-# Bash is required to use exported functions when the default shell is not bash.
-SHELL=$(which bash)
-gh_find_code_history="${BASH_SOURCE%/*}/gh_find_code_history.txt"
# A cached version will be used before a new one is pulled.
gh_default_cache_time="1h"
gh_default_limit=30
@@ -55,12 +113,12 @@ gh_accept_json="Accept: application/vnd.github+json"
gh_accept_raw="Accept: application/vnd.github.raw"
gh_accept_text_match="Accept: application/vnd.github.text-match+json"
gh_rest_api_version="X-GitHub-Api-Version:2022-11-28"
+
# https://github.com/junegunn/fzf/releases/tag/0.52.0
min_fzf_version="0.52.0"
# a bug with 'gh-browse' with relative paths was fixed
# https://github.com/cli/cli/issues/7674
min_gh_version="2.37.0"
-bat_executable=""
# requires 'urllib.parse'
# https://docs.python.org/3/library/urllib.parse.html
min_python_version="3.0.0"
@@ -71,27 +129,24 @@ python_executable=""
# Default directory for trivial files
scratch_directory=$(command mktemp -d)
+store_bat_langs="${scratch_directory}/bat_langs"
store_input_list="${scratch_directory}/input_list"
store_tee_append="${scratch_directory}/tee_append"
store_file_contents="${scratch_directory}/file_contents"
store_skip_count="${scratch_directory}/skip_count"
-store_fzf_ppids="${scratch_directory}/fzf_ppids"
store_query_pids="${scratch_directory}/query_pids"
store_fuzzy_search_string="${scratch_directory}/fuzzy_search_string"
store_search_string="${scratch_directory}/search_string"
-store_gh_find_code_history_tmp="${scratch_directory}/gh_find_code_history_tmp"
+store_history_tmp="${scratch_directory}/history_tmp"
+store_gh_api_error="${scratch_directory}/gh_api_error"
+store_gh_search_error="${scratch_directory}/gh_search_error"
store_hold_gh_query_loop="${scratch_directory}/hold_gh_query_loop"
store_last_query_signature="${scratch_directory}/last_search_setup"
store_current_header="${scratch_directory}/current_header"
-# Debug directory
-debug_directory=$(command mktemp -d)
-store_bat_debug="${debug_directory}/bat_debug"
-store_grep_extended_debug="${debug_directory}/grep_extended_debug"
-store_gh_search_debug="${debug_directory}/gh_search_debug"
-store_gh_content_debug="${debug_directory}/gh_content_debug"
-
-# =========================== basics ============================
+###############################################################################
+# Cleanup Functions
+###############################################################################
# Terminates processes whose IDs are stored in the given file and empty the file
kill_processes() {
@@ -117,14 +172,10 @@ kill_processes() {
}
cleanup() {
- kill_processes "$store_fzf_ppids"
kill_processes "$store_query_pids"
-
- $debug_mode && printf "%bDebug mode was ON, the following files weren't deleted.%b\n" "$YELLOW_NORMAL" "$COLOR_RESET"
command rm -rf "$scratch_directory" 2>/dev/null
- if ! $debug_mode; then
- command rm -rf "$debug_directory" 2>/dev/null
- else
+ if ((GHFC_DEBUG_MODE)); then
+ printf "%bDebug mode was active. The following files have not been deleted:%b\n" "$YELLOW_NORMAL" "$COLOR_RESET"
find "$debug_directory" -mindepth 1 2>/dev/null | while read -r matching_file; do
if [[ ! -s $matching_file ]]; then
command rm -f "$matching_file"
@@ -135,13 +186,12 @@ cleanup() {
fi
}
-die() {
- echo ERROR: "$*" >&2
- exit 1
-}
-
trap cleanup EXIT SIGHUP SIGINT
+###############################################################################
+# Helper Functions
+###############################################################################
+
# This function validates the version of a tool.
check_version() {
local tool=$1 threshold=$2 on_error=${3:-die}
@@ -153,7 +203,6 @@ check_version() {
IFS='.' read -ra ver_parts <<<"$user_version"
IFS='.' read -ra threshold_parts <<<"$threshold"
-
for i in "${!threshold_parts[@]}"; do
if ((i >= ${#ver_parts[@]})) || ((ver_parts[i] < threshold_parts[i])); then
$on_error "Your '$tool' version '$user_version' is insufficient. The minimum required version is '$threshold'."
@@ -163,6 +212,82 @@ check_version() {
done
}
+validate_environment() {
+ local value
+ if ((GHFC_DEBUG_MODE)); then
+ default_fzf_prompt="$(printf "%b❮ 𝙳𝚎𝚋𝚞𝚐 𝙼𝚘𝚍𝚎 ❯ Code: %b" "$YELLOW_NORMAL" "$COLOR_RESET")"
+ fi
+
+ # Collect all 'bat' language extensions once to avoid repetitive calls within the loop.
+ command "$bat_executable" --list-languages --color=never |
+ command awk -F ":" '{print $2}' | command tr ',' '\n' >"$store_bat_langs"
+
+ # Rule of Thumb: If it's listed under 'Utilities' in this link, don't check for it
+ # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html
+ for value in column curl fzf gh; do
+ if ! command -v $value >/dev/null; then
+ die "'$value' was not found."
+ fi
+ done
+ check_version fzf "$min_fzf_version"
+ check_version gh "$min_gh_version"
+
+ # Iterate over the possible python versions and assign python_executable
+ for value in python python3; do
+ if command -v $value >/dev/null &&
+ [[ -z $(check_version "$value" "$min_python_version" echo) ]]; then
+ python_executable="$value"
+ break
+ fi
+ done
+ # If no suitable python version was found, terminate the script
+ [[ -z $python_executable ]] && die "No suitable 'python' version found."
+
+ # Verify if there are at least two spaces between columns. The delimiter in 'fzf' is set to
+ # '\t' or '\s\s+' to separate fields. By default, the 'column' command should separate any
+ # columns with two spaces. If this is not the case, you cannot proceed. It appears that
+ # older versions, e.g. BSD 1997, of 'column' had a default of two spaces.
+ # https://github.com/freebsd/freebsd-src/blob/0da30e9aa/usr.bin/column/column.c#L245
+ # Newer versions allow the output-separator to be defined.
+ # https://github.com/util-linux/util-linux/commit/47bd8ddc
+ # https://github.com/util-linux/util-linux/issues/1699#issuecomment-1140972918
+ # https://man7.org/linux/man-pages/man1/column.1.html
+ if [[ $(command column -t <<<"A Z" 2>/dev/null) != *" "* ]]; then
+ die "Your 'column' command does not separate columns with at least two spaces. Please report this issue, stating your operating system and 'column' version."
+ fi
+
+ # Check if GHFC_HISTORY_LIMIT is a number
+ if ! [[ $GHFC_HISTORY_LIMIT =~ ^[0-9]+$ ]]; then
+ die "GHFC_HISTORY_LIMIT must be a number."
+ fi
+
+ # Check if the necessary history file exists and is readable and writable
+ if ((GHFC_HISTORY_LIMIT)); then
+ if [[ -d $GHFC_HISTORY_FILE ]]; then
+ die "$GHFC_HISTORY_FILE is a directory"
+ fi
+
+ if [[ ! -f $GHFC_HISTORY_FILE ]]; then
+ command mkdir -p "$(command dirname "${GHFC_HISTORY_FILE}")"
+ if command touch "$GHFC_HISTORY_FILE"; then
+ echo "History file successfully created at: $GHFC_HISTORY_FILE"
+ else
+ die "Unable to create: $GHFC_HISTORY_FILE"
+ fi
+ fi
+ [[ -r $GHFC_HISTORY_FILE ]] || die "Permission denied: unable to read from: $GHFC_HISTORY_FILE"
+ [[ -w $GHFC_HISTORY_FILE ]] || die "Permission denied: unable to write to: $GHFC_HISTORY_FILE"
+
+ # Add some examples if the history file is empty.
+ if [[ ! -s $GHFC_HISTORY_FILE ]]; then
+ command cat <<'EOF' >"$GHFC_HISTORY_FILE"
+repo:junegunn/fzf FZF_PORT
+extension:rs "Hello, world!"
+EOF
+ fi
+ fi
+}
+
# IMPORTANT: Keep it in sync with the readme.md
print_help_text() {
local help_text
@@ -174,7 +299,6 @@ ${WHITE_BOLD}Usage${COLOR_RESET}
gh find-code [Flags] [Search query]
${WHITE_BOLD}Flags${COLOR_RESET}
- ${GREEN_NORMAL}-d${COLOR_RESET} debug mode, primarily used for identifying and resolving bugs
${GREEN_NORMAL}-l${COLOR_RESET} limit the number of listed results (default ${gh_default_limit}, max 100)
${GREEN_NORMAL}-h${COLOR_RESET} help
@@ -204,89 +328,7 @@ EOF
echo -e "$help_text"
}
-# ====================== parse command-line options =======================
-
-while getopts ":dhl:" value; do
- case $value in
- d)
- default_fzf_prompt="$(printf "%b❮ 𝙳𝚎𝚋𝚞𝚐 𝙼𝚘𝚍𝚎 ❯ Code: %b" "$YELLOW_NORMAL" "$COLOR_RESET")"
- export GH_DEBUG="api"
- debug_mode="true"
- ;;
- l)
- if ! [[ $OPTARG =~ ^[0-9]+$ ]]; then
- die "Value is not a valid number: '$OPTARG'"
- fi
- if ((OPTARG < 1 || OPTARG > 100)); then
- die "Value for '-l' must be between 1 and 100"
- fi
- gh_user_limit="${OPTARG}"
- ;;
- h)
- print_help_text
- exit 0
- ;;
- *) die "Invalid Option: -${OPTARG-}" ;;
- esac
-done
-shift "$((OPTIND - 1))"
-
-# ====================== check requirements =======================
-
-# Rule of Thumb: If it's listed under 'Utilities' in this link, don't check for it
-# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html
-for value in column curl fzf gh; do
- if ! command -v $value >/dev/null; then
- die "'$value' was not found."
- fi
-done
-check_version fzf "$min_fzf_version"
-check_version gh "$min_gh_version"
-
-# Iterate over the possible bat names and assign bat_executable
-for value in bat batcat; do
- if command -v $value >/dev/null; then
- bat_executable="$value"
- break
- fi
-done
-[[ -z $bat_executable ]] && die "'bat' was not found."
-
-# Iterate over the possible python versions and assign python_executable
-for value in python python3; do
- if command -v $value >/dev/null &&
- [[ -z $(check_version "$value" "$min_python_version" echo) ]]; then
- python_executable="$value"
- break
- fi
-done
-# If no suitable python version was found, terminate the script
-[[ -z $python_executable ]] && die "No suitable 'python' version found."
-
-# Verify if there are at least two spaces between columns. The delimiter in 'fzf' is set to
-# '\t' or '\s\s+' to separate fields. By default, the 'column' command should separate any
-# columns with two spaces. If this is not the case, you cannot proceed. It appears that
-# older versions, e.g. BSD 1997, of 'column' had a default of two spaces.
-# https://github.com/freebsd/freebsd-src/blob/0da30e9aa/usr.bin/column/column.c#L245
-# Newer versions allow the output-separator to be defined.
-# https://github.com/util-linux/util-linux/commit/47bd8ddc
-# https://github.com/util-linux/util-linux/issues/1699#issuecomment-1140972918
-# https://man7.org/linux/man-pages/man1/column.1.html
-if [[ $(command column -t <<<"A Z" 2>/dev/null) != *" "* ]]; then
- die "Your 'column' command does not separate columns with at least two spaces. Please report this issue, stating your operating system and 'column' version."
-fi
-
-# If the history file is empty and MAX_LINES_HISTORY is greater than zero, add some examples.
-if [[ ! -s $gh_find_code_history ]] && ((MAX_LINES_HISTORY)); then
- command cat <<'EOF' >"$gh_find_code_history"
-repo:junegunn/fzf FZF_PORT
-extension:rs "Hello, world!"
-EOF
-fi
-
-# ===================== helper functions ==========================
-
-# send a POST request to fzf
+# send a POST request to 'fzf'
curl_custom() {
command curl --header "x-api-key: $FZF_API_KEY" \
--request POST "localhost:$FZF_PORT" \
@@ -320,25 +362,32 @@ open_query_in_browser() {
# Adding the current value for 'FZF_QUERY', exported by 'fzf', to the history file.
add_history() {
- echo "$FZF_QUERY" >>"$gh_find_code_history"
- # In the case of duplicates, only the most recent entry is kept. One could use 'tac' instead
- # of 'tail -r', but it requires 'coreutils'. Be careful not to read and write the same file
- # in the same pipeline - https://shellcheck.net/wiki/SC2094. This could lead to a race
- # condition, resulting in the file being erased.
- command tail -r "$gh_find_code_history" | command awk '{$1=$1}; NF && !x[$0]++' |
- command grep --invert-match '^$' | command tail -r |
- command tail -n "$MAX_LINES_HISTORY" >"$store_gh_find_code_history_tmp"
-
- command mv "$store_gh_find_code_history_tmp" "$gh_find_code_history"
+ if ((GHFC_HISTORY_LIMIT)); then
+ echo "$FZF_QUERY" >>"$GHFC_HISTORY_FILE"
+ # To avoid duplicates, only the most recent entry is retained. Since 'tail -r' does not work
+ # with the 'coreutils' version and 'tac' requires 'coreutils', 'sed' is used to reverse the
+ # order of lines. Be cautious not to read from and write to the same file in the same pipeline
+ # to prevent a race condition that could erase the file. See: https://shellcheck.net/wiki/SC2094
+ # for more information.
+ if command sed '1!G;h;$!d' "$GHFC_HISTORY_FILE" | command awk '{$1=$1}; NF && !x[$0]++' |
+ command grep --invert-match --regexp='^$' | command sed '1!G;h;$!d' |
+ command tail -n "$GHFC_HISTORY_LIMIT" >"$store_history_tmp"; then
+
+ command mv "$store_history_tmp" "$GHFC_HISTORY_FILE"
+ fi
+ fi
}
# Removing a specified line from the history file.
remove_history() {
- # Remove the specified line from the history file
- command grep --fixed-strings --line-regexp --invert-match "$*" \
- "$gh_find_code_history" >"$store_gh_find_code_history_tmp"
+ if ((GHFC_HISTORY_LIMIT)); then
+ # Attach '--regexp' directly to its argument to handle leading dashes.
+ if command grep --fixed-strings --line-regexp --invert-match --regexp="$*" \
+ "$GHFC_HISTORY_FILE" >"$store_history_tmp"; then
- command mv "$store_gh_find_code_history_tmp" "$gh_find_code_history"
+ command mv "$store_history_tmp" "$GHFC_HISTORY_FILE"
+ fi
+ fi
}
show_api_limits() {
@@ -371,7 +420,7 @@ reset_default_prompt() {
gh_query() {
local trimmed_query data items total_count total_count_si_format skip_count
local index owner_repo_name file_name file_path patterns
- local file_extension bat_langs sanitized_patterns sanitized_owner_repo_name sanitized_file_path
+ local file_extension sanitized_patterns sanitized_owner_repo_name sanitized_file_path
local matched_line error_encountered update_preview_window_size redirect_location index_color
local base_name dir_name
declare -a line_numbers
@@ -384,12 +433,13 @@ gh_query() {
fi
# delete leading and trailing whitespace from the query
- IFS=$' \t' read -r trimmed_query <<<"$FZF_QUERY"
+ trimmed_query=$(command awk '{$1=$1;print}' <<<"$FZF_QUERY")
+
# If the query is the same as before, don't bother running it again, provided that the results
# of the last query are still there and there was no error. Useful when switching between fuzzy
# mode and search mode.
current_query_signature=$(echo -n "${trimmed_query}${gh_user_limit}")
- if [[ -s $store_input_list && -s $store_current_header && ! -s $store_gh_search_debug &&
+ if [[ -s $store_input_list && -s $store_current_header && ! -s $store_gh_search_error &&
$current_query_signature == "$(<"$store_last_query_signature")" ]]; then
curl_custom "reload(command cat $store_input_list)+change-header:$(<"$store_current_header")"
return
@@ -399,7 +449,7 @@ gh_query() {
# Ensure all background jobs are terminated before starting new ones
kill_processes "$store_query_pids"
# empty the files
- : >"$store_gh_search_debug"
+ : >"$store_gh_search_error"
: >"$store_current_header"
: >"$store_input_list"
@@ -424,18 +474,21 @@ gh_query() {
patterns: ([.value.text_matches[] | .. | .text? | select(type=="string")] as $patterns_array |
if $patterns_array == [] then "__NoPatternFound__" else $patterns_array | unique | join("|") end)
} | [.index, .owner_repo_name, .file_name, .file_path, .patterns] | @tsv)' \
- 2>"$store_gh_search_debug") || [[ -z $data ]]; then
- if grep --quiet --ignore-case "API rate limit exceeded" "$store_gh_search_debug"; then
- show_api_limits >>"$store_gh_search_debug"
+ 2>"$store_gh_search_error") || [[ -z $data ]]; then
+ if grep --quiet --ignore-case "API rate limit exceeded" "$store_gh_search_error"; then
+ show_api_limits >>"$store_gh_search_error"
fi
- if [[ ! -s $store_gh_search_debug ]]; then
- echo "Unknown reason: The query failed, but no error text was written." >>"$store_gh_search_debug"
+ if [[ ! -s $store_gh_search_error ]]; then
+ echo "Unknown reason: The query failed, but no error text was written." >>"$store_gh_search_error"
fi
# Add a line to the beginning of the error file
echo "------- GitHub Code Search Failure -------" |
- command cat - "$store_gh_search_debug" >"${store_gh_search_debug}_tmp"
- command mv "${store_gh_search_debug}_tmp" "$store_gh_search_debug"
- curl_custom "unbind(tab,resize)+change-prompt($fzf_prompt_failure)+change-preview-window(99%:nohidden:wrap:~0:+1)+change-preview(command cat $store_gh_search_debug)+transform-header:printf '%bCheck preview window, query syntax, internet connection, ...%b' '$RED_NORMAL' '$COLOR_RESET'"
+ command cat - "$store_gh_search_error" >"${store_gh_search_error}_tmp"
+ command mv "${store_gh_search_error}_tmp" "$store_gh_search_error"
+ curl_custom "unbind(tab,resize)+change-prompt($fzf_prompt_failure)+change-preview-window(99%:nohidden:wrap:~0:+1)+change-preview(command cat $store_gh_search_error)+transform-header:printf '%bCheck preview window, query syntax, internet connection, ...%b' '$RED_NORMAL' '$COLOR_RESET'"
+ if ((GHFC_DEBUG_MODE)); then
+ command cp "$store_gh_search_error" "$store_gh_search_debug"
+ fi
return
else
reset_default_prompt
@@ -446,6 +499,8 @@ gh_query() {
# first line
IFS=' ' read -r items total_count
+ # Running commands in the background of a script can cause it to hang, especially if the
+ # command outputs to stdout: https://tldp.org/LDP/abs/html/x9644.html#WAITHANG
while IFS=$'\t' read -r index owner_repo_name file_name file_path patterns; do
# https://github.com/junegunn/fzf/issues/398
# Tested with 'sudo opensnoop -n bash', without a break check it keeps going through
@@ -465,14 +520,14 @@ gh_query() {
--header "$gh_accept_raw" \
--header "$gh_rest_api_version" \
>"${store_file_contents}_${index}" \
- 2>"$store_gh_content_debug"; then
+ 2>"$store_gh_api_error"; then
:
elif command nice -n 20 command gh api "https://raw.githubusercontent.com/${sanitized_owner_repo_name}/HEAD/${sanitized_file_path}" \
--cache "$gh_default_cache_time" \
--header "$gh_accept_raw" \
--header "$gh_rest_api_version" \
>"${store_file_contents}_${index}" \
- 2>"$store_gh_content_debug"; then
+ 2>"$store_gh_api_error"; then
:
fi
) &
@@ -517,11 +572,9 @@ gh_query() {
fi
# This covers special cases where syntax highlighting requires a leading
# dot, such as filenames like 'zshrc', '.zshrc' or 'macos.zshrc'
- bat_langs=$(command "$bat_executable" --list-languages --color=never |
- command awk -F ":" '{print $2}' | command tr ',' '\n')
- if command grep --quiet --word-regexp "^\.${file_extension}$" <<<"$bat_langs"; then
+ if command grep --quiet --max-count=1 --regexp="^\.${file_extension}$" -- "$store_bat_langs"; then
file_extension=".${file_extension}"
- elif command grep --quiet --word-regexp "^\.${file_name}$" <<<"$bat_langs"; then
+ elif command grep --quiet --max-count=1 --regexp="^\.${file_name}$" -- "$store_bat_langs"; then
file_extension=".${file_name}"
fi
@@ -540,7 +593,7 @@ gh_query() {
# The file is needed now to get the line numbers in the next step.
# Therefore, the file will be skipped.
echo "$index" >>"$store_skip_count"
- if $debug_mode; then
+ if ((GHFC_DEBUG_MODE)); then
error_encountered=true
fi
index_color="$RED_NORMAL"
@@ -548,7 +601,7 @@ gh_query() {
break
fi
done
- if command grep --quiet --word-regexp "$index" "$store_skip_count"; then
+ if command grep --quiet --word-regexp --regexp="$index" -- "$store_skip_count"; then
continue
fi
@@ -560,23 +613,18 @@ gh_query() {
curl_custom "transform-header:printf '%b%s/%s of %s collected...%b' '$DARK_GRAY' \
'$index' '$total_listed_results' '$total_count_si_format' '$COLOR_RESET'"
- if $debug_mode; then
+ if ((GHFC_DEBUG_MODE)); then
redirect_location="${store_grep_extended_debug}_${index}"
fi
# Escape special charters before using the string in extended 'grep'.
# However, the "|" character should be left unescaped.
- sanitized_patterns=$(command sed -e 's/[][?*+.\/$^(){}]/\\&/g' <<<"$patterns")
+ sanitized_patterns=$(command sed -e 's/[][?*+.$^()]/\\&/g' <<<"$patterns")
line_numbers=()
[[ $patterns != "__NoPatternFound__" ]] && while IFS='' read -r matched_line; do
# Ensure only valid numbers are included
if [[ $matched_line =~ ^[0-9]+ ]]; then
line_numbers+=("$matched_line")
- else
- {
- echo "The matched line is not a number."
- echo "matched_line: '$matched_line'"
- } >>"${store_grep_extended_debug}_${index}"
fi
# Use the '--text' flag, as grep will simply print 'Binary file … matches' if
# the file contains binary characters. It won't even throw an error.
@@ -588,7 +636,7 @@ gh_query() {
--extended-regexp --regexp="$sanitized_patterns" -- \
"${store_file_contents}_${index}_fetched" 2>"${redirect_location}" | cut -d: -f1)
# Save additional information only if an error is encountered by grep
- if $debug_mode && [[ -s ${store_grep_extended_debug}_${index} ]]; then
+ if ((GHFC_DEBUG_MODE)) && [[ -s ${store_grep_extended_debug}_${index} ]]; then
{
for value in "index" "owner_repo_name" "file_path" "patterns" "sanitized_patterns"; do
echo "$value = '${!value}'"
@@ -631,9 +679,12 @@ gh_query() {
skip_count="$(command sed -n '$=' "$store_skip_count")"
if $error_encountered; then
- show_api_limits >>"$store_gh_content_debug"
+ show_api_limits >>"$store_gh_api_error"
curl_custom "transform-header(printf '%bAPI failed for repos/%s/contents/%s%b' \
- '$RED_NORMAL' '$owner_repo_name' '$file_path' '$COLOR_RESET')+change-preview:command cat '$store_gh_content_debug'"
+ '$RED_NORMAL' '$owner_repo_name' '$file_path' '$COLOR_RESET')+change-preview:command cat '$store_gh_api_error'"
+ if ((GHFC_DEBUG_MODE)); then
+ command cp "$store_gh_api_error" "$store_gh_api_debug"
+ fi
elif ((skip_count > 0)); then
printf "%b%s of ∑ %s%b (Skipped: %d %s [%s])%b | ? help · esc quit%b\n" \
"$GREEN_NORMAL" "$items" "$total_count_si_format" "$RED_NORMAL" "$skip_count" \
@@ -704,19 +755,6 @@ view_contents() {
return 0
fi
- if $debug_mode; then
- {
- echo -n "bat version: "
- command "$bat_executable" --version
- echo -n "config file: "
- command "$bat_executable" --config-file
- command cat "$(command "$bat_executable" --config-file)"
- for value in BAT_PAGER BAT_CONFIG_PATH BAT_STYLE BAT_THEME BAT_TABS PAGER; do
- echo "$value = '${!value}'"
- done
- } >"$store_bat_debug" 2>&1
- fi
-
bat_args+=("--paging=always")
# The 'less' pager can move to a specific line.
if [[ $(command basename "${PAGER-}") =~ ^(bat|less)$ ]]; then
@@ -746,7 +784,6 @@ fzf_basic_style() {
: | command fzf -- \
--ansi \
--bind 'scroll-up:offset-up,scroll-down:offset-down' \
- --bind "ctrl-c:execute:kill_processes $store_fzf_ppids" \
--border block \
--color 'bg+:233,bg:235,gutter:235,border:238:dim,scrollbar:235' \
--color 'preview-bg:234,preview-border:236,preview-scrollbar:237' \
@@ -757,7 +794,9 @@ fzf_basic_style() {
--height=100% \
--header-lines 0 \
--highlight-line \
+ --no-expect \
--no-multi \
+ --no-print-query \
--info hidden \
--layout reverse \
--pointer '▶' \
@@ -766,27 +805,31 @@ fzf_basic_style() {
--separator '' \
--sync \
--unicode \
+ --with-shell "${execution_shell:-"$(which bash) -c"}" \
"$@"
}
view_history_commands() {
local header_string header_color history_command selection
- if [[ ! -s $gh_find_code_history ]]; then
+ if [[ ! -s $GHFC_HISTORY_FILE ]]; then
header_color="yellow"
header_string="No history entries yet. Check back on your next run. Press 'esc' to exit."
fi
echo "hold" >"$store_hold_gh_query_loop"
- # the extra pipe for 'bat' is to enhance the text visibility
- history_command=$'command tail -r "$gh_find_code_history" | command nl -s "\t" -n ln -w 3 | command '$bat_executable' --plain --color=always'
+ # The additional pipe for 'bat' is used to improve text visibility.
+ history_command=$'command sed \'1!G;h;$!d\' "$GHFC_HISTORY_FILE" | command nl -s "\t" -n ln -w 3 | command '$bat_executable' --plain --color=always'
+ # The Ctrl+C keybind instantly closes 'fzf' by terminating both instances.
selection=$(
fzf_basic_style \
--bind "change:first" \
- --bind "start:execute-silent(echo \${PPID-} >>$store_fzf_ppids)+reload:$history_command" \
+ --bind "start:reload:$history_command" \
+ --bind "ctrl-c:become:curl_custom 'abort'" \
--bind "ctrl-d:reload:remove_history {2..}; $history_command" \
--bind "ctrl-r:reload:$history_command" \
--bind 'ctrl-space:abort' \
--bind 'esc:abort' \
--color "header:${header_color:--1}" \
+ --delimiter '\s+' \
--header "${header_string:-"enter select · ^d delete an entry · esc quit"}" \
--info inline \
--preview-window 'hidden' \
@@ -814,38 +857,72 @@ preview_transformer() {
echo "change-preview-window:${lines%.*}"
}
-# NOTE: The 'change-preview-window' action in 'transform' should precede 'change-preview'.
-# NOTE: In the transform action, placeholders and functions using placeholders as arguments must be
-# escaped, e.g. '\view_contents \{}', but not 'print_help_text'.
-# TODO: The hotkeys 'ctrl-t' and '?' may cause issues if used during certain operations; a solution
-# is needed to address this complexity.
-fzf_basic_style \
- --bind "change:first+reload:command sleep 0.5; gh_query" \
- --bind "resize:transform:preview_transformer" \
- --bind "start:execute-silent(echo \${PPID-} >>$store_fzf_ppids)+reload:gh_query" \
- --bind 'ctrl-b:execute-silent:command gh browse --repo {4} {5}:{1}' \
- --bind "ctrl-o:transform:[[ \$FZF_MATCH_COUNT -ge 1 && ${EDITOR##*/} =~ ^(code|codium)$ ]] &&
+main() {
+ validate_environment
+
+ # CLI Options
+ while getopts ":hl:" value; do
+ case $value in
+ l)
+ if ! [[ $OPTARG =~ ^[0-9]+$ ]]; then
+ die "Value is not a valid number: '$OPTARG'"
+ fi
+ if ((OPTARG < 1 || OPTARG > 100)); then
+ die "Value for '-l' must be between 1 and 100"
+ fi
+ gh_user_limit="${OPTARG}"
+ ;;
+ h)
+ print_help_text
+ exit 0
+ ;;
+ *) die "Invalid Option: -${OPTARG-}" ;;
+ esac
+ done
+ shift "$((OPTIND - 1))"
+
+ # Disable the ERR trap to ignore SIGINT from terminating 'fzf' with Ctrl+C.
+ trap - ERR
+
+ # NOTE: The 'change-preview-window' action in 'transform' should precede 'change-preview'.
+ # NOTE: In the transform action, placeholders and functions using placeholders as arguments must be
+ # escaped, e.g. '\view_contents \{}', but not 'print_help_text'.
+ # TODO: The hotkeys 'ctrl-t' and '?' may cause issues if used during certain operations; a solution
+ # is needed to address this complexity.
+ fzf_basic_style \
+ --bind "change:first+reload:command sleep 0.5; gh_query" \
+ --bind "resize:transform:preview_transformer" \
+ --bind "start:reload:gh_query" \
+ --bind 'ctrl-b:execute-silent:command gh browse --repo {4} {5}:{1}' \
+ --bind "ctrl-o:transform:[[ \$FZF_MATCH_COUNT -ge 1 && ${EDITOR##*/} =~ ^(code|codium)$ ]] &&
echo 'execute-silent:open_in_editor=true \view_contents \{}' ||
echo 'execute:open_in_editor=true \view_contents \{}'" \
- --bind "ctrl-p:transform-query:echo repo:{4}" \
- --bind 'ctrl-r:reload:gh_user_limit=100 gh_query' \
- --bind 'ctrl-space:execute:view_history_commands' \
- --bind "ctrl-t:transform:[[ ! \$FZF_PROMPT == \"$fzf_prompt_fuzzyAB\" ]] &&
+ --bind "ctrl-p:transform-query:echo repo:{4}" \
+ --bind 'ctrl-r:reload:gh_user_limit=100 gh_query' \
+ --bind 'ctrl-space:execute:view_history_commands' \
+ --bind "ctrl-t:transform:[[ ! \$FZF_PROMPT == \"$fzf_prompt_fuzzyAB\" ]] &&
echo 'unbind(change)+change-prompt($fzf_prompt_fuzzyAB)+enable-search+transform-query:echo \{fzf:query} > $store_search_string; command cat $store_fuzzy_search_string' ||
echo 'rebind(change)+change-prompt($default_fzf_prompt)+disable-search+transform-query:echo \{fzf:query} > $store_fuzzy_search_string; command cat $store_search_string'" \
- --bind 'ctrl-x:execute-silent:open_query_in_browser {fzf:query}' \
- --bind $'enter:execute:[[ $FZF_MATCH_COUNT -ge 1 ]] && view_contents {}' \
- --bind 'esc:become:' \
- --bind "tab:change-prompt($default_fzf_prompt)+change-preview(view_contents {})+change-preview-window:hidden:hidden|+{1}+3/3" \
- --bind "?:transform:[[ ! \$FZF_PROMPT =~ \"$fzf_prompt_helpABC\" ]] &&
+ --bind 'ctrl-x:execute-silent:open_query_in_browser {fzf:query}' \
+ --bind $'enter:execute:[[ $FZF_MATCH_COUNT -ge 1 ]] && view_contents {}' \
+ --bind 'esc:become:' \
+ --bind "tab:change-prompt($default_fzf_prompt)+change-preview(view_contents {})+change-preview-window:hidden:hidden|+{1}+3/3" \
+ --bind "?:transform:[[ ! \$FZF_PROMPT =~ \"$fzf_prompt_helpABC\" ]] &&
echo 'change-prompt($fzf_prompt_helpABC)+change-preview-window(~0:+1)+change-preview:print_help_text' ||
echo 'change-prompt($default_fzf_prompt)+change-preview-window(+\{1}+3/3)+change-preview:\view_contents \{}'" \
- --delimiter '\t|\s\s+' \
- --disabled \
- --listen \
- --nth=2..,.. \
- --preview 'view_contents {}' \
- --preview-window 'border-block:~3:+{1}+3/3:nohidden:bottom:nowrap:66%' \
- --prompt "$default_fzf_prompt" \
- --query "$*" \
- --with-nth=3..
+ --delimiter '\t|\s\s+' \
+ --disabled \
+ --listen \
+ --nth=2..,.. \
+ --preview 'view_contents {}' \
+ --preview-window 'border-block:~3:+{1}+3/3:nohidden:bottom:nowrap:66%' \
+ --prompt "$default_fzf_prompt" \
+ --query "$*" \
+ --with-nth=3..
+}
+
+###############################################################################
+# Script Execution
+###############################################################################
+
+main "$@"
diff --git a/readme.md b/readme.md
index 1ac50b6..c6f270d 100644
--- a/readme.md
+++ b/readme.md
@@ -37,16 +37,15 @@ gh find-code [Flags] [Search query]
> [!IMPORTANT]
-> The search syntax differs between the WebUI and the REST API, with the latter not
-> supporting regex.
+> The search syntax differs between the WebUI and the REST API, with the latter
+> not supporting regex.
---
-| Flags | Description |
-| ----- | ------------------------------------------------------------- |
-| `-d` | debug mode, primarily used for identifying and resolving bugs |
-| `-l` | limit the number of listed results (default 30, max 100) |
-| `-h` | help |
+| Flags | Description |
+| ----- | -------------------------------------------------------- |
+| `-l` | limit the number of listed results (default 30, max 100) |
+| `-h` | help |
| Key Bindings fzf | Description |
| ------------------------------- | ------------------------------------ |
@@ -69,10 +68,10 @@ gh find-code [Flags] [Search query]
- [curl](https://github.com/curl/curl) - sending updates to `fzf`
- [Fuzzy Finder (fzf)](https://github.com/junegunn/fzf#installation) - allow for
interaction with listed data
-- [GitHub command line tool (gh)](https://github.com/cli/cli#installation) - get the data
- from Github
-- [Python](https://www.python.org) - used to parse and open custom URLs on different
- operating systems
+- [GitHub command line tool (gh)](https://github.com/cli/cli#installation) - get
+ the data from Github
+- [Python](https://www.python.org) - used to parse and open custom URLs on
+ different operating systems
```sh
# install this extension
@@ -88,8 +87,8 @@ gh ext remove LangLangBart/gh-find-code
## 💁 TIPS
### Alias
-- The name `gh find-code` was chosen for its descriptive nature. For frequent use,
- consider setting up an alias.
+- The name `gh find-code` was chosen for its descriptive nature. For frequent
+ use, consider setting up an alias.
```sh
# ~/.bashrc or ~/.zshrc
@@ -99,8 +98,9 @@ alias ghfc='BAT_THEME="Dracula" EDITOR="vim" gh find-code'
```
### Bat
-- The color scheme of the preview is determined by the `BAT_THEME` environment variable.
- If not explicitly set by the user, the theme defaults to `Monokai Extended`.
+- The color scheme of the preview is determined by the `BAT_THEME` environment
+ variable. If not explicitly set by the user, the theme defaults to `Monokai
+ Extended`.
```sh
# To view all default themes
@@ -110,11 +110,21 @@ bat --list-themes --color=never
BAT_THEME="Dracula" gh find-code
```
+### Debugging
+- To activate debug mode, set `GHFC_DEBUG_MODE=1`. This enables `xtrace` and
+ logs outputs to a file, with the file's location displayed after script
+ execution.
+
+```bash
+GHFC_DEBUG_MODE=1 gh find-code
+```
+
### Editor
-- The extension uses the `EDITOR` environment variable to determine in which editor
- the selected file will be opened, works with `nano`, `nvim/vi/vim`, and
- `VSCode/VSCodium`.
-- The code from opened files is stored temporarily and is removed when the program ends.
+- The extension uses the `EDITOR` environment variable to determine in which
+ editor the selected file will be opened, works with `nano`, `nvim/vi/vim`,
+ and `VSCode/VSCodium`.
+- The code from opened files is stored temporarily and is removed when the
+ program ends.
```sh
# Set the editor to Visual Studio Code
@@ -134,33 +144,42 @@ export FZF_DEFAULT_OPTS="
- See `man fzf` for `AVAILABLE KEYS` or
[junegunn/fzf](https://github.com/junegunn/fzf#environment-variables) for more
details.
-- NOTE: [How to use ALT commands in a terminal on
- macOS?](https://superuser.com/questions/496090/how-to-use-alt-commands-in-a-terminal-on-os-x)
+- **NOTE:** [How to use ALT commands in a terminal on macOS?](https://superuser.com/questions/496090/how-to-use-alt-commands-in-a-terminal-on-os-x)
### History
-- The `gh_find_code_history.txt` file stores successfully completed unique commands. All commands
- can be viewed with ⌃ Control + Space. In case of duplicates, only the most
- recent entry is preserved. The maximum number of command entries is 500 by default, but this can
- be overridden by assigning a value to the `MAX_LINES_HISTORY` variable.
+- Recent commands can be viewed with ⌃ Control + Space.
+- The history file stores successfully completed unique commands, one can
+ specify a custom location using the `GHFC_HISTORY_FILE` environment variable.
+
+```bash
+# Default location: ${BASH_SOURCE%/*}/gh_find_code_history.txt
+# Specify a custom location for the history file
+GHFC_HISTORY_FILE="/custom/location/history.txt" gh find-code
+```
-```sh
+- In case of duplicates, only the most recent entry is preserved. The default
+ maximum number of command entries is **500**, but this can be adjusted by
+ setting the `GHFC_HISTORY_LIMIT` variable.
+
+```bash
# Set the maximum number of stored commands to 1000
-MAX_LINES_HISTORY="1000" gh find-code
+GHFC_HISTORY_LIMIT="1000" gh find-code
```
### Pager
-- If the `PAGER` environment variable is set to `less` or `bat`, when opening the destination file,
- it will automatically scroll to the matching line found.
+- If the `PAGER` environment variable is set to `less` or `bat`, when opening
+ the destination file, it will automatically scroll to the matching line found.
---
## 💪 Contributing
> [!NOTE]
-> _Pre-commit is a multi-language package manager for pre-commit hooks. You specify a list_
-> _of hooks you want and **pre-commit manages the installation and execution** of any hook_
-> _written in any language before every commit._ **Source:** [pre-commit
-> introduction](https://pre-commit.com/#introduction)
+> _Pre-commit is a multi-language package manager for pre-commit hooks. You_
+> _specify a list of hooks you want and pre-commit **manages** the installation_
+> _and **execution** of any hook written in any language before every commit._
+>
+> **Source:** [pre-commit introduction](https://pre-commit.com/#introduction)
```sh
# shellcheck and shfmt are necessary dependencies for one hook