Skip to content

Instantly share code, notes, and snippets.

@KernelGhost
Created October 8, 2024 08:55
Show Gist options
  • Select an option

  • Save KernelGhost/fd174694f0d2eb0c7879e3f5d0b62d75 to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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