Last active
September 7, 2023 19:29
-
-
Save richdougherty/196c5311052571078b0223600ef11f22 to your computer and use it in GitHub Desktop.
which-hunt - an alternative to 'which' that traces symlinks and shims
This file contains 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/bash | |
# Copyright 2023 Rich Dougherty | |
# which-hunt - an alternative to 'which' that traces symlinks and shims | |
# - Examples - | |
# | |
# $ which java | |
# /usr/bin/java | |
# | |
# $ which-hunt java | |
# /usr/lib/jvm/java-17-openjdk-amd64/bin/java | |
# | |
# $ which-hunt -v java | |
# /usr/bin/java (link) | |
# /etc/alternatives/java (link) | |
# /usr/lib/jvm/java-17-openjdk-amd64/bin/java | |
# | |
# $ which-hunt -v python | |
# /home/rich/.pyenv/shims/python (pyenv) | |
# /usr/bin/python (link) | |
# /usr/bin/python3 (link) | |
# /usr/bin/python3.10 | |
# The which-hunt script is a utility for tracing the actual executable file | |
# that a command points to in Unix-like operating systems. It handles both | |
# symlink chains as well as shim-based version manager setups (like pyenv, jenv, etc). | |
# The script supports verbose output which includes the type of each link in the chain | |
# and employs color coding (if supported by the terminal and not piped to commands like 'less') | |
# for better readability. The script also manages relative paths and converts them to | |
# absolute paths during the trace. | |
# Function to check if the script's output is being piped | |
is_piped() { | |
[[ ! -t 1 ]] | |
} | |
# Determine color support and whether to enable color output (disable if piped) | |
COLOR_SUPPORTED=$(tput colors 2>/dev/null || echo 0) | |
COLOR_ENABLED=$(( COLOR_SUPPORTED >= 8 && ! is_piped )) | |
verbose=0 | |
# Function to output colored text (if enabled) | |
color() { | |
if (( COLOR_ENABLED )); then | |
echo -ne "\e[${1}m" | |
fi | |
} | |
# Function to output verbose text (if verbose mode is enabled) | |
verbose_output() { | |
if [[ $verbose -eq 1 ]]; then | |
echo -e "$1" | |
fi | |
} | |
# Display usage information and exit with a status of 1 (indicating an error) | |
usage() { | |
echo "Usage: $0 [-v|--verbose] <command>" | |
exit 1 | |
} | |
# Check if at least one argument is supplied, otherwise display usage and exit | |
if [[ $# -lt 1 ]]; then | |
usage | |
fi | |
# Parse the supplied arguments (verbose flag and command to trace) | |
while [[ $# -gt 0 ]]; do | |
key="$1" | |
case $key in | |
-v|--verbose) | |
verbose=1 | |
shift | |
;; | |
-h|--help) | |
usage | |
;; | |
*) | |
cmd="$1" | |
shift | |
;; | |
esac | |
done | |
# If no command is specified, display usage information and exit with error status | |
if [[ -z $cmd ]]; then | |
usage | |
fi | |
# Get the initial path of the command using 'which' | |
current_path=$(which -- "$cmd" 2>/dev/null) | |
if [[ -z $current_path ]]; then | |
echo "Error: Command '$cmd' not found" | |
exit 1 | |
fi | |
# Main loop to trace the command through any symlink or shim chains | |
# Main loop to trace the command through any symlink or shim chains | |
while : ; do | |
if [[ $current_path == $HOME/.*/shims/* ]]; then | |
shim_mgr=${current_path#*$HOME/.} | |
shim_mgr=${shim_mgr%%/shims/*} | |
link_type="$shim_mgr" | |
next_path=$("$shim_mgr" which "$cmd") | |
elif [[ -L $current_path ]]; then | |
link_type="link" | |
next_path=$(readlink -- "$current_path") | |
# Ensure that the next path is an absolute path | |
if [[ ! $next_path == /* ]]; then | |
next_path=$(dirname -- "$current_path")/"$next_path" | |
fi | |
else | |
break | |
fi | |
verbose_output "$(color 36)${current_path}$(color 0) $(color 33)(${link_type})$(color 0)" | |
current_path="$next_path" | |
done | |
# Output the final resolved path of the command | |
echo "$current_path" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment