diff --git a/.gitignore b/.gitignore index 1d5d63e..cb1c2cc 100644 --- a/.gitignore +++ b/.gitignore @@ -44,10 +44,9 @@ temp/ CLAUDE.md copilot-instructions.md -# Go build artifacts +# Go build artifacts (development) cmd/analyze/analyze cmd/status/status /status -bin/analyze-go -bin/status-go mole-analyze +# Note: bin/analyze-go and bin/status-go are released binaries and should be tracked diff --git a/.shellcheckrc b/.shellcheckrc index 5dcdb92..92118f3 100644 --- a/.shellcheckrc +++ b/.shellcheckrc @@ -2,14 +2,6 @@ disable=SC2155 # Declare and assign separately disable=SC2034 # Unused variables -disable=SC2154 # Referenced but not assigned (global vars) -disable=SC2001 # Use parameter expansion instead of sed disable=SC2059 # Don't use variables in printf format disable=SC1091 # Not following sourced files -disable=SC1003 # Backslash escape warnings -disable=SC2295 # Expansions in ${..} quoting -disable=SC2162 # read without -r -disable=SC2329 # Function never invoked warnings -disable=SC2016 # Expressions in single quotes -disable=SC2317 # Unreachable command warnings diff --git a/bin/analyze-go b/bin/analyze-go new file mode 100755 index 0000000..c239061 Binary files /dev/null and b/bin/analyze-go differ diff --git a/bin/clean.sh b/bin/clean.sh index 6711a32..2aae28c 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -43,8 +43,10 @@ WHITELIST_WARNINGS=() if [[ -f "$HOME/.config/mole/whitelist" ]]; then while IFS= read -r line; do # Trim whitespace - line="${line#${line%%[![:space:]]*}}" - line="${line%${line##*[![:space:]]}}" + # shellcheck disable=SC2295 + line="${line#"${line%%[![:space:]]*}"}" + # shellcheck disable=SC2295 + line="${line%"${line##*[![:space:]]}"}" # Skip empty lines and comments [[ -z "$line" || "$line" =~ ^# ]] && continue diff --git a/bin/status-go b/bin/status-go new file mode 100755 index 0000000..6d0d7f4 Binary files /dev/null and b/bin/status-go differ diff --git a/lib/check/all.sh b/lib/check/all.sh index 9aac02d..ddf4aa2 100644 --- a/lib/check/all.sh +++ b/lib/check/all.sh @@ -374,8 +374,10 @@ check_mole_update() { fi # Normalize version strings (remove leading 'v' or 'V') - current_version=$(echo "$current_version" | sed 's/^[vV]//') - latest_version=$(echo "$latest_version" | sed 's/^[vV]//') + current_version="${current_version#v}" + current_version="${current_version#V}" + latest_version="${latest_version#v}" + latest_version="${latest_version#V}" if [[ -n "$latest_version" && "$current_version" != "$latest_version" ]]; then # Compare versions @@ -398,7 +400,8 @@ check_all_updates() { check_homebrew_updates # Preload software update data to avoid delays between subsequent checks - get_software_updates > /dev/null 2>&1 + # Only redirect stdout, keep stderr for spinner display + get_software_updates > /dev/null check_appstore_updates check_macos_update @@ -599,7 +602,7 @@ check_swap_usage() { local swap_info=$(sysctl vm.swapusage 2> /dev/null || echo "") if [[ -n "$swap_info" ]]; then local swap_used=$(echo "$swap_info" | grep -o "used = [0-9.]*[GM]" | awk '{print $3}' || echo "0M") - local swap_num=$(echo "$swap_used" | sed 's/[GM]//') + local swap_num="${swap_used//[GM]/}" if [[ "$swap_used" == *"G"* ]]; then local swap_gb=${swap_num%.*} diff --git a/lib/core/common.sh b/lib/core/common.sh index bcbcdd0..f9b1d3b 100755 --- a/lib/core/common.sh +++ b/lib/core/common.sh @@ -88,7 +88,7 @@ is_interactive() { # Get spinner characters (overridable via MO_SPINNER_CHARS) mo_spinner_chars() { local chars="${MO_SPINNER_CHARS:-|/-\\}" - [[ -z "$chars" ]] && chars='|/-\\' + [[ -z "$chars" ]] && chars="|/-\\" printf "%s" "$chars" } @@ -803,7 +803,7 @@ request_sudo_access() { request_sudo() { echo "This operation requires administrator privileges." echo -n "Please enter your password: " - read -s password + read -r -s password echo if echo "$password" | sudo -S true 2> /dev/null; then return 0 @@ -822,6 +822,7 @@ update_via_homebrew() { local brew_pid="" local brew_tmp_file="" local brew_exit_file="" + # shellcheck disable=SC2329 cleanup_brew_update() { if [[ -n "$brew_pid" ]] && kill -0 "$brew_pid" 2> /dev/null; then kill -TERM "$brew_pid" 2> /dev/null || true @@ -971,7 +972,7 @@ start_inline_spinner() { trap 'exit 0' TERM INT EXIT local chars chars="$(mo_spinner_chars)" - [[ -z "$chars" ]] && chars='|/-\' + [[ -z "$chars" ]] && chars="|/-\\" local i=0 while true; do local c="${chars:$((i % ${#chars})):1}" diff --git a/lib/manage/whitelist.sh b/lib/manage/whitelist.sh index 9cce13a..8bd96e1 100755 --- a/lib/manage/whitelist.sh +++ b/lib/manage/whitelist.sh @@ -142,8 +142,10 @@ load_whitelist() { if [[ -f "$WHITELIST_CONFIG" ]]; then while IFS= read -r line; do - line="${line#${line%%[![:space:]]*}}" - line="${line%${line##*[![:space:]]}}" + # shellcheck disable=SC2295 + line="${line#"${line%%[![:space:]]*}"}" + # shellcheck disable=SC2295 + line="${line%"${line##*[![:space:]]}"}" [[ -z "$line" || "$line" =~ ^# ]] && continue patterns+=("$line") done < "$WHITELIST_CONFIG" diff --git a/lib/ui/menu_paginated.sh b/lib/ui/menu_paginated.sh index c59b6d6..01459e0 100755 --- a/lib/ui/menu_paginated.sh +++ b/lib/ui/menu_paginated.sh @@ -129,7 +129,7 @@ paginated_multi_select() { for ((i = 0; i < len; i++)); do c="${s:i:1}" case "$c" in - '\' | '*' | '?' | '[' | ']') out+="\\$c" ;; + $'\\' | '*' | '?' | '[' | ']') out+="\\$c" ;; *) out+="$c" ;; esac done @@ -196,6 +196,7 @@ paginated_multi_select() { } # Interrupt handler + # shellcheck disable=SC2329 handle_interrupt() { cleanup exit 130 # Standard exit code for Ctrl+C @@ -216,6 +217,7 @@ paginated_multi_select() { hide_cursor # Helper functions + # shellcheck disable=SC2329 print_line() { printf "\r\033[2K%s\n" "$1" >&2; } # Print footer lines wrapping only at separators diff --git a/lib/ui/menu_simple.sh b/lib/ui/menu_simple.sh index 17fdccb..4e43fbc 100755 --- a/lib/ui/menu_simple.sh +++ b/lib/ui/menu_simple.sh @@ -109,6 +109,7 @@ paginated_multi_select() { } # Interrupt handler + # shellcheck disable=SC2329 handle_interrupt() { cleanup exit 130 # Standard exit code for Ctrl+C @@ -129,6 +130,7 @@ paginated_multi_select() { hide_cursor # Helper functions + # shellcheck disable=SC2329 print_line() { printf "\r\033[2K%s\n" "$1" >&2; } render_item() { diff --git a/lib/uninstall/batch.sh b/lib/uninstall/batch.sh index 764be3c..9ef4d53 100755 --- a/lib/uninstall/batch.sh +++ b/lib/uninstall/batch.sh @@ -98,9 +98,11 @@ remove_file_list() { } # Batch uninstall with single confirmation +# Globals: selected_apps (read) - array of selected applications batch_uninstall_applications() { local total_size_freed=0 + # shellcheck disable=SC2154 if [[ ${#selected_apps[@]} -eq 0 ]]; then log_warning "No applications selected for uninstallation" return 0 @@ -132,12 +134,16 @@ batch_uninstall_applications() { local app_size_kb=$(get_path_size_kb "$app_path") local related_files=$(find_app_files "$bundle_id" "$app_name") local related_size_kb=$(calculate_total_size "$related_files") + # system_files is a newline-separated string, not an array + # shellcheck disable=SC2178,SC2128 local system_files=$(find_app_system_files "$bundle_id" "$app_name") + # shellcheck disable=SC2128 local system_size_kb=$(calculate_total_size "$system_files") local total_kb=$((app_size_kb + related_size_kb + system_size_kb)) ((total_estimated_size += total_kb)) # Check if system files require sudo + # shellcheck disable=SC2128 if [[ -n "$system_files" ]]; then sudo_apps+=("$app_name") fi @@ -165,7 +171,7 @@ batch_uninstall_applications() { local app_size_display=$(bytes_to_human "$((total_kb * 1024))") echo -e "${BLUE}${ICON_CONFIRM}${NC} ${app_name} ${GRAY}(${app_size_display})${NC}" - echo -e " ${GREEN}${ICON_SUCCESS}${NC} $(echo "$app_path" | sed "s|$HOME|~|")" + echo -e " ${GREEN}${ICON_SUCCESS}${NC} ${app_path/$HOME/~}" # Show related files (limit to 5 most important ones for brevity) local file_count=0 @@ -173,7 +179,7 @@ batch_uninstall_applications() { while IFS= read -r file; do if [[ -n "$file" && -e "$file" ]]; then if [[ $file_count -lt $max_files ]]; then - echo -e " ${GREEN}${ICON_SUCCESS}${NC} $(echo "$file" | sed "s|$HOME|~|")" + echo -e " ${GREEN}${ICON_SUCCESS}${NC} ${file/$HOME/~}" fi ((file_count++)) fi diff --git a/mole b/mole index a29cf5b..8e9d67b 100755 --- a/mole +++ b/mole @@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/core/common.sh" # Version info -VERSION="1.11.13" +VERSION="1.11.14" MOLE_TAGLINE="can dig deep to clean your Mac." # Check if Touch ID is already configured @@ -46,7 +46,9 @@ get_latest_version_from_github() { "https://api.github.com/repos/tw93/mole/releases/latest" 2> /dev/null | grep '"tag_name"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/') # Remove 'v' or 'V' prefix if present - echo "$version" | sed 's/^[vV]//' + version="${version#v}" + version="${version#V}" + echo "$version" } # Check if installed via Homebrew @@ -695,7 +697,6 @@ main() { ;; "remove") remove_mole - exit 0 ;; "help" | "--help" | "-h") show_help diff --git a/tests/sudo_manager.bats b/tests/sudo_manager.bats index 8de78f8..4366ed1 100644 --- a/tests/sudo_manager.bats +++ b/tests/sudo_manager.bats @@ -27,6 +27,7 @@ setup() { # We can't actually test sudo without prompting, but we can test structure # Mock sudo to avoid actual auth + # shellcheck disable=SC2329 function sudo() { return 1 # Simulate no sudo available } diff --git a/tests/update_manager.bats b/tests/update_manager.bats index b253415..f0c5846 100644 --- a/tests/update_manager.bats +++ b/tests/update_manager.bats @@ -28,6 +28,7 @@ setup() { # Test brew_has_outdated function @test "brew_has_outdated returns 1 when brew not installed" { + # shellcheck disable=SC2329 function brew() { return 127 # Command not found } @@ -39,6 +40,7 @@ setup() { @test "brew_has_outdated checks formula by default" { # Mock brew to simulate outdated formulas + # shellcheck disable=SC2329 function brew() { if [[ "$1" == "outdated" && "$2" != "--cask" ]]; then echo "package1"