Skip to content

Instantly share code, notes, and snippets.

@superkeyor
Last active January 6, 2024 00:58
Show Gist options
  • Save superkeyor/7d238245a28c899f0149890860c7c341 to your computer and use it in GitHub Desktop.
Save superkeyor/7d238245a28c899f0149890860c7c341 to your computer and use it in GitHub Desktop.
bash script to maintan configuration files in macOS (inspired by mackup)
cfgsync() {
# Usage: cfgsync {uplink|linkstore|backup|restore|reset} {directory_or_file} [-q|--quiet]
# cfgsync backup Clipboard # initialize backup, assuming config/ subfolder in backup
# cfgsync restore Clipboard/config/clipboard.cfg # restore to a new machine
#
# uplink: (backup first + link second) Backs up the original file/folder, according to .cfg, by copying it to a backup location and then creates a symbolic link to the backup in the original place.
# linkstore: (link to an existing backup to restore) Restores the original file by removing it (if it exists) and creating a symbolic link to the backup in the original place.
# backup: (backup, no link) Backs up the original file/folder, according to .cfg, by copying it to a backup location. No symbolic link is created.
# restore: (restore, no link) Similar to linkstore, but instead of creating a symbolic link, it copies the backup back to the original location.
# reset: Deletes the original file or symbolic link, with the hope that the application will recreate a default configuration file if necessary.
# -q or --quiet to skip confirmation
#
# cfg format (non-existing file will be skipped):
# https://github.com/lra/mackup/tree/master/mackup/applications (this function hard-codingly converts [xdg_configuration_files] to .config)
# [application]
# name = Bartender
#
# [configuration_files]
# # assuming $HOME/ prefix
# Library/Preferences/com.surteesstudios.Bartender.plist
# Library/Preferences/com.surteesstudios.Bartender-setapp.plist
# Library/Application Support/Bartender/Bartender.BartenderPreferences
# # if path starts with /, then no prefix will be added, use it as is
# /Library/Application Support
#
# [xdg_configuration_files]
# # assuming $HOME/.config/ prefix
# karabiner
local quiet=$3
prompt_for_confirmation() {
local message=$1
if [[ "$quiet" == "-q" || "$quiet" == "--quiet" ]]; then
echo "$message ([y]/n):" # Optionally display the message
return 0 # Automatically confirm
fi
read -p "$message ([y]/n): " response
response=${response:-y}
if [[ $response == [Nn]* ]]; then
echo "User cancelled."
return 1
fi
return 0
}
local mode=$1
local input_path=$2
local cfg_file="" # cfg_file in input_path
local cfg_dir="" # cfg_dir containing cfg_file
local plist_files=() # plist_files extracted from cfg_file
local line # loop cfg_file
local plist_original # loop plist_files
local plist_backup # backup plist_files saved in cfg_dir
# Determine whether the input is a directory or a specific file
if [ -d "$input_path" ]; then
# Remove trailing slash if it exists for input path
input_path="${input_path%/}"
cfg_dir="$(realpath ${input_path}/config)"
# Find the first .cfg file in the directory
cfg_file="$(find "$cfg_dir" -maxdepth 1 -type f -name '*.cfg' | head -n 1)"
if [ -z "$cfg_file" ]; then
echo "No .cfg file found in directory $cfg_dir."
return 1
fi
elif [ -f "$input_path" ]; then
cfg_file="$(realpath $input_path)"
cfg_dir="$(realpath $(dirname "$cfg_file"))"
else
# echo "Invalid input. Please provide a directory or a .cfg file."
echo "Usage: cfgsync {uplink|linkstore|backup|restore|reset} {directory_or_file}"
return 1
fi
mode_title_case=$(echo "$mode" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
prompt_for_confirmation "Found $cfg_file. $mode_title_case?"
# Parse the configuration file to extract file paths
parseCfg() {
local in_config_section=false
local in_xdg_section=false
while IFS= read -r line || [[ -n "$line" ]]; do
# Check if the current line is a section header
if [[ $line =~ ^\[.*\]$ ]]; then
if [[ $line == "[configuration_files]" ]]; then
in_config_section=true
else
in_config_section=false
fi
if [[ $line == "[xdg_configuration_files]" ]]; then
in_xdg_section=true
else
in_xdg_section=false
fi
continue
fi
# If we are in the configuration_files section, add non-empty and non-comment lines to the array
if [[ $in_config_section == true && -n $line && ! $line =~ ^[[:space:]]*# ]]; then
# /Library/Application Support/
if [[ $line == /* ]]; then
plist_files=("${plist_files[@]}" "${line}")
else
plist_files=("${plist_files[@]}" "$HOME/${line}")
fi
fi
if [[ $in_xdg_section == true && -n $line && ! $line =~ ^[[:space:]]*# ]]; then
plist_files=("${plist_files[@]}" "$HOME/.config/${line}")
fi
done < "$cfg_file"
}
parseCfg
# Apply operations on the extracted file paths
for plist_original in "${plist_files[@]}"; do
plist_backup="$cfg_dir/$(basename "$plist_original")"
case $mode in
uplink)
# skip if it has already been a soft link
if [ -L "$plist_original" ]; then
local linked_file=$(readlink "$plist_original")
if [ "$linked_file" = "$plist_backup" ]; then
echo "$plist_original is already a symbolic link to $plist_backup. No need to relink."
else
# red
echo -e "\033[31mCan not uplink! $plist_original is a symbolic link, but not pointing to $plist_backup.\033[0m"
fi
continue
fi
# .cfg may contain unnecessary files
if [ ! -e "$plist_original" ]; then
# grey
echo -e "\033[90mIgnored: $plist_original does not exist.\033[0m"
continue
fi
# remove previous backup, esp. needed if backup is a folder, in order for cp to work properly
rm -rf "$plist_backup"
# now we do cp, rm, ln
cp -r "$plist_original" "$plist_backup"
rm -r "$plist_original"
if [ -d "$plist_backup" ]; then
ln -sf "$plist_backup" "$(dirname "$plist_original")"
else
ln -sf "$plist_backup" "$plist_original"
fi
echo "Symbolic link created for backup of $plist_original."
;;
backup)
# skip if it is a soft link
if [ -L "$plist_original" ]; then
# red
echo -e "\033[31mCan not backup! $plist_original is just a symbolic link.\033[0m"
continue
fi
# .cfg may contain unnecessary files
if [ ! -e "$plist_original" ]; then
# grey
echo -e "\033[90mIgnored: $plist_original does not exist.\033[0m"
continue
fi
# remove previous backup, esp. needed if backup is a folder, in order for cp to work properly
rm -rf "$plist_backup"
# now we do cp
cp -r "$plist_original" "$plist_backup"
echo "Backup created for $plist_original."
;;
linkstore)
# Again, .cfg may contain unnecessary files (if no backup, no restore)
if [ ! -e "$plist_backup" ]; then
echo -e "\033[90mIgnored: $plist_backup does not exist.\033[0m"
continue
fi
# remove original file (could be a soft link)
rm -rf "$plist_original"
# create necessary parent folder if not there
mkdir -p "$(dirname "$plist_original")"
# at this point, plist_original should not exist, ln below would work properly esp. when it is a folder
if [ -d "$plist_backup" ]; then
ln -sf "$plist_backup" "$(dirname "$plist_original")"
else
ln -sf "$plist_backup" "$plist_original"
fi
echo "Configuration linkstored from backup for $plist_original."
;;
restore)
# restore is similar to linkstore, except for the last part
# Again, .cfg may contain unnecessary files
if [ ! -e "$plist_backup" ]; then
echo -e "\033[90mIgnored: $plist_backup does not exist.\033[0m"
continue
fi
# remove original file (could be a soft link)
rm -rf "$plist_original"
# create necessary parent folder if not there
mkdir -p "$(dirname "$plist_original")"
# the above are the same as restore
cp -r "$plist_backup" "$plist_original"
echo "Backup restored for $plist_original."
;;
reset)
rm -rf "$plist_original"
echo "Configuration reset for $plist_original."
# hopefully a default config file will be auto recreated by the app
# in case not, I have backups, hopefully already, in the .cfg folder
;;
*)
echo "Usage: cfgsync {uplink|linkstore|backup|restore|reset} {directory_or_file}"
return 1
;;
esac
done
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment