Files
dotfiles/local/bin/x-path

282 lines
6.5 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# x-path: A unified script to manipulate the PATH variable.
#
# This script supports four subcommands:
# - append (or a): Remove duplicates and append one or more directories.
# - prepend (or p): Remove duplicates and prepend one or more directories.
# - remove: Remove one or more directories from PATH.
# - check: Check if the directories (or all directories in PATH if none provided) are valid.
#
# All directory arguments are normalized (trailing slashes removed, except for "/"),
# and the current PATH is normalized before any operations.
#
# Usage:
# x-path <command> <directory1> [<directory2> ...]
#
# Examples:
# x-path append /usr/local/bin /opt/bin
# x-path p /home/user/bin
# x-path remove /usr/local/bin
# x-path check # Check all directories in PATH
# x-path check /usr/local/bin /bin
#
# Enable verbose output by setting:
# export VERBOSE=1
VERBOSE="${VERBOSE:-0}"
#######################################
# Normalize a directory by removing a trailing slash (unless the directory is "/").
# Globals:
# None
# Arguments:
# $1 - Directory path to normalize
# Returns:
# Echoes the normalized directory.
#######################################
normalize_dir()
{
local d="$1"
if [ "$d" != "/" ]; then
d="${d%/}"
fi
echo "$d"
}
#######################################
# Normalize the PATH variable by normalizing each of its components.
# Globals:
# PATH
# Arguments:
# None
# Returns:
# Updates and exports PATH.
#######################################
normalize_path_var()
{
local new_path=""
local d
IFS=':' read -r -a arr <<< "$PATH"
for d in "${arr[@]}"; do
d=$(normalize_dir "$d")
if [ -z "$new_path" ]; then
new_path="$d"
else
new_path="$new_path:$d"
fi
done
PATH="$new_path"
export PATH
}
#######################################
# Remove all occurrences of a normalized directory from PATH.
# Globals:
# PATH
# Arguments:
# $1 - Normalized directory to remove from PATH.
# Returns:
# Updates PATH.
#######################################
remove_from_path()
{
local d="$1"
PATH=":${PATH}:"
PATH="${PATH//:$d:/:}"
PATH="${PATH#:}"
PATH="${PATH%:}"
}
#######################################
# Append one or more directories to PATH.
# Globals:
# PATH, VERBOSE
# Arguments:
# One or more directory paths.
# Returns:
# Updates PATH.
#######################################
do_append()
{
local processed=""
local d
for arg in "$@"; do
d=$(normalize_dir "$arg")
if [[ " $processed " == *" $d "* ]]; then
continue
else
processed="$processed $d"
fi
if [ ! -d "$d" ]; then
[ "$VERBOSE" -eq 1 ] && echo "(?) Directory '$d' does not exist. Skipping."
continue
fi
remove_from_path "$d"
PATH="${PATH:+"$PATH:"}$d"
[ "$VERBOSE" -eq 1 ] && echo "Appended '$d' to PATH."
done
export PATH
}
#######################################
# Prepend one or more directories to PATH.
# Directories are processed in reverse order so that the first argument ends up leftmost.
# Globals:
# PATH, VERBOSE
# Arguments:
# One or more directory paths.
# Returns:
# Updates PATH.
#######################################
do_prepend()
{
local processed=""
local d
local -a arr=("$@")
local i
for ((i = ${#arr[@]} - 1; i >= 0; i--)); do
d=$(normalize_dir "${arr[i]}")
if [[ " $processed " == *" $d "* ]]; then
continue
else
processed="$processed $d"
fi
if [ ! -d "$d" ]; then
[ "$VERBOSE" -eq 1 ] && echo "(?) Directory '$d' does not exist. Skipping."
continue
fi
remove_from_path "$d"
PATH="$d${PATH:+":$PATH"}"
[ "$VERBOSE" -eq 1 ] && echo "Prepended '$d' to PATH."
done
export PATH
}
#######################################
# Remove one or more directories from PATH.
# Globals:
# PATH, VERBOSE
# Arguments:
# One or more directory paths.
# Returns:
# Updates PATH.
#######################################
do_remove()
{
local processed=""
local d
for arg in "$@"; do
d=$(normalize_dir "$arg")
if [[ " $processed " == *" $d "* ]]; then
continue
else
processed="$processed $d"
fi
case ":$PATH:" in
*":$d:"*)
remove_from_path "$d"
[ "$VERBOSE" -eq 1 ] && echo "Removed '$d' from PATH."
;;
*)
[ "$VERBOSE" -eq 1 ] && echo "(?) '$d' is not in PATH."
;;
esac
done
export PATH
}
#######################################
# Check the validity of directories.
# If arguments are provided, check those directories; otherwise, check all directories in PATH.
# Globals:
# PATH
# Arguments:
# Zero or more directory paths.
# Returns:
# Outputs the validity status of each directory.
#######################################
do_check()
{
local d
if [ "$#" -eq 0 ]; then
echo "Checking all directories in PATH:"
IFS=':' read -r -a arr <<< "$PATH"
for d in "${arr[@]}"; do
d=$(normalize_dir "$d")
if [ -d "$d" ]; then
echo "Valid: $d"
else
echo "Invalid: $d"
fi
done
else
for arg in "$@"; do
d=$(normalize_dir "$arg")
if [ -d "$d" ]; then
echo "Valid: $d"
else
echo "Invalid: $d"
fi
done
fi
}
#######################################
# Main routine: Parse subcommand and arguments, normalize PATH,
# and dispatch to the appropriate functionality.
#######################################
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <command> <directory1> [<directory2> ...]"
echo "Commands:"
echo " append (or a) - Append directories to PATH"
echo " prepend (or p) - Prepend directories to PATH"
echo " remove - Remove directories from PATH"
echo " check - Check validity of directories (or all in PATH if none given)"
exit 1
fi
cmd="$1"
shift
# Normalize the current PATH variable.
normalize_path_var
case "$cmd" in
append | a)
[ "$#" -ge 1 ] || {
echo "Usage: $0 append <directory1> [<directory2> ...]"
exit 1
}
do_append "$@"
;;
prepend | p)
[ "$#" -ge 1 ] || {
echo "Usage: $0 prepend <directory1> [<directory2> ...]"
exit 1
}
do_prepend "$@"
;;
remove)
[ "$#" -ge 1 ] || {
echo "Usage: $0 remove <directory1> [<directory2> ...]"
exit 1
}
do_remove "$@"
;;
check)
# If no directories are provided, check all directories in PATH.
do_check "$@"
;;
*)
echo "Unknown command: $cmd"
echo "Usage: $0 <command> <directory1> [<directory2> ...]"
exit 1
;;
esac