Last active
July 11, 2025 11:03
-
-
Save yayanet/b6ee9c22ca6a2eedfd95436d1f6ef20b to your computer and use it in GitHub Desktop.
list and clean expired provisioning profiles on macOS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # ----------------------------------------------------------------------------- | |
| # Function: Scan and manage provisioning profiles from system and Xcode paths. | |
| # Version: 4.1 (Fixes bug in file list processing from v4.0) | |
| # | |
| # Usage: | |
| # ./cleanup_profiles.sh -l - List the status (Valid/Expired) of all profiles. | |
| # ./cleanup_profiles.sh -d - Interactively move all expired profiles to Trash. | |
| # ----------------------------------------------------------------------------- | |
| # --- Configuration --- | |
| COLOR_RED='\033[0;31m' | |
| COLOR_GREEN='\033[0;32m' | |
| COLOR_YELLOW='\033[1;33m' | |
| COLOR_CYAN='\033[0;36m' | |
| COLOR_NC='\033[0m' | |
| # Global arrays to store information about expired files for the delete operation. | |
| declare -a expired_files | |
| declare -a expired_file_displays | |
| # --- Function Definitions --- | |
| # Function: Display usage instructions. | |
| function show_usage() { | |
| echo "Usage: $0 [OPTION]" | |
| echo | |
| echo "Options:" | |
| echo " -l, --list Scan and list the status of all provisioning profiles." | |
| echo " -d, --delete Scan all profiles and start an interactive flow to delete expired ones." | |
| echo | |
| } | |
| # Function: Core logic to scan a given path and display results. | |
| # Arguments: | |
| # $1: The file path to scan for profiles. | |
| # $2: A descriptive name for the path being scanned (for display). | |
| function scan_and_display_profiles() { | |
| local target_path="$1" | |
| local path_description="$2" | |
| local currentTimestamp=$(date "+%s") | |
| # Check if the directory exists before scanning | |
| if [ ! -d "$target_path" ]; then | |
| echo -e "${COLOR_YELLOW}Warning: Directory not found, skipping scan:${COLOR_NC} $target_path" | |
| echo | |
| return | |
| fi | |
| echo -e "${COLOR_CYAN}--- Scanning: $path_description ---${COLOR_NC}" | |
| echo | |
| # Temporary arrays to store file info for sorting. | |
| declare -a all_profiles | |
| declare -a all_statuses | |
| declare -a all_dates | |
| declare -a all_files | |
| declare -a all_displays | |
| local profile_found=0 | |
| # **BUG FIX**: Reverted to direct process substitution for find. | |
| # This correctly handles NULL-delimited filenames with special characters. | |
| while IFS= read -r -d $'\0' file; do | |
| profile_found=1 | |
| local filename=$(basename "$file") | |
| local plist_content=$(security cms -D -i "$file" 2>/dev/null) | |
| if [ -z "$plist_content" ]; then | |
| printf "${COLOR_RED}[Parse Error]${COLOR_NC} - Could not decode file: %s\n" "$filename" | |
| continue | |
| fi | |
| local profileName=$(echo "$plist_content" | grep -A1 '<key>Name</key>' | tail -n1 | sed -E 's/.*<string>(.*)<\/string>.*/\1/') | |
| local expirationDateString=$(echo "$plist_content" | grep -A1 ExpirationDate | tail -n1 | sed -E 's/.*<date>(.*)<\/date>.*/\1/') | |
| if [ -n "$expirationDateString" ]; then | |
| local expirationTimestamp=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$expirationDateString" "+%s" 2>/dev/null) | |
| if [ $? -eq 0 ]; then | |
| local formattedExpirationDate=$(date -j -f "%s" "$expirationTimestamp" "+%Y-%m-%d") | |
| local display_info | |
| if [ -n "$profileName" ]; then | |
| display_info="$profileName (${filename})" | |
| else | |
| display_info="$filename" | |
| profileName="$filename" # Use filename for sorting if name is missing | |
| fi | |
| # Store in temporary arrays for sorting | |
| all_profiles+=("$profileName") | |
| all_files+=("$file") | |
| all_displays+=("$display_info") | |
| all_dates+=("$formattedExpirationDate") | |
| if [ "$expirationTimestamp" -lt "$currentTimestamp" ]; then | |
| all_statuses+=("expired") | |
| else | |
| all_statuses+=("valid") | |
| fi | |
| else | |
| printf "${COLOR_RED}[Date Error]${COLOR_NC} - %-80s\n" "$profileName ($filename)" | |
| fi | |
| else | |
| local uuid=$(echo "$plist_content" | grep -A1 UUID | tail -n1 | sed -E 's/.*<string>(.*)<\/string>.*/\1/') | |
| printf "${COLOR_RED}[File Error]${COLOR_NC} - UUID: %s (%s)\n" "$uuid" "$filename" | |
| fi | |
| done < <(find "$target_path" \( -name "*.mobileprovision" -o -name "*.provisionprofile" \) -print0 2>/dev/null) | |
| if [ "$profile_found" -eq 0 ]; then | |
| echo "No profiles found in this directory." | |
| echo | |
| return | |
| fi | |
| # Create an index array for sorting | |
| declare -a indices | |
| for i in "${!all_profiles[@]}"; do | |
| indices+=("$i") | |
| done | |
| # Sort indices based on profile name | |
| IFS=$'\n' sorted_indices=($( | |
| for i in "${indices[@]}"; do | |
| # Use a tab character to safely separate name from index | |
| printf '%s\t%s\n' "${all_profiles[$i]}" "$i" | |
| done | sort -f -k1,1 | cut -f2 | |
| )) | |
| unset IFS | |
| # Print results in sorted order | |
| for i in "${sorted_indices[@]}"; do | |
| local status="${all_statuses[$i]}" | |
| local display_info="${all_displays[$i]}" | |
| local date="${all_dates[$i]}" | |
| local file_path="${all_files[$i]}" | |
| if [ "$status" = "expired" ]; then | |
| printf "${COLOR_RED}[Expired]${COLOR_NC} - %-80s ${COLOR_YELLOW}Expiration Date: %s${COLOR_NC}\n" "$display_info" "$date" | |
| # Add to global array for deletion | |
| expired_files+=("$file_path") | |
| expired_file_displays+=("$display_info") | |
| else | |
| printf "${COLOR_GREEN}[ Valid ]${COLOR_NC} - %-80s ${COLOR_YELLOW}Expiration Date: %s${COLOR_NC}\n" "$display_info" "$date" | |
| fi | |
| done | |
| echo | |
| } | |
| # Function: High-level wrapper to scan all configured locations. | |
| function main_scan_logic() { | |
| local currentTimestamp=$(date "+%s") | |
| echo -e "${COLOR_CYAN}==========================================================================================${COLOR_NC}" | |
| echo -e "${COLOR_CYAN}Scanning Provisioning Profiles...${COLOR_NC}" | |
| echo -e "${COLOR_CYAN}Current Date: $(date -j -f "%s" "$currentTimestamp" "+%Y-%m-%d")${COLOR_NC}" | |
| echo -e "${COLOR_CYAN}==========================================================================================${COLOR_NC}" | |
| echo | |
| # Scan the primary system path | |
| scan_and_display_profiles "$HOME/Library/MobileDevice/Provisioning Profiles" "System Profiles (~/Library/MobileDevice/Provisioning Profiles)" | |
| # Scan the Xcode user data path | |
| scan_and_display_profiles "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" "Xcode User-Data Profiles (~/Library/Developer/Xcode/UserData/Provisioning Profiles)" | |
| echo -e "${COLOR_CYAN}==========================================================================================${COLOR_NC}" | |
| echo -e "${COLOR_CYAN}Scan Complete.${COLOR_NC}" | |
| } | |
| # Function: Execute the deletion flow. | |
| function perform_delete_flow() { | |
| echo | |
| if [ ${#expired_files[@]} -eq 0 ]; then | |
| echo -e "${COLOR_GREEN}🎉 Congratulations! No expired provisioning profiles were found.${COLOR_NC}" | |
| return | |
| fi | |
| echo -e "${COLOR_YELLOW}⚠️ Starting deletion process...${COLOR_NC}" | |
| echo "The following ${#expired_files[@]} expired profile(s) will be moved to the Trash:" | |
| echo "------------------------------------------------------------------" | |
| for i in "${!expired_files[@]}"; do | |
| echo -e " - ${COLOR_RED}${expired_file_displays[$i]}${COLOR_NC}" | |
| done | |
| echo "------------------------------------------------------------------" | |
| read -p "Are you sure you want to continue? (y/N): " -n 1 -r | |
| echo | |
| if [[ $REPLY =~ ^[Yy]$ ]]; then | |
| echo "Moving files to Trash..." | |
| for i in "${!expired_files[@]}"; do | |
| local file_to_move="${expired_files[$i]}" | |
| mv "$file_to_move" ~/.Trash/ | |
| echo " -> Moved: ${expired_file_displays[$i]}" | |
| done | |
| echo -e "${COLOR_GREEN}✅ Operation complete! All expired profiles have been moved to the Trash.${COLOR_NC}" | |
| else | |
| echo -e "${COLOR_YELLOW}Operation canceled. No files were changed.${COLOR_NC}" | |
| fi | |
| } | |
| # --- Main Logic --- | |
| # If no arguments are provided, show usage and exit. | |
| if [ $# -eq 0 ]; then | |
| show_usage | |
| exit 0 | |
| fi | |
| # Decide what to do based on the first argument. | |
| case "$1" in | |
| -l|--list) | |
| main_scan_logic | |
| ;; | |
| -d|--delete) | |
| main_scan_logic | |
| perform_delete_flow | |
| ;; | |
| *) | |
| echo -e "${COLOR_RED}Error: Unknown option '$1'${COLOR_NC}" | |
| echo | |
| show_usage | |
| exit 1 | |
| ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment