diff --git a/README.md b/README.md index eb09942..a5c6457 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,9 @@ Configuration ------------- Dotbot uses json-formatted configuration files to let you specify how to set up -your dotfiles. Currently, Dotbot knows how to `link` files and execute `shell` -commands. Dotbot executes tasks in the order that they are specified in. +your dotfiles. Currently, Dotbot knows how to `link` files, execute `shell` +commands, and `clean` directories of broken symbolic links. Dotbot executes +tasks in the order that they are specified in. **Ideally, bootstrap configurations should be idempotent. That is, the installer should be able to be run multiple times without causing any @@ -100,7 +101,8 @@ dictionary that contains a command name mapping to data for that command. For `link`, you specify how files should be linked in a dictionary. For `shell`, you specify an array consisting of commands, where each command is an array consisting of the shell command as the first element and a description as the -second. +second. For `clean`, you specify an array consisting of targets, where each +target is a path to a directory. Dotbot is aware of a base directory (that is specified when running the installer), so link targets can be specified relative to that, and shell @@ -111,6 +113,9 @@ started. The convention for configuration file names is `install.conf.json`. ```json [ + { + "clean": ["~"] + }, { "link": { "~/.tmux.conf": "tmux.conf", diff --git a/dotbot/executor/__init__.py b/dotbot/executor/__init__.py index 1762f78..d87ca4b 100644 --- a/dotbot/executor/__init__.py +++ b/dotbot/executor/__init__.py @@ -1,3 +1,4 @@ from .executor import Executor from .linker import Linker +from .cleaner import Cleaner from .commandrunner import CommandRunner diff --git a/dotbot/executor/cleaner.py b/dotbot/executor/cleaner.py new file mode 100644 index 0000000..59b9259 --- /dev/null +++ b/dotbot/executor/cleaner.py @@ -0,0 +1,49 @@ +import os +from . import Executor + +class Cleaner(Executor): + ''' + Cleans broken symbolic links. + ''' + + _directive = 'clean' + + def can_handle(self, directive): + return directive == self._directive + + def handle(self, directive, data): + if directive != self._directive: + raise ValueError('Cleaner cannot handle directive %s' % directive) + return self._process_clean(data) + + def _process_clean(self, targets): + success = True + for target in targets: + success &= self._clean(target) + if success: + self._log.info('All targets have been cleaned') + else: + self._log.error('Some targets were not succesfully cleaned') + return success + + def _clean(self, target): + ''' + Cleans all the broken symbolic links in target that point to + a subdirectory of the base directory. + ''' + for item in os.listdir(os.path.expanduser(target)): + path = os.path.join(os.path.expanduser(target), item) + if not os.path.exists(path) and os.path.islink(path): + if self._in_directory(path, self._base_directory): + self._log.lowinfo('Removing invalid link %s -> %s' % + (path, os.path.join(os.path.dirname(path), os.readlink(path)))) + os.remove(path) + return True + + def _in_directory(self, path, directory): + ''' + Returns true if the path is in the directory. + ''' + directory = os.path.join(os.path.realpath(directory), '') + path = os.path.realpath(path) + return os.path.commonprefix([path, directory]) == directory