diff --git a/README.md b/README.md index 486fbd9..5c79337 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ have a defined ordering. ### Link Link commands specify how files and directories should be symbolically linked. +If desired, items can be specified to be forcibly linked, overwriting existing +files if necessary. #### Format @@ -99,13 +101,23 @@ locations. Source locations are specified relative to the base directory (that is specified when running the installer). Source directory names should contain a trailing "/" character. +Link commands support an (optional) extended configuration. In this type of +configuration, instead of specifying source locations directly, targets are +mapped to extended configuration dictionaries. These dictionaries map "path" to +the source path, and specify "force" as true if the file or directory should be +forcibly linked. + ##### Example ```json { "link": { "~/.vimrc": "vimrc", - "~/.vim": "vim/" + "~/.vim": "vim/", + "~/.zshrc": { + "path": "zshrc", + "force": true + } } } ``` diff --git a/dotbot/executor/linker.py b/dotbot/executor/linker.py index 60c76b5..2d14ecc 100644 --- a/dotbot/executor/linker.py +++ b/dotbot/executor/linker.py @@ -1,4 +1,4 @@ -import os +import os, shutil from . import Executor class Linker(Executor): @@ -19,7 +19,15 @@ class Linker(Executor): def _process_links(self, links): success = True for destination, source in links.items(): - success &= self._link(source, destination) + if isinstance(source, dict): + # extended config + path = source['path'] + force = source.get('force', False) + if force: + success &= self._delete(destination) + else: + path = source + success &= self._link(path, destination) if success: self._log.info('All links have been set up') else: @@ -47,6 +55,22 @@ class Linker(Executor): path = os.path.expanduser(path) return os.path.exists(path) + def _delete(self, path): + success = True + if self._exists(path) and not self._is_link(path): + fullpath = os.path.expanduser(path) + try: + if os.path.isdir(fullpath): + shutil.rmtree(fullpath) + else: + os.remove(fullpath) + except OSError: + self._log.warning('Failed to remove %s' % path) + success = False + else: + self._log.lowinfo('Removing %s' % path) + return success + def _link(self, source, link_name): ''' Links link_name to source.