#!/usr/bin/env python3 """ Syncronizes dotfiles from a source dir that mimics a homedir tree to a homedir, removing old symlinks as necessary as well as optionally files. """ import os import pathlib FORCE = False HOMEDIR = os.path.expanduser("~") SOURCEDIR = os.path.dirname(os.path.join(os.path.realpath(__file__))) EXCLUSIONS = [ ".git", "setup_dotfiles.py", "README.md" ] def check_and_remove(source, destination, force=False): """ Checks if a file exists and removes it if appropriate. If it's a symlink we check it points the right place and remove it if not. If it's a file, we remove it if --force is provided, or error if not. If we need to re-deploy the symlink, return True, else return False. """ pathlib.Path(os.path.dirname(destination)).mkdir(parents=True, exist_ok=True) # Destination exists as a symlink if os.path.islink(destination): # Symlink is correct, return if os.readlink(destination) == source: return False # Symlink is incorrect, remove it os.unlink(destination) # Destination exists as a file elif os.path.exists(destination): # Force mode enabled, remove it (Data loss risk!!) if force: os.remove(destination) # Force mode disabled, error out else: raise OSError(destination + " exists as a file and --force not provided") # File doesn't exist or undefined "thing" happened, return True to try to # write, and if something goes wrong, we'll see the OSError return True def install_dotfiles(source_dir, dest_dir, exclusions, force=False): """ Iterates the files present in source_dir, excluding EXCLUSIONS, and symlinks them in dest_dir, overwriting files that exist if force is true. """ # source_dir needs to end in an / to make replacements work if not source_dir.endswith("/"): source_dir = source_dir + "/" for root, dir_names, file_names in os.walk(source_dir): # Remove exclusions from the walk list so we don't touch them for exclusion in exclusions: if exclusion in dir_names: dir_names.remove(exclusion) if exclusion in file_names: file_names.remove(exclusion) for file_name in file_names: # Get the full path for the source file source = os.path.join(root, file_name) # Get the full path for where the symlink should go # We remove the source_dir to get a dotfile dir relative path destination = os.path.join(dest_dir, source.replace(source_dir, '')) # Check for and remove the destination file if it exists # If a removal occurs, catch True and deploy the symlink if check_and_remove(source, destination, force): print("Symlinking " + source + " => " + destination) os.symlink(source, destination) if __name__ == "__main__": install_dotfiles(SOURCEDIR, HOMEDIR, EXCLUSIONS, False)