Skip to content

Instantly share code, notes, and snippets.

@richdougherty
Last active September 7, 2023 19:29
Show Gist options
  • Save richdougherty/196c5311052571078b0223600ef11f22 to your computer and use it in GitHub Desktop.
Save richdougherty/196c5311052571078b0223600ef11f22 to your computer and use it in GitHub Desktop.
which-hunt - an alternative to 'which' that traces symlinks and shims
#!/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