1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-16 15:18:39 +00:00

feat: add shell completion script

This commit introduces a new `completion` command that provides shell
completion for bash, zsh, and fish.

The completion is implemented as a shell script in `bin/completion.sh`
and the main `mole` script has been updated to use it.

This approach was chosen to keep the completion logic in shell script,
as the `mole` command is primarily a set of shell scripts.
This commit is contained in:
Jean-Jacques Martrès
2026-01-01 15:27:23 +01:00
parent 7d125e1a95
commit 553bd47127
2 changed files with 738 additions and 644 deletions

92
bin/completion.sh Executable file
View File

@@ -0,0 +1,92 @@
#!/bin/bash
case "$1" in
bash)
cat << 'EOF'
_mole_completions()
{
local cur_word prev_word
cur_word="${COMP_WORDS[COMP_CWORD]}"
prev_word="${COMP_WORDS[COMP_CWORD-1]}"
if [ "$COMP_CWORD" -eq 1 ]; then
COMPREPLY=( $(compgen -W "optimize clean uninstall analyze status purge touchid update remove help version completion" -- "$cur_word") )
else
case "$prev_word" in
completion)
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur_word") )
;;
*)
COMPREPLY=()
;;
esac
fi
}
complete -F _mole_completions mole
EOF
;;
zsh)
cat << 'EOF'
#compdef mole
_mole() {
local -a subcommands
subcommands=(
'optimize:Free up disk space'
'clean:Remove apps completely'
'uninstall:Check and maintain system'
'analyze:Explore disk usage'
'status:Monitor system health'
'purge:Remove old project artifacts'
'touchid:Configure Touch ID for sudo'
'update:Update to latest version'
'remove:Remove Mole from system'
'help:Show help'
'version:Show version'
'completion:Generate shell completions'
)
_describe 'subcommand' subcommands
}
_mole
EOF
;;
fish)
cat << 'EOF'
complete -c mole -n "__fish_mole_no_subcommand" -a optimize -d "Free up disk space"
complete -c mole -n "__fish_mole_no_subcommand" -a clean -d "Remove apps completely"
complete -c mole -n "__fish_mole_no_subcommand" -a uninstall -d "Check and maintain system"
complete -c mole -n "__fish_mole_no_subcommand" -a analyze -d "Explore disk usage"
complete -c mole -n "__fish_mole_no_subcommand" -a status -d "Monitor system health"
complete -c mole -n "__fish_mole_no_subcommand" -a purge -d "Remove old project artifacts"
complete -c mole -n "__fish_mole_no_subcommand" -a touchid -d "Configure Touch ID for sudo"
complete -c mole -n "__fish_mole_no_subcommand" -a update -d "Update to latest version"
complete -c mole -n "__fish_mole_no_subcommand" -a remove -d "Remove Mole from system"
complete -c mole -n "__fish_mole_no_subcommand" -a help -d "Show help"
complete -c mole -n "__fish_mole_no_subcommand" -a version -d "Show version"
complete -c mole -n "__fish_mole_no_subcommand" -a completion -d "Generate shell completions"
complete -c mole -n "not __fish_mole_no_subcommand" -a bash -d "generate bash completion" -n "__fish_see_subcommand_path completion"
complete -c mole -n "not __fish_mole_no_subcommand" -a zsh -d "generate zsh completion" -n "__fish_see_subcommand_path completion"
complete -c mole -n "not __fish_mole_no_subcommand" -a fish -d "generate fish completion" -n "__fish_see_subcommand_path completion"
function __fish_mole_no_subcommand
for i in (commandline -opc)
if contains -- $i optimize clean uninstall analyze status purge touchid update remove help version completion
return 1
end
end
return 0
end
function __fish_see_subcommand_path
string match -q -- "completion" (commandline -opc)[1]
end
EOF
;;
*)
echo "Usage: mole completion [bash|zsh|fish]"
exit 1
;;
esac

82
mole
View File

@@ -17,19 +17,19 @@ MOLE_TAGLINE="Deep clean and optimize your Mac."
is_touchid_configured() {
local pam_sudo_file="/etc/pam.d/sudo"
[[ -f "$pam_sudo_file" ]] && grep -q "pam_tid.so" "$pam_sudo_file" 2> /dev/null
[[ -f "$pam_sudo_file" ]] && grep -q "pam_tid.so" "$pam_sudo_file" 2>/dev/null
}
get_latest_version() {
curl -fsSL --connect-timeout 2 --max-time 3 -H "Cache-Control: no-cache" \
"https://raw.githubusercontent.com/tw93/mole/main/mole" 2> /dev/null |
"https://raw.githubusercontent.com/tw93/mole/main/mole" 2>/dev/null |
grep '^VERSION=' | head -1 | sed 's/VERSION="\(.*\)"/\1/'
}
get_latest_version_from_github() {
local version
version=$(curl -fsSL --connect-timeout 2 --max-time 3 \
"https://api.github.com/repos/tw93/mole/releases/latest" 2> /dev/null |
"https://api.github.com/repos/tw93/mole/releases/latest" 2>/dev/null |
grep '"tag_name"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
version="${version#v}"
version="${version#V}"
@@ -39,11 +39,11 @@ get_latest_version_from_github() {
# Install detection (Homebrew vs manual).
is_homebrew_install() {
local mole_path
mole_path=$(command -v mole 2> /dev/null) || return 1
mole_path=$(command -v mole 2>/dev/null) || return 1
if [[ -L "$mole_path" ]] && readlink "$mole_path" | grep -q "Cellar/mole"; then
if command -v brew > /dev/null 2>&1; then
brew list --formula 2> /dev/null | grep -q "^mole$" && return 0
if command -v brew >/dev/null 2>&1; then
brew list --formula 2>/dev/null | grep -q "^mole$" && return 0
else
return 1
fi
@@ -53,8 +53,8 @@ is_homebrew_install() {
case "$mole_path" in
/opt/homebrew/bin/mole | /usr/local/bin/mole)
if [[ -d /opt/homebrew/Cellar/mole ]] || [[ -d /usr/local/Cellar/mole ]]; then
if command -v brew > /dev/null 2>&1; then
brew list --formula 2> /dev/null | grep -q "^mole$" && return 0
if command -v brew >/dev/null 2>&1; then
brew list --formula 2>/dev/null | grep -q "^mole$" && return 0
else
return 0 # Cellar exists, probably Homebrew install
fi
@@ -63,11 +63,11 @@ is_homebrew_install() {
esac
fi
if command -v brew > /dev/null 2>&1; then
if command -v brew >/dev/null 2>&1; then
local brew_prefix
brew_prefix=$(brew --prefix 2> /dev/null)
brew_prefix=$(brew --prefix 2>/dev/null)
if [[ -n "$brew_prefix" && "$mole_path" == "$brew_prefix/bin/mole" && -d "$brew_prefix/Cellar/mole" ]]; then
brew list --formula 2> /dev/null | grep -q "^mole$" && return 0
brew list --formula 2>/dev/null | grep -q "^mole$" && return 0
fi
fi
@@ -89,12 +89,12 @@ check_for_updates() {
fi
if [[ -n "$latest" && "$VERSION" != "$latest" && "$(printf '%s\n' "$VERSION" "$latest" | sort -V | head -1)" == "$VERSION" ]]; then
printf "\nUpdate available: %s → %s, run %smo update%s\n\n" "$VERSION" "$latest" "$GREEN" "$NC" > "$msg_cache"
printf "\nUpdate available: %s → %s, run %smo update%s\n\n" "$VERSION" "$latest" "$GREEN" "$NC" >"$msg_cache"
else
echo -n > "$msg_cache"
echo -n >"$msg_cache"
fi
) &
disown 2> /dev/null || true
disown 2>/dev/null || true
}
show_update_notification() {
@@ -107,7 +107,7 @@ show_update_notification() {
# UI helpers
show_brand_banner() {
cat << EOF
cat <<EOF
${GREEN} __ __ _ ${NC}
${GREEN}| \/ | ___ | | ___ ${NC}
${GREEN}| |\/| |/ _ \| |/ _ \\${NC}
@@ -131,7 +131,7 @@ animate_mole_intro() {
if is_christmas_season; then
while IFS= read -r line; do
mole_lines+=("$line")
done << 'EOF'
done <<'EOF'
*
/o\
{/\_/\}
@@ -145,7 +145,7 @@ EOF
else
while IFS= read -r line; do
mole_lines+=("$line")
done << 'EOF'
done <<'EOF'
/\_/\
____/ o o \
/~____ =o= /
@@ -195,7 +195,7 @@ EOF
show_version() {
local os_ver
if command -v sw_vers > /dev/null; then
if command -v sw_vers >/dev/null; then
os_ver=$(sw_vers -productVersion)
else
os_ver="Unknown"
@@ -208,15 +208,15 @@ show_version() {
kernel=$(uname -r)
local sip_status
if command -v csrutil > /dev/null; then
sip_status=$(csrutil status 2> /dev/null | grep -o "enabled\|disabled" || echo "Unknown")
sip_status="$(tr '[:lower:]' '[:upper:]' <<< "${sip_status:0:1}")${sip_status:1}"
if command -v csrutil >/dev/null; then
sip_status=$(csrutil status 2>/dev/null | grep -o "enabled\|disabled" || echo "Unknown")
sip_status="$(tr '[:lower:]' '[:upper:]' <<<"${sip_status:0:1}")${sip_status:1}"
else
sip_status="Unknown"
fi
local disk_free
disk_free=$(df -h / 2> /dev/null | awk 'NR==2 {print $4}' || echo "Unknown")
disk_free=$(df -h / 2>/dev/null | awk 'NR==2 {print $4}' || echo "Unknown")
local install_method="Manual"
if is_homebrew_install; then
@@ -257,6 +257,8 @@ show_help() {
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize --whitelist" "$NC" "Manage protected items"
printf " %s%-28s%s %s\n" "$GREEN" "mo purge --paths" "$NC" "Configure scan directories"
echo
printf " %s%-28s%s %s\n" "$GREEN" "mo completion" "$NC" "Generate shell completion"
echo
printf "%s%s%s\n" "$BLUE" "OPTIONS" "$NC"
printf " %s%-28s%s %s\n" "$GREEN" "--debug" "$NC" "Show detailed operation logs"
echo
@@ -304,7 +306,7 @@ update_mole() {
}
local download_error=""
if command -v curl > /dev/null 2>&1; then
if command -v curl >/dev/null 2>&1; then
download_error=$(curl -fsSL --connect-timeout 10 --max-time 60 "$installer_url" -o "$tmp_installer" 2>&1) || {
local curl_exit=$?
if [[ -t 1 ]]; then stop_inline_spinner; fi
@@ -321,7 +323,7 @@ update_mole() {
echo -e "${YELLOW}Tip:${NC} URL: $installer_url"
exit 1
}
elif command -v wget > /dev/null 2>&1; then
elif command -v wget >/dev/null 2>&1; then
download_error=$(wget --timeout=10 --tries=3 -qO "$tmp_installer" "$installer_url" 2>&1) || {
if [[ -t 1 ]]; then stop_inline_spinner; fi
rm -f "$tmp_installer"
@@ -342,7 +344,7 @@ update_mole() {
chmod +x "$tmp_installer"
local mole_path
mole_path="$(command -v mole 2> /dev/null || echo "$0")"
mole_path="$(command -v mole 2>/dev/null || echo "$0")"
local install_dir
install_dir="$(cd "$(dirname "$mole_path")" && pwd)"
@@ -379,7 +381,7 @@ update_mole() {
if ! printf '%s\n' "$output" | grep -Eq "Updated to latest version|Already on latest version"; then
local new_version
new_version=$("$mole_path" --version 2> /dev/null | awk 'NF {print $NF}' || echo "")
new_version=$("$mole_path" --version 2>/dev/null | awk 'NF {print $NF}' || echo "")
printf '\n%s\n\n' "${GREEN}${ICON_SUCCESS}${NC} Updated to latest version (${new_version:-unknown})"
else
printf '\n'
@@ -424,7 +426,7 @@ remove_mole() {
local -a manual_installs=()
local -a alias_installs=()
if command -v brew > /dev/null 2>&1; then
if command -v brew >/dev/null 2>&1; then
brew_cmd="brew"
elif [[ -x "/opt/homebrew/bin/brew" ]]; then
brew_cmd="/opt/homebrew/bin/brew"
@@ -433,7 +435,7 @@ remove_mole() {
fi
if [[ -n "$brew_cmd" ]]; then
if "$brew_cmd" list --formula 2> /dev/null | grep -q "^mole$"; then
if "$brew_cmd" list --formula 2>/dev/null | grep -q "^mole$"; then
brew_has_mole="true"
fi
fi
@@ -443,7 +445,7 @@ remove_mole() {
fi
local found_mole
found_mole=$(command -v mole 2> /dev/null || true)
found_mole=$(command -v mole 2>/dev/null || true)
if [[ -n "$found_mole" && -f "$found_mole" ]]; then
if [[ ! -L "$found_mole" ]] || ! readlink "$found_mole" | grep -q "Cellar/mole"; then
manual_installs+=("$found_mole")
@@ -465,7 +467,7 @@ remove_mole() {
done
local found_mo
found_mo=$(command -v mo 2> /dev/null || true)
found_mo=$(command -v mo 2>/dev/null || true)
if [[ -n "$found_mo" && -f "$found_mo" ]]; then
alias_installs+=("$found_mo")
fi
@@ -544,11 +546,11 @@ remove_mole() {
for install in "${manual_installs[@]}"; do
if [[ -f "$install" ]]; then
if [[ ! -w "$(dirname "$install")" ]]; then
if ! sudo rm -f "$install" 2> /dev/null; then
if ! sudo rm -f "$install" 2>/dev/null; then
has_error=true
fi
else
if ! rm -f "$install" 2> /dev/null; then
if ! rm -f "$install" 2>/dev/null; then
has_error=true
fi
fi
@@ -559,11 +561,11 @@ remove_mole() {
for alias in "${alias_installs[@]}"; do
if [[ -f "$alias" ]]; then
if [[ ! -w "$(dirname "$alias")" ]]; then
if ! sudo rm -f "$alias" 2> /dev/null; then
if ! sudo rm -f "$alias" 2>/dev/null; then
has_error=true
fi
else
if ! rm -f "$alias" 2> /dev/null; then
if ! rm -f "$alias" 2>/dev/null; then
has_error=true
fi
fi
@@ -571,10 +573,10 @@ remove_mole() {
done
fi
if [[ -d "$HOME/.cache/mole" ]]; then
rm -rf "$HOME/.cache/mole" 2> /dev/null || true
rm -rf "$HOME/.cache/mole" 2>/dev/null || true
fi
if [[ -d "$HOME/.config/mole" ]]; then
rm -rf "$HOME/.config/mole" 2> /dev/null || true
rm -rf "$HOME/.config/mole" 2>/dev/null || true
fi
local final_message
@@ -607,12 +609,12 @@ show_main_menu() {
while IFS= read -r line || [[ -n "$line" ]]; do
printf '\r\033[2K%s\n' "$line"
done <<< "$banner"
done <<<"$banner"
if [[ -n "$update_message" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
printf '\r\033[2K%s\n' "$line"
done <<< "$update_message"
done <<<"$update_message"
fi
printf '\r\033[2K\n'
@@ -642,7 +644,7 @@ show_main_menu() {
interactive_main_menu() {
if [[ -t 1 ]]; then
local tty_name
tty_name=$(tty 2> /dev/null || echo "")
tty_name=$(tty 2>/dev/null || echo "")
if [[ -n "$tty_name" ]]; then
local flag_file
local cache_dir="$HOME/.cache/mole"
@@ -664,7 +666,7 @@ interactive_main_menu() {
MAIN_MENU_BANNER="$brand_banner"
if [[ -f "$msg_cache" && -s "$msg_cache" ]]; then
update_message="$(cat "$msg_cache" 2> /dev/null || echo "")"
update_message="$(cat "$msg_cache" 2>/dev/null || echo "")"
fi
MAIN_MENU_UPDATE_MESSAGE="$update_message"