Created
October 8, 2024 08:55
-
-
Save KernelGhost/fd174694f0d2eb0c7879e3f5d0b62d75 to your computer and use it in GitHub Desktop.
Recursively delete Apple Double Files (ADFs) and '.DS_Store' files from a specified directory.
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
| #!/usr/bin/env bash | |
| #-------------------------------------------------------------------------------------------------------- | |
| # Name: RemoveAppleDoubleFiles.sh | |
| # Purpose: Recursively remove Apple Double Files (ADFs) and '.DS_Store' files from a specified directory. | |
| # Compatability: GNU/Linux & macOS | |
| # Usage: ./RemoveAppleDoubleFiles.sh [/path/to/dir] | |
| # Notes: | |
| # - ADFs have file names beginning with '._'. | |
| # - ADFs begin with the 'magic' sequence '0x00051607'. | |
| # - '.DS_Store' files commence with '0000 0001' followed by the 'magic' sequence '0x42756431' ("Bud1"). | |
| # Sources: | |
| # - https://nulib.com/library/AppleSingle_AppleDouble.pdf | |
| # - https://opensource.apple.com/source/copyfile/copyfile-42/copyfile.c | |
| # - https://0day.work/parsing-the-ds_store-file-format/ | |
| #-------------------------------------------------------------------------------------------------------- | |
| ################################## | |
| # GLOBAL VARIABLES AND CONSTANTS # | |
| ################################## | |
| ANSI_RED="\033[1;31m" | |
| ANSI_YELLOW="\033[1;33m" | |
| ANSI_GREEN="\033[1;32m" | |
| ANSI_CLEAR="\033[0m" | |
| readonly ANSI_RED | |
| readonly ANSI_YELLOW | |
| readonly ANSI_GREEN | |
| readonly ANSI_CLEAR | |
| appleDoubleFilePaths=() | |
| DSStoreFilePaths=() | |
| searchDirectory="" | |
| ############# | |
| # FUNCTIONS # | |
| ############# | |
| function check_dependencies() { | |
| if ! command -v xxd &>/dev/null; then | |
| echo -e "${ANSI_RED}[ERROR]${ANSI_CLEAR} Command 'xxd' missing!" | |
| echo "Please install 'xxd':" | |
| echo "Debian/Ubuntu" | |
| echo " sudo apt install xxd" | |
| echo "Fedora/RHEL" | |
| echo " sudo dnf install vim-common" | |
| echo "Exiting with status 2." | |
| exit 2 | |
| fi | |
| } | |
| function confirm_action() { | |
| local answer="" | |
| while true; do | |
| read -p "${1} (y/n): " answer | |
| answer=$(echo "$answer" | tr '[:upper:]' '[:lower:]') # Convert answer to lowercase. | |
| # Check if the answer is 'y' or 'n'. | |
| if [[ "$answer" == "y" ]]; then | |
| # Double check with the user. | |
| read -p "I am going to ask you again. Are you absolutely sure? (y/n): " answer | |
| answer=$(echo "$answer" | tr '[:upper:]' '[:lower:]') # Convert answer to lowercase. | |
| # Check if the answer is 'y' or 'n'. | |
| if [[ "$answer" == "y" ]]; then | |
| break | |
| elif [[ "$answer" == "n" ]]; then | |
| return 1 | |
| else | |
| echo -e "${ANSI_RED}[ERROR]${ANSI_CLEAR} Invalid response. Please answer with either 'y' or 'n'." | |
| fi | |
| elif [[ "$answer" == "n" ]]; then | |
| return 1 | |
| else | |
| echo -e "${ANSI_RED}[ERROR]${ANSI_CLEAR} Invalid response. Please answer with either 'y' or 'n'." | |
| fi | |
| done | |
| return 0 | |
| } | |
| # Check if 'xxd' is available. | |
| check_dependencies | |
| ######################## | |
| # SET SEARCH DIRECTORY # | |
| ######################## | |
| if [ -n "$1" ]; then | |
| # Check if the argument is a valid directory. | |
| if [ -d "$1" ]; then | |
| # Set searchDirectory to the provided argument. | |
| searchDirectory="$1" | |
| else | |
| echo -e "${ANSI_RED}[ERROR]${ANSI_CLEAR} '${1}' is not a valid directory!" | |
| echo "Exiting with status 1." | |
| exit 1 | |
| fi | |
| else | |
| # Set searchDirectory to the current working directory. | |
| searchDirectory=$(pwd) | |
| fi | |
| ###################### | |
| # APPLE DOUBLE FILES # | |
| ###################### | |
| # Use a while loop without a pipe to avoid running in a subshell. | |
| while IFS= read -r file; do | |
| # Header structure: | |
| # 1. Magic Number (4 Bytes) <-- Offset 0x00 (00) [EXAMPLE: '0005 1607'] | |
| # 2. Version Number (4 Bytes) <-- Offset 0x04 (04) [EXAMPLE: '0002 0000'] | |
| # 3. Filler (16 Bytes) <-- Offset 0x08 (08) [EXAMPLE: '4D61 6320 4F53 2058 2020 2020 2020 2020'] | |
| # 4. Number of Entries (2 Bytes) <-- Offset 0x18 (24) [EXAMPLE: '0002'] (Uint16) | |
| # Check if the first 4 bytes of the file match the magic hex sequence '0x00051607'. | |
| if [ "$(xxd -p -l 4 < "$file")" == "00051607" ]; then | |
| # Append the absolute file path to the array. | |
| appleDoubleFilePaths+=("$(realpath "$file")") | |
| fi | |
| done < <(find "$searchDirectory" -name '._*' -type f -print) | |
| if [ ${#appleDoubleFilePaths[@]} -ne 0 ]; then | |
| # List detected ADFs. | |
| echo -e "${ANSI_GREEN}DETECTED ADFs!${ANSI_CLEAR}" | |
| printf "%s\n" "${appleDoubleFilePaths[@]}" | |
| echo "" | |
| # Request user permission to delete ADFs. | |
| confirm_action "Proceed with deletion of the above files?" || { | |
| echo -e "${ANSI_YELLOW}[WARNING]${ANSI_CLEAR} Operation cancelled by user." | |
| exit 0 | |
| } | |
| # Delete ADFs. | |
| errors=0 | |
| echo "" | |
| for appleDoubleFile in "${appleDoubleFilePaths[@]}"; do | |
| echo -e -n "${ANSI_RED}-->${ANSI_CLEAR} Deleting '${appleDoubleFile}'... " | |
| # Attempt to delete the file. | |
| if rm -f "$appleDoubleFile"; then | |
| echo -e "${ANSI_GREEN}OK!${ANSI_CLEAR}" | |
| else | |
| echo -e "${ANSI_RED}FAILED!${ANSI_CLEAR}" | |
| ((errors++)) | |
| fi | |
| done | |
| if [ "$errors" -gt 0 ]; then | |
| echo "" | |
| echo -e "${ANSI_YELLOW}[WARNING]${ANSI_CLEAR} ${errors} file(s) were unable to be deleted!" | |
| fi | |
| echo "" | |
| fi | |
| ##################### | |
| # '.DS_Store' FILES # | |
| ##################### | |
| # Use a while loop without a pipe to avoid running in a subshell. | |
| while IFS= read -r file; do | |
| # Header structure: | |
| # 1. Alignment (4 Bytes) <-- Offset 0x00 (00) [ALWAYS: '0000 0001'] | |
| # 2. Magic Number (4 Bytes) <-- Offset 0x04 (04) [ALWAYS: '4275 6431'] | |
| # Check if 4 bytes at offset 0x4 match the magic hex sequence '0x42756431'. | |
| if [ "$(xxd -p -s 0x4 -l 4 < "$file")" == "42756431" ]; then | |
| # Append the absolute file path to the array. | |
| DSStoreFilePaths+=("$(realpath "$file")") | |
| fi | |
| done < <(find "$searchDirectory" -name '.DS_Store' -type f -print) | |
| if [ ${#DSStoreFilePaths[@]} -ne 0 ]; then | |
| # List detected '.DS_Store' files. | |
| echo -e "${ANSI_GREEN}DETECTED '.DS_Store' FILES!${ANSI_CLEAR}" | |
| printf "%s\n" "${DSStoreFilePaths[@]}" | |
| echo "" | |
| # Request user permission to delete '.DS_Store' files. | |
| confirm_action "Proceed with deletion of the above files?" || { | |
| echo -e "${ANSI_YELLOW}[WARNING]${ANSI_CLEAR} Operation cancelled by user." | |
| exit 0 | |
| } | |
| # Delete '.DS_Store' files. | |
| errors=0 | |
| echo "" | |
| for DSStoreFile in "${DSStoreFilePaths[@]}"; do | |
| echo -e -n "${ANSI_RED}-->${ANSI_CLEAR} Deleting '${DSStoreFile}'... " | |
| # Attempt to delete the file. | |
| if rm -f "$DSStoreFile"; then | |
| echo -e "${ANSI_GREEN}OK!${ANSI_CLEAR}" | |
| else | |
| echo -e "${ANSI_RED}FAILED!${ANSI_CLEAR}" | |
| ((errors++)) | |
| fi | |
| done | |
| if [ "$errors" -gt 0 ]; then | |
| echo "" | |
| echo -e "${ANSI_YELLOW}[WARNING]${ANSI_CLEAR} ${errors} file(s) were unable to be deleted!" | |
| fi | |
| echo "" | |
| fi | |
| # Exit. | |
| echo -e "${ANSI_GREEN}DONE!${ANSI_CLEAR}" | |
| exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment