From 3c56fe06330388ac9cad51e0c4c97430c87ebf71 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Sun, 12 Oct 2025 15:43:45 +0800 Subject: [PATCH] Complete automated testing --- .github/workflows/shell-format.yml | 45 ++++ .shellcheckrc | 10 + README.md | 39 +++- bin/analyze.sh | 330 +++++++++++++++++++---------- bin/clean.sh | 4 +- bin/uninstall.sh | 74 ++++--- lib/batch_uninstall.sh | 37 +++- lib/paginated_menu.sh | 10 +- mole | 18 +- tests/helpers/uninstall_stubs.sh | 22 -- tests/run.sh | 49 +++-- tests/uninstall.bats | 15 +- tests/z_shellcheck.bats | 29 +++ 13 files changed, 484 insertions(+), 198 deletions(-) create mode 100644 .github/workflows/shell-format.yml create mode 100644 .shellcheckrc delete mode 100644 tests/helpers/uninstall_stubs.sh create mode 100644 tests/z_shellcheck.bats diff --git a/.github/workflows/shell-format.yml b/.github/workflows/shell-format.yml new file mode 100644 index 0000000..437281d --- /dev/null +++ b/.github/workflows/shell-format.yml @@ -0,0 +1,45 @@ +name: Shell Format & Lint + +on: + push: + branches: [main] + paths: + - '**/*.sh' + - mole + pull_request: + paths: + - '**/*.sh' + - mole + +jobs: + format-lint: + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install shfmt and shellcheck + run: brew install shfmt shellcheck + + - name: Run shfmt in diff mode + run: | + set -euo pipefail + mapfile -d '' files < <(find . -type f \( -name '*.sh' -o -name 'mole' \) \ + ! -path './.git/*' ! -path './tests/tmp-*' -print0) + if (( ${#files[@]} == 0 )); then + echo "No shell files found; skipping shfmt" + exit 0 + fi + shfmt -i 4 -ci -sr -d "${files[@]}" + + - name: Run shellcheck + run: | + set -euo pipefail + mapfile -d '' files < <(find . -type f \( -name '*.sh' -o -name 'mole' \) \ + ! -path './.git/*' ! -path './tests/tmp-*' -print0) + if (( ${#files[@]} == 0 )); then + echo "No shell files found; skipping shellcheck" + exit 0 + fi + shellcheck --rcfile .shellcheckrc "${files[@]}" diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..5fa7515 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,10 @@ +# Mole project shellcheck configuration +# +# Keep the lint strict by default. Add rules to disable only when we have a +# clear justification. +# +# Examples: +# disable=SC2034 # unused variables (if intentionally unused) +# disable=SC1091 # sourcing files not present in repo (optional) + +# Currently no global disables are required. diff --git a/README.md b/README.md index 446c588..00ef708 100644 --- a/README.md +++ b/README.md @@ -141,10 +141,47 @@ Total: 156.8GB └─ πŸ“ Desktop 12.7GB ``` +## Development + +### Setup + +Install development tools: + +```bash +brew install shfmt shellcheck bats-core +``` + +### Code Quality + +Format and lint shell scripts: + +```bash +# Format all scripts +./scripts/format.sh + +# Check without modifying +./scripts/format.sh --check + +# Install git hooks for auto-formatting +./scripts/install-hooks.sh +``` + +See [scripts/README.md](scripts/README.md) for detailed development workflow. + +### Testing + +Run automated tests: + +```bash +./tests/run.sh +``` + +GitHub Actions automatically runs tests and formatting checks on PRs. + ## Support - If Mole reclaimed storage for you, consider starring the repo or sharing it with friends needing a cleaner Mac. -- Have ideas or fixes? Open an issue or PR and help shape Mole’s roadmap together with the community. +- Have ideas or fixes? Open an issue or PR and help shape Mole's roadmap together with the community. - Love cats? Treat Tangyuan and Cola to canned food via this link and keep the mascots purring. ## License diff --git a/bin/analyze.sh b/bin/analyze.sh index b65b3ee..cae26a4 100755 --- a/bin/analyze.sh +++ b/bin/analyze.sh @@ -74,7 +74,8 @@ scan_large_files() { local file="" while IFS= read -r file; do if [[ -f "$file" ]]; then - local size=$(stat -f%z "$file" 2>/dev/null || echo "0") + local size + size=$(stat -f%z "$file" 2>/dev/null || echo "0") echo "$size|$file" fi done < <(mdfind -onlyin "$target_path" "kMDItemFSSize > $MIN_LARGE_FILE_SIZE" 2>/dev/null) | \ @@ -93,7 +94,8 @@ scan_medium_files() { local file="" while IFS= read -r file; do if [[ -f "$file" ]]; then - local size=$(stat -f%z "$file" 2>/dev/null || echo "0") + local size + size=$(stat -f%z "$file" 2>/dev/null || echo "0") echo "$size|$file" fi done < <(mdfind -onlyin "$target_path" \ @@ -158,7 +160,8 @@ aggregate_by_directory() { # Get cache file path for a directory get_cache_file() { local target_path="$1" - local path_hash=$(echo "$target_path" | md5 2>/dev/null || echo "$target_path" | shasum | cut -d' ' -f1) + local path_hash + path_hash=$(echo "$target_path" | md5 2>/dev/null || echo "$target_path" | shasum | cut -d' ' -f1) echo "$CACHE_DIR/scan_${path_hash}.cache" } @@ -171,7 +174,8 @@ is_cache_valid() { return 1 fi - local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0))) + local cache_age + cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0))) if [[ $cache_age -lt $max_age ]]; then return 0 fi @@ -236,7 +240,8 @@ perform_scan() { local force_rescan="${2:-false}" # Check cache first - local cache_file=$(get_cache_file "$target_path") + local cache_file + cache_file=$(get_cache_file "$target_path") if [[ "$force_rescan" != "true" ]] && is_cache_valid "$cache_file" 3600; then log_info "Loading cached results for $target_path..." load_from_cache "$cache_file" @@ -328,8 +333,10 @@ generate_bar() { return fi - local filled=$((current * width / max)) - local empty=$((width - filled)) + local filled + filled=$((current * width / max)) + local empty + empty=$((width - filled)) # Ensure non-negative [[ $filled -lt 0 ]] && filled=0 @@ -372,7 +379,8 @@ display_large_files_compact() { local count=0 local total_size=0 - local total_count=$(wc -l < "$temp_large" | tr -d ' ') + local total_count + total_count=$(wc -l < "$temp_large" | tr -d ' ') # Calculate total size while IFS='|' read -r size path; do @@ -385,11 +393,15 @@ display_large_files_compact() { break fi - local human_size=$(bytes_to_human "$size") - local filename=$(basename "$path") - local dirname=$(basename "$(dirname "$path")") + local human_size + human_size=$(bytes_to_human "$size") + local filename + filename=$(basename "$path") + local dirname + dirname=$(basename "$(dirname "$path")") - local info=$(get_file_info "$path") + local info + info=$(get_file_info "$path") local badge="${info%|*}" printf " ${GREEN}%-8s${NC} %s %-40s ${GRAY}%s${NC}\n" \ "$human_size" "$badge" "${filename:0:40}" "$dirname" @@ -398,7 +410,8 @@ display_large_files_compact() { done < "$temp_large" echo "" - local total_human=$(bytes_to_human "$total_size") + local total_human + total_human=$(bytes_to_human "$total_size") echo " ${GRAY}Found $total_count large files (>1GB), totaling $total_human${NC}" echo "" } @@ -430,13 +443,19 @@ display_large_files() { break fi - local human_size=$(bytes_to_human "$size") - local percentage=$(calc_percentage "$size" "$max_size") - local bar=$(generate_bar "$size" "$max_size" 20) - local filename=$(basename "$path") - local dirname=$(dirname "$path" | sed "s|^$HOME|~|") + local human_size + human_size=$(bytes_to_human "$size") + local percentage + percentage=$(calc_percentage "$size" "$max_size") + local bar + bar=$(generate_bar "$size" "$max_size" 20) + local filename + filename=$(basename "$path") + local dirname + dirname=$(dirname "$path" | sed "s|^$HOME|~|") - local info=$(get_file_info "$path") + local info + info=$(get_file_info "$path") local badge="${info%|*}" printf " %s [${GREEN}%s${NC}] %7s\n" "$bar" "$human_size" "" printf " %s %s\n" "$badge" "$filename" @@ -446,7 +465,8 @@ display_large_files() { done < "$temp_large" # Show total count - local total_count=$(wc -l < "$temp_large" | tr -d ' ') + local total_count + total_count=$(wc -l < "$temp_large" | tr -d ' ') if [[ $total_count -gt 10 ]]; then echo " ${GRAY}... and $((total_count - 10)) more files${NC}" echo "" @@ -479,17 +499,22 @@ display_directories_compact() { break fi - local human_size=$(bytes_to_human "$size") - local percentage=$(calc_percentage "$size" "$total_size") - local dirname=$(basename "$path") + local human_size + human_size=$(bytes_to_human "$size") + local percentage + percentage=$(calc_percentage "$size" "$total_size") + local dirname + dirname=$(basename "$path") # Simple bar (10 chars) local bar_width=10 local percentage_int=${percentage%.*} # Remove decimal part - local filled=$((percentage_int * bar_width / 100)) + local filled + filled=$((percentage_int * bar_width / 100)) [[ $filled -gt $bar_width ]] && filled=$bar_width [[ $filled -lt 0 ]] && filled=0 - local empty=$((bar_width - filled)) + local empty + empty=$((bar_width - filled)) [[ $empty -lt 0 ]] && empty=0 local bar="" if [[ $filled -gt 0 ]]; then @@ -538,11 +563,16 @@ display_directories() { break fi - local human_size=$(bytes_to_human "$size") - local percentage=$(calc_percentage "$size" "$total_size") - local bar=$(generate_bar "$size" "$max_size" 20) - local display_path=$(echo "$path" | sed "s|^$HOME|~|") - local dirname=$(basename "$path") + local human_size + human_size=$(bytes_to_human "$size") + local percentage + percentage=$(calc_percentage "$size" "$total_size") + local bar + bar=$(generate_bar "$size" "$max_size" 20) + local display_path + display_path=$(echo "$path" | sed "s|^$HOME|~|") + local dirname + dirname=$(basename "$path") printf " %s [${BLUE}%s${NC}] %5s%%\n" "$bar" "$human_size" "$percentage" printf " %s %s\n\n" "$BADGE_DIR" "$display_path" @@ -568,8 +598,10 @@ display_hotspots() { break fi - local human_size=$(bytes_to_human "$size") - local display_path=$(echo "$path" | sed "s|^$HOME|~|") + local human_size + human_size=$(bytes_to_human "$size") + local display_path + display_path=$(echo "$path" | sed "s|^$HOME|~|") printf " %s\n" "$display_path" printf " ${GREEN}%s${NC} in ${YELLOW}%d${NC} large files\n\n" \ @@ -589,9 +621,11 @@ display_cleanup_suggestions_compact() { # Check common cache locations (only if analyzing Library/Caches or system paths) if [[ "$CURRENT_PATH" == "$HOME/Library/Caches"* ]] || [[ "$CURRENT_PATH" == "$HOME/Library"* ]]; then if [[ -d "$HOME/Library/Caches" ]]; then - local cache_size=$(du -sk "$HOME/Library/Caches" 2>/dev/null | cut -f1) + local cache_size + cache_size=$(du -sk "$HOME/Library/Caches" 2>/dev/null | cut -f1) if [[ $cache_size -gt 1048576 ]]; then # > 1GB - local human=$(bytes_to_human $((cache_size * 1024))) + local human + human=$(bytes_to_human $((cache_size * 1024))) top_suggestion="Clear app caches ($human)" action_command="mole clean" ((potential_space += cache_size * 1024)) @@ -602,7 +636,8 @@ display_cleanup_suggestions_compact() { # Check Downloads folder (only if analyzing Downloads) if [[ "$CURRENT_PATH" == "$HOME/Downloads"* ]]; then - local old_files=$(find "$CURRENT_PATH" -type f -mtime +90 2>/dev/null | wc -l | tr -d ' ') + local old_files + old_files=$(find "$CURRENT_PATH" -type f -mtime +90 2>/dev/null | wc -l | tr -d ' ') if [[ $old_files -gt 0 ]]; then [[ -z "$top_suggestion" ]] && top_suggestion="$old_files files older than 90 days found" [[ -z "$action_command" ]] && action_command="manually review old files" @@ -618,7 +653,8 @@ display_cleanup_suggestions_compact() { local dmg_size=$(mdfind -onlyin "$CURRENT_PATH" \ "kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2>/dev/null | \ xargs stat -f%z 2>/dev/null | awk '{sum+=$1} END {print sum}') - local dmg_human=$(bytes_to_human "$dmg_size") + local dmg_human + dmg_human=$(bytes_to_human "$dmg_size") [[ -z "$top_suggestion" ]] && top_suggestion="$dmg_count DMG files ($dmg_human) can be removed" [[ -z "$action_command" ]] && action_command="manually delete DMG files" ((potential_space += dmg_size)) @@ -628,9 +664,11 @@ display_cleanup_suggestions_compact() { # Check Xcode (only if in developer paths) if [[ "$CURRENT_PATH" == "$HOME/Library/Developer"* ]] && [[ -d "$HOME/Library/Developer/Xcode/DerivedData" ]]; then - local xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2>/dev/null | cut -f1) + local xcode_size + xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2>/dev/null | cut -f1) if [[ $xcode_size -gt 10485760 ]]; then - local xcode_human=$(bytes_to_human $((xcode_size * 1024))) + local xcode_human + xcode_human=$(bytes_to_human $((xcode_size * 1024))) [[ -z "$top_suggestion" ]] && top_suggestion="Xcode cache ($xcode_human) can be cleared" [[ -z "$action_command" ]] && action_command="mole clean" ((potential_space += xcode_size * 1024)) @@ -656,7 +694,8 @@ display_cleanup_suggestions_compact() { echo " ${GRAY}... and $((suggestions_count - 1)) more insights${NC}" fi if [[ $potential_space -gt 0 ]]; then - local space_human=$(bytes_to_human "$potential_space") + local space_human + space_human=$(bytes_to_human "$potential_space") echo " ${GREEN}Potential recovery: ~$space_human${NC}" fi echo "" @@ -680,16 +719,19 @@ display_cleanup_suggestions() { # Check common cache locations if [[ -d "$HOME/Library/Caches" ]]; then - local cache_size=$(du -sk "$HOME/Library/Caches" 2>/dev/null | cut -f1) + local cache_size + cache_size=$(du -sk "$HOME/Library/Caches" 2>/dev/null | cut -f1) if [[ $cache_size -gt 1048576 ]]; then # > 1GB - local human=$(bytes_to_human $((cache_size * 1024))) + local human + human=$(bytes_to_human $((cache_size * 1024))) suggestions+=(" Clear application caches: $human") fi fi # Check Downloads folder if [[ -d "$HOME/Downloads" ]]; then - local old_files=$(find "$HOME/Downloads" -type f -mtime +90 2>/dev/null | wc -l | tr -d ' ') + local old_files + old_files=$(find "$HOME/Downloads" -type f -mtime +90 2>/dev/null | wc -l | tr -d ' ') if [[ $old_files -gt 0 ]]; then suggestions+=(" Clean old downloads: $old_files files older than 90 days") fi @@ -706,18 +748,22 @@ display_cleanup_suggestions() { # Check Xcode derived data if [[ -d "$HOME/Library/Developer/Xcode/DerivedData" ]]; then - local xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2>/dev/null | cut -f1) + local xcode_size + xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2>/dev/null | cut -f1) if [[ $xcode_size -gt 10485760 ]]; then # > 10GB - local human=$(bytes_to_human $((xcode_size * 1024))) + local human + human=$(bytes_to_human $((xcode_size * 1024))) suggestions+=(" Clear Xcode cache: $human") fi fi # Check iOS device backups if [[ -d "$HOME/Library/Application Support/MobileSync/Backup" ]]; then - local backup_size=$(du -sk "$HOME/Library/Application Support/MobileSync/Backup" 2>/dev/null | cut -f1) + local backup_size + backup_size=$(du -sk "$HOME/Library/Application Support/MobileSync/Backup" 2>/dev/null | cut -f1) if [[ $backup_size -gt 5242880 ]]; then # > 5GB - local human=$(bytes_to_human $((backup_size * 1024))) + local human + human=$(bytes_to_human $((backup_size * 1024))) suggestions+=(" πŸ“± Review iOS backups: $human") fi fi @@ -728,7 +774,8 @@ display_cleanup_suggestions() { mdfind -onlyin "$CURRENT_PATH" "kMDItemFSSize > 10000000" 2>/dev/null | \ xargs -I {} stat -f "%z" {} 2>/dev/null | \ sort | uniq -d | wc -l | tr -d ' ' > "$temp_dup" 2>/dev/null || echo "0" > "$temp_dup" - local dup_count=$(cat "$temp_dup" 2>/dev/null || echo "0") + local dup_count + dup_count=$(cat "$temp_dup" 2>/dev/null || echo "0") if [[ $dup_count -gt 5 ]]; then suggestions+=(" ♻️ Possible duplicates: $dup_count size matches in large files (>10MB)") fi @@ -772,11 +819,13 @@ display_disk_summary() { log_header "Disk Situation" - local target_display=$(echo "$CURRENT_PATH" | sed "s|^$HOME|~|") + local target_display + target_display=$(echo "$CURRENT_PATH" | sed "s|^$HOME|~|") echo " ${BLUE}Scanning:${NC} $target_display | ${BLUE}Free:${NC} $(get_free_space)" if [[ $total_large_count -gt 0 ]]; then - local large_human=$(bytes_to_human "$total_large_size") + local large_human + large_human=$(bytes_to_human "$total_large_size") echo " ${BLUE}Large Files:${NC} $total_large_count files ($large_human) | ${BLUE}Total:${NC} $(bytes_to_human "$total_dirs_size") in $total_dirs_count dirs" elif [[ $total_dirs_size -gt 0 ]]; then echo " ${BLUE}Total Scanned:${NC} $(bytes_to_human "$total_dirs_size") across $total_dirs_count directories" @@ -820,10 +869,14 @@ get_file_age() { return fi - local mtime=$(stat -f%m "$path" 2>/dev/null || echo "0") - local now=$(date +%s) - local diff=$((now - mtime)) - local days=$((diff / 86400)) + local mtime + mtime=$(stat -f%m "$path" 2>/dev/null || echo "0") + local now + now=$(date +%s) + local diff + diff=$((now - mtime)) + local days + days=$((diff / 86400)) if [[ $days -lt 1 ]]; then echo "Today" @@ -832,10 +885,12 @@ get_file_age() { elif [[ $days -lt 30 ]]; then echo "${days}d" elif [[ $days -lt 365 ]]; then - local months=$((days / 30)) + local months + months=$((days / 30)) echo "${months}mo" else - local years=$((days / 365)) + local years + years=$((days / 365)) echo "${years}yr" fi } @@ -860,13 +915,17 @@ display_large_files_table() { break fi - local human_size=$(bytes_to_human "$size") - local filename=$(basename "$path") + local human_size + human_size=$(bytes_to_human "$size") + local filename + filename=$(basename "$path") local ext="${filename##*.}" - local age=$(get_file_age "$path") + local age + age=$(get_file_age "$path") # Get file info and badge - local info=$(get_file_info "$path") + local info + info=$(get_file_info "$path") local badge="${info%|*}" # Truncate filename if too long @@ -889,7 +948,8 @@ display_large_files_table() { ((count++)) done < "$temp_large" - local total=$(wc -l < "$temp_large" | tr -d ' ') + local total + total=$(wc -l < "$temp_large" | tr -d ' ') if [[ $total -gt 20 ]]; then echo " ${GRAY}... $((total - 20)) more files${NC}" fi @@ -925,19 +985,24 @@ display_unified_directories() { break fi - local percentage=$((size * 100 / total_size)) - local bar_width=$((percentage * chart_width / 100)) + local percentage + percentage=$((size * 100 / total_size)) + local bar_width + bar_width=$((percentage * chart_width / 100)) [[ $bar_width -lt 1 ]] && bar_width=1 - local dirname=$(basename "$path") - local human_size=$(bytes_to_human "$size") + local dirname + dirname=$(basename "$path") + local human_size + human_size=$(bytes_to_human "$size") # Build compact bar local bar="" if [[ $bar_width -gt 0 ]]; then bar=$(printf "%${bar_width}s" "" | tr ' ' 'β–“') fi - local empty=$((chart_width - bar_width)) + local empty + empty=$((chart_width - bar_width)) if [[ $empty -gt 0 ]]; then bar="${bar}$(printf "%${empty}s" "" | tr ' ' 'β–‘')" fi @@ -1009,12 +1074,16 @@ display_space_chart() { break fi - local percentage=$((size * 100 / total_size)) - local bar_width=$((percentage * chart_width / 100)) + local percentage + percentage=$((size * 100 / total_size)) + local bar_width + bar_width=$((percentage * chart_width / 100)) [[ $bar_width -lt 1 ]] && bar_width=1 - local dirname=$(basename "$path") - local human_size=$(bytes_to_human "$size") + local dirname + dirname=$(basename "$path") + local human_size + human_size=$(bytes_to_human "$size") # Build visual bar local bar="" @@ -1048,8 +1117,10 @@ display_recent_large_files() { "kMDItemFSSize > 100000000 && kMDItemContentCreationDate >= \$time.today(-30)" 2>/dev/null | \ while IFS= read -r file; do if [[ -f "$file" ]]; then - local size=$(stat -f%z "$file" 2>/dev/null || echo "0") - local mtime=$(stat -f%m "$file" 2>/dev/null || echo "0") + local size + size=$(stat -f%z "$file" 2>/dev/null || echo "0") + local mtime + mtime=$(stat -f%m "$file" 2>/dev/null || echo "0") echo "$size|$mtime|$file" fi done | sort -t'|' -k1 -rn | head -10 > "$temp_recent" @@ -1062,12 +1133,17 @@ display_recent_large_files() { local count=0 while IFS='|' read -r size mtime path; do - local human_size=$(bytes_to_human "$size") - local filename=$(basename "$path") - local dirname=$(dirname "$path" | sed "s|^$HOME|~|") - local days_ago=$(( ($(date +%s) - mtime) / 86400 )) + local human_size + human_size=$(bytes_to_human "$size") + local filename + filename=$(basename "$path") + local dirname + dirname=$(dirname "$path" | sed "s|^$HOME|~|") + local days_ago + days_ago=$(( ($(date +%s) - mtime) / 86400 )) - local info=$(get_file_info "$path") + local info + info=$(get_file_info "$path") local badge="${info%|*}" printf " %s %s ${GRAY}(%s)${NC}\n" "$badge" "$filename" "$human_size" @@ -1088,7 +1164,8 @@ get_subdirectories() { find "$target" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | \ while IFS= read -r dir; do - local size=$(du -sk "$dir" 2>/dev/null | cut -f1) + local size + size=$(du -sk "$dir" 2>/dev/null | cut -f1) echo "$((size * 1024))|$dir" done | sort -t'|' -k1 -rn > "$temp_file" } @@ -1116,11 +1193,16 @@ display_directory_list() { # Display with cursor while IFS='|' read -r size path; do - local human_size=$(bytes_to_human "$size") - local percentage=$(calc_percentage "$size" "$total_size") - local bar=$(generate_bar "$size" "$max_size" 20) - local display_path=$(echo "$path" | sed "s|^$HOME|~|") - local dirname=$(basename "$path") + local human_size + human_size=$(bytes_to_human "$size") + local percentage + percentage=$(calc_percentage "$size" "$total_size") + local bar + bar=$(generate_bar "$size" "$max_size" 20) + local display_path + display_path=$(echo "$path" | sed "s|^$HOME|~|") + local dirname + dirname=$(basename "$path") # Highlight selected line if [[ $idx -eq $cursor_pos ]]; then @@ -1168,7 +1250,8 @@ count_directories() { echo "0" return fi - local count=$(wc -l < "$temp_dirs" | tr -d ' ') + local count + count=$(wc -l < "$temp_dirs" | tr -d ' ') [[ $count -gt 15 ]] && count=15 echo "$count" } @@ -1252,20 +1335,24 @@ display_file_types() { ;; esac - local files=$(mdfind -onlyin "$CURRENT_PATH" "$query" 2>/dev/null) - local count=$(echo "$files" | grep -c . || echo "0") + local files + files=$(mdfind -onlyin "$CURRENT_PATH" "$query" 2>/dev/null) + local count + count=$(echo "$files" | grep -c . || echo "0") local total_size=0 if [[ $count -gt 0 ]]; then while IFS= read -r file; do if [[ -f "$file" ]]; then - local fsize=$(stat -f%z "$file" 2>/dev/null || echo "0") + local fsize + fsize=$(stat -f%z "$file" 2>/dev/null || echo "0") ((total_size += fsize)) fi done <<< "$files" if [[ $total_size -gt 0 ]]; then - local human_size=$(bytes_to_human "$total_size") + local human_size + human_size=$(bytes_to_human "$total_size") printf " %s %-12s %8s (%d files)\n" "$badge" "$type_name:" "$human_size" "$count" fi fi @@ -1292,7 +1379,8 @@ scan_directory_contents_fast() { local show_progress="${4:-true}" # Auto-detect optimal parallel jobs using common function - local num_jobs=$(get_optimal_parallel_jobs "io") + local num_jobs + num_jobs=$(get_optimal_parallel_jobs "io") # Cap at reasonable limits for I/O operations [[ $num_jobs -gt 24 ]] && num_jobs=24 [[ $num_jobs -lt 12 ]] && num_jobs=12 @@ -1456,7 +1544,8 @@ combine_initial_scan_results() { if [[ -f "$temp_large" ]]; then while IFS='|' read -r size path; do # Only include if parent directory is the current scan path - local parent=$(dirname "$path") + local parent + parent=$(dirname "$path") if [[ "$parent" == "$CURRENT_PATH" ]]; then echo "$size|file|$path" fi @@ -1484,7 +1573,8 @@ show_volumes_overview() { if [[ -d "/Volumes" ]]; then local vol_priority=500 find /Volumes -mindepth 1 -maxdepth 1 -type d 2>/dev/null | while IFS= read -r vol; do - local vol_name=$(basename "$vol") + local vol_name + vol_name=$(basename "$vol") echo "$((vol_priority))|$vol|Volume: $vol_name" ((vol_priority--)) done @@ -1505,7 +1595,8 @@ show_volumes_overview() { stty -echo 2>/dev/null || true local cursor=0 - local total_items=$(wc -l < "$temp_volumes" | tr -d ' ') + local total_items + total_items=$(wc -l < "$temp_volumes" | tr -d ' ') while true; do # Ensure cursor is always hidden @@ -1655,7 +1746,8 @@ interactive_drill_down() { # Only scan if needed (directory changed or refresh requested) if [[ "$need_scan" == "true" ]]; then # Generate cache key (use md5 hash of path) - local cache_key=$(echo "$current_path" | md5 2>/dev/null || echo "$current_path" | shasum | cut -d' ' -f1) + local cache_key + cache_key=$(echo "$current_path" | md5 2>/dev/null || echo "$current_path" | shasum | cut -d' ' -f1) local cache_file="$cache_dir/$cache_key" # Check if we have cached results for this directory @@ -1732,7 +1824,8 @@ interactive_drill_down() { if [[ ${#path_stack[@]} -gt 0 ]]; then # Use bash 3.2 compatible way to get last element local stack_size=${#path_stack[@]} - local last_index=$((stack_size - 1)) + local last_index + last_index=$((stack_size - 1)) current_path="${path_stack[$last_index]}" unset "path_stack[$last_index]" cursor=0 @@ -1757,7 +1850,8 @@ interactive_drill_down() { local max_show=15 # Show 15 items per page local page_start=$scroll_offset - local page_end=$((scroll_offset + max_show)) + local page_end + page_end=$((scroll_offset + max_show)) [[ $page_end -gt $total_items ]] && page_end=$total_items local display_idx=0 @@ -1778,7 +1872,8 @@ interactive_drill_down() { local rest="${item_info#*|}" local type="${rest%%|*}" local path="${rest#*|}" - local name=$(basename "$path") + local name + name=$(basename "$path") local human_size if [[ "$size" -eq 0 ]]; then @@ -1796,7 +1891,8 @@ interactive_drill_down() { fi else local ext="${name##*.}" - local info=$(get_file_info "$path") + local info + info=$(get_file_info "$path") badge="${info%|*}" case "$ext" in dmg|iso|pkg|zip|tar|gz|rar|7z) @@ -1871,7 +1967,8 @@ interactive_drill_down() { if [[ $cursor -lt $((total_items - 1)) ]]; then ((cursor++)) # Scroll down if cursor goes below visible area - local page_end=$((scroll_offset + max_show)) + local page_end + page_end=$((scroll_offset + max_show)) if [[ $cursor -ge $page_end ]]; then scroll_offset=$((cursor - max_show + 1)) fi @@ -1895,7 +1992,8 @@ interactive_drill_down() { else # It's a file - open it for viewing local file_ext="${selected_path##*.}" - local filename=$(basename "$selected_path") + local filename + filename=$(basename "$selected_path") local open_success=false # For text-like files, use less or fallback to open @@ -1972,7 +2070,8 @@ interactive_drill_down() { # Pop from stack and go back # Use bash 3.2 compatible way to get last element local stack_size=${#path_stack[@]} - local last_index=$((stack_size - 1)) + local last_index + last_index=$((stack_size - 1)) current_path="${path_stack[$last_index]}" unset "path_stack[$last_index]" cursor=0 @@ -2008,8 +2107,10 @@ interactive_drill_down() { local rest="${selected#*|}" local type="${rest%%|*}" local selected_path="${rest#*|}" - local selected_name=$(basename "$selected_path") - local human_size=$(bytes_to_human "$size") + local selected_name + selected_name=$(basename "$selected_path") + local human_size + human_size=$(bytes_to_human "$size") # Check if sudo is needed local needs_sudo=false @@ -2034,7 +2135,8 @@ interactive_drill_down() { if [[ "$type" == "dir" ]]; then echo " ${BADGE_DIR} ${YELLOW}$selected_name${NC}" else - local info=$(get_file_info "$selected_path") + local info + info=$(get_file_info "$selected_path") local badge="${info%|*}" echo " $badge ${YELLOW}$selected_name${NC}" fi @@ -2092,7 +2194,8 @@ interactive_drill_down() { sleep 0.8 # Clear cache to force rescan - local cache_key=$(echo "$current_path" | md5 2>/dev/null || echo "$current_path" | shasum | cut -d' ' -f1) + local cache_key + cache_key=$(echo "$current_path" | md5 2>/dev/null || echo "$current_path" | shasum | cut -d' ' -f1) local cache_file="$cache_dir/$cache_key" rm -f "$cache_file" 2>/dev/null || true @@ -2142,7 +2245,8 @@ interactive_mode() { type drain_pending_input >/dev/null 2>&1 && drain_pending_input display_interactive_menu - local key=$(read_key) + local key + key=$(read_key) case "$key" in "QUIT") break @@ -2154,14 +2258,16 @@ interactive_mode() { ;; "DOWN") if [[ "$VIEW_MODE" == "navigate" ]]; then - local max_count=$(count_directories) + local max_count + max_count=$(count_directories) ((CURSOR_POS < max_count - 1)) && ((CURSOR_POS++)) fi ;; "RIGHT") if [[ "$VIEW_MODE" == "navigate" ]]; then # Enter selected directory - local selected_path=$(get_path_at_cursor "$CURSOR_POS") + local selected_path + selected_path=$(get_path_at_cursor "$CURSOR_POS") if [[ -n "$selected_path" ]] && [[ -d "$selected_path" ]]; then CURRENT_PATH="$selected_path" CURSOR_POS=0 @@ -2194,7 +2300,8 @@ interactive_mode() { "ENTER") if [[ "$VIEW_MODE" == "navigate" ]]; then # Same as RIGHT - local selected_path=$(get_path_at_cursor "$CURSOR_POS") + local selected_path + selected_path=$(get_path_at_cursor "$CURSOR_POS") if [[ -n "$selected_path" ]] && [[ -d "$selected_path" ]]; then CURRENT_PATH="$selected_path" CURSOR_POS=0 @@ -2231,7 +2338,8 @@ export_to_csv() { { echo "Size (Bytes),Size (Human),Path" while IFS='|' read -r size path; do - local human=$(bytes_to_human "$size") + local human + human=$(bytes_to_human "$size") echo "$size,\"$human\",\"$path\"" done < "$temp_dirs" } > "$output_file" @@ -2260,7 +2368,8 @@ export_to_json() { while IFS='|' read -r size path; do [[ "$first" == "false" ]] && echo "," first=false - local human=$(bytes_to_human "$size") + local human + human=$(bytes_to_human "$size") printf ' {"size": %d, "size_human": "%s", "path": "%s"}' "$size" "$human" "$path" done < "$temp_dirs" @@ -2273,7 +2382,8 @@ export_to_json() { while IFS='|' read -r size path; do [[ "$first" == "false" ]] && echo "," first=false - local human=$(bytes_to_human "$size") + local human + human=$(bytes_to_human "$size") printf ' {"size": %d, "size_human": "%s", "path": "%s"}' "$size" "$human" "$path" done < "$temp_large" echo "" diff --git a/bin/clean.sh b/bin/clean.sh index 4b88f96..bbaa2ad 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -211,7 +211,9 @@ safe_clean() { description="$1" targets=("$1") else - description="${@: -1}" + # Get last argument as description + description="${*: -1}" + # Get all arguments except last as targets array targets=("${@:1:$#-1}") fi diff --git a/bin/uninstall.sh b/bin/uninstall.sh index 30aed56..90da6ac 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -67,13 +67,16 @@ total_size_cleaned=0 # Get app last used date in human readable format get_app_last_used() { local app_path="$1" - local last_used=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null) + local last_used + last_used=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null) if [[ "$last_used" == "(null)" || -z "$last_used" ]]; then echo "Never" else - local last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$last_used" "+%s" 2>/dev/null) - local current_epoch=$(date "+%s") + local last_used_epoch + last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$last_used" "+%s" 2>/dev/null) + local current_epoch + current_epoch=$(date "+%s") local days_ago=$(( (current_epoch - last_used_epoch) / 86400 )) if [[ $days_ago -eq 0 ]]; then @@ -103,7 +106,8 @@ scan_applications() { mkdir -p "$cache_dir" 2>/dev/null # Quick count of current apps (system + user directories) - local current_app_count=$( + local current_app_count + current_app_count=$( (find /Applications -name "*.app" -maxdepth 1 2>/dev/null; find ~/Applications -name "*.app" -maxdepth 1 2>/dev/null) | wc -l | tr -d ' ' ) @@ -111,7 +115,8 @@ scan_applications() { # Check if cache is valid unless explicitly disabled if [[ -f "$cache_file" && -f "$cache_meta" ]]; then local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0))) - local cached_app_count=$(cat "$cache_meta" 2>/dev/null || echo "0") + local cached_app_count + cached_app_count=$(cat "$cache_meta" 2>/dev/null || echo "0") # Cache is valid if: age < TTL AND app count matches if [[ $cache_age -lt $cache_ttl && "$cached_app_count" == "$current_app_count" ]]; then @@ -121,10 +126,12 @@ scan_applications() { fi fi - local temp_file=$(create_temp_file) + local temp_file + temp_file=$(create_temp_file) # Pre-cache current epoch to avoid repeated calls - local current_epoch=$(date "+%s") + local current_epoch + current_epoch=$(date "+%s") # Spinner for scanning feedback (simple ASCII for compatibility) local spinner_chars="|/-\\" @@ -135,7 +142,8 @@ scan_applications() { while IFS= read -r -d '' app_path; do if [[ ! -e "$app_path" ]]; then continue; fi - local app_name=$(basename "$app_path" .app) + local app_name + app_name=$(basename "$app_path" .app) # Try to get English name from bundle info, fallback to folder name local bundle_id="unknown" @@ -144,14 +152,17 @@ scan_applications() { bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo "unknown") # Try to get English name from bundle info - local bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2>/dev/null) + local bundle_executable + bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2>/dev/null) # Smart display name selection - prefer descriptive names over generic ones local candidates=() # Get all potential names - local bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2>/dev/null) - local bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2>/dev/null) + local bundle_display_name + bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2>/dev/null) + local bundle_name + bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2>/dev/null) # Check if executable name is generic/technical (should be avoided) local is_generic_executable=false @@ -242,7 +253,8 @@ scan_applications() { local last_used_epoch=0 if [[ -d "$app_path" ]]; then - local metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null) + local metadata_date + metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null) if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$metadata_date" "+%s" 2>/dev/null || echo "0") @@ -417,15 +429,20 @@ uninstall_applications() { fi # Find related files (user-level) - local related_files=$(find_app_files "$bundle_id" "$app_name") + local related_files + related_files=$(find_app_files "$bundle_id" "$app_name") # Find system-level files (requires sudo) - local system_files=$(find_app_system_files "$bundle_id" "$app_name") + local system_files + system_files=$(find_app_system_files "$bundle_id" "$app_name") # Calculate total size - local app_size_kb=$(du -sk "$app_path" 2>/dev/null | awk '{print $1}' || echo "0") - local related_size_kb=$(calculate_total_size "$related_files") - local system_size_kb=$(calculate_total_size "$system_files") + local app_size_kb + app_size_kb=$(du -sk "$app_path" 2>/dev/null | awk '{print $1}' || echo "0") + local related_size_kb + related_size_kb=$(calculate_total_size "$related_files") + local system_size_kb + system_size_kb=$(calculate_total_size "$system_files") local total_kb=$((app_size_kb + related_size_kb + system_size_kb)) # Show what will be removed @@ -619,20 +636,27 @@ main() { if [[ $selection_count -eq 0 ]]; then echo "No apps selected"; rm -f "$apps_file"; return 0 fi - # Compact one-line summary (list up to 3 names, aggregate rest) - local names=() + # Show selected apps, max 3 per line + echo -e "${BLUE}${ICON_CONFIRM}${NC} Selected ${selection_count} app(s):" local idx=0 + local line="" for selected_app in "${selected_apps[@]}"; do IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app" - if (( idx < 3 )); then - names+=("${app_name}(${size})") + local display_item="${app_name}(${size})" + + if (( idx % 3 == 0 )); then + # Start new line + [[ -n "$line" ]] && echo " $line" + line="$display_item" + else + # Add to current line + line="$line, $display_item" fi ((idx++)) done - local extra=$((selection_count-3)) - local list="${names[*]}" - [[ $extra -gt 0 ]] && list+=" +${extra}" - echo -e "${BLUE}${ICON_CONFIRM}${NC} ${selection_count} apps: ${list}" + # Print the last line + [[ -n "$line" ]] && echo " $line" + echo "" # Execute batch uninstallation (handles confirmation) batch_uninstall_applications diff --git a/lib/batch_uninstall.sh b/lib/batch_uninstall.sh index b4cc0f3..31398e6 100755 --- a/lib/batch_uninstall.sh +++ b/lib/batch_uninstall.sh @@ -192,17 +192,42 @@ batch_uninstall_applications() { if [[ -n "$freed_display" ]]; then success_line+=", freed ${GREEN}${freed_display}${NC}" fi + + # Format app list with max 3 per line if [[ -n "$success_list" ]]; then - local -a formatted_apps=() + local idx=0 + local is_first_line=true + local current_line="" + for app_name in "${success_items[@]}"; do - formatted_apps+=("${GREEN}${app_name}${NC}") + local display_item="${GREEN}${app_name}${NC}" + + if (( idx % 3 == 0 )); then + # Start new line + if [[ -n "$current_line" ]]; then + summary_details+=("$current_line") + fi + if [[ "$is_first_line" == true ]]; then + # First line: append to success_line + current_line="${success_line}: $display_item" + is_first_line=false + else + # Subsequent lines: just the apps + current_line="$display_item" + fi + else + # Add to current line + current_line="$current_line, $display_item" + fi + ((idx++)) done - if [[ ${#formatted_apps[@]} -gt 0 ]]; then - local IFS=', ' - success_line+=": ${formatted_apps[*]}" + # Add the last line + if [[ -n "$current_line" ]]; then + summary_details+=("$current_line") fi + else + summary_details+=("$success_line") fi - summary_details+=("$success_line") fi if [[ $failed_count -gt 0 ]]; then diff --git a/lib/paginated_menu.sh b/lib/paginated_menu.sh index 112e864..6cdcf88 100755 --- a/lib/paginated_menu.sh +++ b/lib/paginated_menu.sh @@ -262,14 +262,8 @@ EOF fi done - if [[ ${#selected_indices[@]} -eq 0 ]]; then - local default_idx=$((top_index + cursor_pos)) - if [[ $default_idx -ge 0 && $default_idx -lt $total_items ]]; then - selected[default_idx]=true - selected_indices=("$default_idx") - fi - fi - + # Allow empty selection - don't auto-select cursor position + # This fixes the bug where unselecting all items would still select the last cursor position local final_result="" if [[ ${#selected_indices[@]} -gt 0 ]]; then local IFS=',' diff --git a/mole b/mole index 24ff7b9..d3099ed 100755 --- a/mole +++ b/mole @@ -47,7 +47,8 @@ check_for_updates() { # Background version check (save to file, don't output) ( - local latest=$(get_latest_version) + local latest + latest=$(get_latest_version) 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" @@ -155,7 +156,8 @@ update_mole() { fi # Check for updates - local latest=$(get_latest_version) + local latest + latest=$(get_latest_version) if [[ -z "$latest" ]]; then log_error "Unable to check for updates. Check network connection." @@ -449,9 +451,11 @@ show_main_menu() { interactive_main_menu() { # Show intro animation only once per terminal tab if [[ -t 1 ]]; then - local tty_name=$(tty 2>/dev/null || echo "") + local tty_name + tty_name=$(tty 2>/dev/null || echo "") if [[ -n "$tty_name" ]]; then - local flag_file="/tmp/mole_intro_$(echo "$tty_name" | tr -c '[:alnum:]_' '_')" + local flag_file + flag_file="/tmp/mole_intro_$(echo "$tty_name" | tr -c '[:alnum:]_' '_')" if [[ ! -f "$flag_file" ]]; then animate_mole_intro touch "$flag_file" 2>/dev/null || true @@ -489,8 +493,10 @@ interactive_main_menu() { # Drain any pending input to prevent touchpad scroll issues drain_pending_input - local key=$(read_key) - [[ $? -ne 0 ]] && continue + local key + if ! key=$(read_key); then + continue + fi case "$key" in "UP") ((current_option > 1)) && ((current_option--)) ;; diff --git a/tests/helpers/uninstall_stubs.sh b/tests/helpers/uninstall_stubs.sh deleted file mode 100644 index 7689c6e..0000000 --- a/tests/helpers/uninstall_stubs.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2329 -# Helper stub definitions for uninstall tests - -setup_uninstall_stubs() { - request_sudo_access() { return 0; } - start_inline_spinner() { :; } - stop_inline_spinner() { :; } - enter_alt_screen() { :; } - leave_alt_screen() { :; } - hide_cursor() { :; } - show_cursor() { :; } - remove_apps_from_dock() { :; } - - pgrep() { return 1; } - pkill() { return 0; } - sudo() { return 0; } - - export -f request_sudo_access start_inline_spinner stop_inline_spinner \ - enter_alt_screen leave_alt_screen hide_cursor show_cursor \ - remove_apps_from_dock pgrep pkill sudo || true -} diff --git a/tests/run.sh b/tests/run.sh index 580a778..7589c19 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -4,26 +4,41 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -if ! command -v bats >/dev/null 2>&1; then +if command -v shellcheck >/dev/null 2>&1; then + SHELLCHECK_TARGETS=() + while IFS= read -r file; do + SHELLCHECK_TARGETS+=("$file") + done < <(find "$PROJECT_ROOT/tests" -type f \( -name '*.bats' -o -name '*.sh' \) | sort) + + if [[ ${#SHELLCHECK_TARGETS[@]} -gt 0 ]]; then + shellcheck --rcfile "$PROJECT_ROOT/.shellcheckrc" "${SHELLCHECK_TARGETS[@]}" + else + echo "No shell files to lint under tests/." >&2 + fi +else + echo "shellcheck not found; skipping linting." >&2 +fi + +if command -v bats >/dev/null 2>&1; then + cd "$PROJECT_ROOT" + + if [[ -z "${TERM:-}" ]]; then + export TERM="xterm-256color" + fi + + if [[ $# -eq 0 ]]; then + set -- tests + fi + + if [[ -t 1 ]]; then + bats -p "$@" + else + TERM="${TERM:-xterm-256color}" bats --tap "$@" + fi +else cat <<'EOF' >&2 bats is required to run Mole's test suite. Install via Homebrew with 'brew install bats-core' or via npm with 'npm install -g bats'. EOF exit 1 fi - -cd "$PROJECT_ROOT" - -if [[ -z "${TERM:-}" ]]; then - export TERM="xterm-256color" -fi - -if [[ $# -eq 0 ]]; then - set -- tests -fi - -if [[ -t 1 ]]; then - bats -p "$@" -else - TERM="${TERM:-xterm-256color}" bats --tap "$@" -fi diff --git a/tests/uninstall.bats b/tests/uninstall.bats index 47d7eee..6edbc39 100644 --- a/tests/uninstall.bats +++ b/tests/uninstall.bats @@ -77,8 +77,19 @@ EOF set -euo pipefail source "$PROJECT_ROOT/lib/common.sh" source "$PROJECT_ROOT/lib/batch_uninstall.sh" -source "$PROJECT_ROOT/tests/helpers/uninstall_stubs.sh" -setup_uninstall_stubs + +# Test stubs +request_sudo_access() { return 0; } +start_inline_spinner() { :; } +stop_inline_spinner() { :; } +enter_alt_screen() { :; } +leave_alt_screen() { :; } +hide_cursor() { :; } +show_cursor() { :; } +remove_apps_from_dock() { :; } +pgrep() { return 1; } +pkill() { return 0; } +sudo() { return 0; } app_bundle="$HOME/Applications/TestApp.app" mkdir -p "$app_bundle" diff --git a/tests/z_shellcheck.bats b/tests/z_shellcheck.bats new file mode 100644 index 0000000..11253c5 --- /dev/null +++ b/tests/z_shellcheck.bats @@ -0,0 +1,29 @@ +#!/usr/bin/env bats + +setup_file() { + PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + export PROJECT_ROOT +} + +@test "shellcheck passes for test scripts" { + if ! command -v shellcheck >/dev/null 2>&1; then + skip "shellcheck not installed" + fi + + run env PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' +set -euo pipefail +cd "$PROJECT_ROOT" +targets=() +while IFS= read -r file; do + targets+=("$file") +done < <(find "$PROJECT_ROOT/tests" -type f \( -name '*.bats' -o -name '*.sh' \) | sort) +if [[ ${#targets[@]} -eq 0 ]]; then + echo "No test shell files found" + exit 0 +fi +shellcheck --rcfile "$PROJECT_ROOT/.shellcheckrc" "${targets[@]}" +EOF + + printf '%s\n' "$output" >&3 + [ "$status" -eq 0 ] +}