From 79c9c0747e876c0b97a4fb3fe91fa9825e462722 Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Sun, 15 Nov 2020 16:25:57 +0100 Subject: Add a dry run mode. --- dotbot/cli.py | 5 ++++- dotbot/dispatcher.py | 5 +++-- dotbot/plugin.py | 5 ++++- dotbot/plugins/clean.py | 33 +++++++++++++++++++++++--------- dotbot/plugins/create.py | 34 +++++++++++++++++++++----------- dotbot/plugins/link.py | 50 ++++++++++++++++++++++++++++-------------------- dotbot/plugins/shell.py | 13 +++++++++++-- 7 files changed, 98 insertions(+), 47 deletions(-) diff --git a/dotbot/cli.py b/dotbot/cli.py index 0795d40..b09461e 100644 --- a/dotbot/cli.py +++ b/dotbot/cli.py @@ -18,6 +18,8 @@ def add_options(parser): help='suppress most output') parser.add_argument('-v', '--verbose', action='store_true', help='enable verbose output') + parser.add_argument('-n', '--dry-run', action='store_true', + help='do not actually perform any changes') parser.add_argument('-d', '--base-directory', help='execute commands from within BASEDIR', metavar='BASEDIR') @@ -97,7 +99,8 @@ def main(): # default to directory of config file base_directory = os.path.dirname(os.path.abspath(options.config_file)) os.chdir(base_directory) - dispatcher = Dispatcher(base_directory, only=options.only, skip=options.skip) + dispatcher = Dispatcher(base_directory, only=options.only, skip=options.skip, + dry_run = options.dry_run) success = dispatcher.dispatch(tasks) if success: log.verbose('\n==> All tasks executed successfully') diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py index f809bef..9c55050 100644 --- a/dotbot/dispatcher.py +++ b/dotbot/dispatcher.py @@ -4,12 +4,13 @@ from .messenger import Messenger from .context import Context class Dispatcher(object): - def __init__(self, base_directory, only=None, skip=None): + def __init__(self, base_directory, only=None, skip=None, dry_run = False): self._log = Messenger() self._setup_context(base_directory) self._load_plugins() self._only = only self._skip = skip + self._dry_run = dry_run def _setup_context(self, base_directory): path = os.path.abspath( @@ -34,7 +35,7 @@ class Dispatcher(object): for plugin in self._plugins: if plugin.can_handle(action): try: - success &= plugin.handle(action, task[action]) + success &= plugin.handle(action, task[action], self._dry_run) handled = True except Exception as err: self._log.error( diff --git a/dotbot/plugin.py b/dotbot/plugin.py index 5e2c923..2d12636 100644 --- a/dotbot/plugin.py +++ b/dotbot/plugin.py @@ -18,10 +18,13 @@ class Plugin(object): ''' return directive == self._directive - def handle(self, directive, data): + def handle(self, directive, data, dry_run = False): ''' Executes the directive. Returns true if the Plugin successfully handled the directive. + + When dry_run is True, the plugin will not perform any actions, only + return False when there is nothing to do, True otherwise. ''' raise NotImplementedError diff --git a/dotbot/plugins/clean.py b/dotbot/plugins/clean.py index c8f3a03..212fb07 100644 --- a/dotbot/plugins/clean.py +++ b/dotbot/plugins/clean.py @@ -9,7 +9,7 @@ class Clean(dotbot.Plugin): _directive = 'clean' - def handle(self, directive, targets): + def handle(self, directive, targets, dry_run): success = True defaults = self._context.defaults().get(self._directive, {}) for target in targets: @@ -18,14 +18,22 @@ class Clean(dotbot.Plugin): if isinstance(targets, dict) and isinstance(targets[target], dict): force = targets[target].get('force', force) recursive = targets[target].get('recursive', recursive) - success &= self._clean(target, force, recursive) - if success: - self._log.verbose('All targets have been cleaned') + success &= self._clean(target, force, recurseve, dry_run) + + if dry_run: + if success: + self._log.verbose('clean/dry run: nothing to do') + else: + self._log.verbose('clean/dry run: some targets are dirty') else: - self._log.error('Some targets were not successfully cleaned') + if success: + self._log.verbose('All targets have been cleaned') + else: + self._log.error('Some targets were not successfully cleaned') + return success - def _clean(self, target, force, recursive): + def _clean(self, target, force, recursive, dry_run): ''' Cleans all the broken symbolic links in target if they point to a subdirectory of the base directory or if forced to clean. @@ -33,21 +41,28 @@ class Clean(dotbot.Plugin): if not os.path.isdir(os.path.expandvars(os.path.expanduser(target))): self._log.debug('Ignoring nonexistent directory %s' % target) return True + + ret = True for item in os.listdir(os.path.expandvars(os.path.expanduser(target))): path = os.path.join(os.path.expandvars(os.path.expanduser(target)), item) if recursive and os.path.isdir(path): # isdir implies not islink -- we don't want to descend into # symlinked directories. okay to do a recursive call here # because depth should be fairly limited - self._clean(path, force, recursive) + ret &= self._clean(path, force, recursive) if not os.path.exists(path) and os.path.islink(path): points_at = os.path.join(os.path.dirname(path), os.readlink(path)) if self._in_directory(path, self._context.base_directory()) or force: self._log.info('Removing invalid link %s -> %s' % (path, points_at)) - os.remove(path) + + if dry_run: + ret = False + else: + os.remove(path) else: self._log.verbose('Link %s -> %s not removed.' % (path, points_at)) - return True + + return ret def _in_directory(self, path, directory): ''' diff --git a/dotbot/plugins/create.py b/dotbot/plugins/create.py index 2420558..aa14277 100644 --- a/dotbot/plugins/create.py +++ b/dotbot/plugins/create.py @@ -9,15 +9,23 @@ class Create(dotbot.Plugin): _directive = 'create' - def handle(self, directive, paths): + def handle(self, directive, paths, dry_run): success = True for path in paths: path = os.path.expandvars(os.path.expanduser(path)) - success &= self._create(path) - if success: - self._log.verbose('All paths have been set up') + success &= self._create(path, dry_run) + + if dry_run: + if success: + self._log.verbose('create/dry run: nothing to do') + else: + self._log.verbose('create/dry run: some targets are missing') else: - self._log.error('Some paths were not successfully set up') + if success: + self._log.verbose('All paths have been set up') + else: + self._log.error('Some paths were not successfully set up') + return success def _exists(self, path): @@ -27,16 +35,20 @@ class Create(dotbot.Plugin): path = os.path.expanduser(path) return os.path.exists(path) - def _create(self, path): + def _create(self, path, dry_run): + if self._exists(path): + self._log.verbose('Path exists %s' % path) + return True + success = True - if not self._exists(path): - self._log.debug('Trying to create path %s' % path) + self._log.info('Creating path %s' % path) + if dry_run: + success = False + else: try: - self._log.info('Creating path %s' % path) os.makedirs(path) except OSError: self._log.warning('Failed to create path %s' % path) success = False - else: - self._log.verbose('Path exists %s' % path) + return success diff --git a/dotbot/plugins/link.py b/dotbot/plugins/link.py index 411a0ef..5e2bd90 100644 --- a/dotbot/plugins/link.py +++ b/dotbot/plugins/link.py @@ -11,7 +11,7 @@ class Link(dotbot.Plugin): _directive = 'link' - def handle(self, directive, links): + def handle(self, directive, links, dry_run): success = True defaults = self._context.defaults().get('link', {}) for destination, source in links.items(): @@ -47,12 +47,20 @@ class Link(dotbot.Plugin): (destination, path)) continue if force or relink: - success &= self._delete(path, destination, relative, canonical_path, force) - success &= self._link(path, destination, relative, canonical_path, ignore_missing) - if success: - self._log.verbose('All links have been set up') + success &= self._delete(path, destination, relative, canonical_path, force, dry_run) + success &= self._link(path, destination, relative, canonical_path, ignore_missing, dry_run) + + if dry_run: + if success: + self._log.verbose('link/dry run: nothing to do') + else: + self._log.verbose('link/dry run: some targets are missing') else: - self._log.error('Some links were not successfully set up') + if success: + self._log.verbose('All links have been set up') + else: + self._log.error('Some links were not successfully set up') + return success def _test_success(self, command): @@ -91,7 +99,7 @@ class Link(dotbot.Plugin): path = os.path.expanduser(path) return os.path.exists(path) - def _delete(self, source, path, relative, canonical_path, force): + def _delete(self, source, path, relative, canonical_path, force, dry_run): success = True source = os.path.join(self._context.base_directory(canonical_path=canonical_path), source) fullpath = os.path.expanduser(path) @@ -99,7 +107,11 @@ class Link(dotbot.Plugin): source = self._relative_path(source, fullpath) if ((self._is_link(path) and self._link_destination(path) != source) or (self._exists(path) and not self._is_link(path))): - removed = False + self._log.info('Removing %s' % path) + + if dry_run: + return False + try: if os.path.islink(fullpath): os.unlink(fullpath) @@ -107,16 +119,12 @@ class Link(dotbot.Plugin): elif force: if os.path.isdir(fullpath): shutil.rmtree(fullpath) - removed = True else: os.remove(fullpath) - removed = True except OSError: self._log.warning('Failed to remove %s' % path) success = False - else: - if removed: - self._log.info('Removing %s' % path) + return success def _relative_path(self, source, destination): @@ -127,7 +135,7 @@ class Link(dotbot.Plugin): destination_dir = os.path.dirname(destination) return os.path.relpath(source, destination_dir) - def _link(self, source, link_name, relative, canonical_path, ignore_missing): + def _link(self, source, link_name, relative, canonical_path, ignore_missing, dry_run): ''' Links link_name to source. @@ -149,13 +157,13 @@ class Link(dotbot.Plugin): # directory, and if source is relative, it will be relative to the # destination directory elif not self._exists(link_name) and (ignore_missing or self._exists(absolute_source)): - try: - os.symlink(source, destination) - except OSError: - self._log.warning('Linking failed %s -> %s' % (link_name, source)) - else: - self._log.verbose('Creating link %s -> %s' % (link_name, source)) - success = True + self._log.verbose('Creating link %s -> %s' % (link_name, source)) + if not dry_run: + try: + os.symlink(source, destination) + success = True + except OSError: + self._log.warning('Linking failed %s -> %s' % (link_name, source)) elif self._exists(link_name) and not self._is_link(link_name): self._log.warning( '%s already exists but is a regular file or directory' % diff --git a/dotbot/plugins/shell.py b/dotbot/plugins/shell.py index f0121e6..75d4dca 100644 --- a/dotbot/plugins/shell.py +++ b/dotbot/plugins/shell.py @@ -11,7 +11,7 @@ class Shell(dotbot.Plugin): _directive = 'shell' - def handle(self, directive, data): + def handle(self, directive, data, dry_run): success = True defaults = self._context.defaults().get('shell', {}) for item in data: @@ -38,6 +38,10 @@ class Shell(dotbot.Plugin): self._log.info('%s' % msg) else: self._log.info('%s [%s]' % (msg, cmd)) + + if dry_run: + continue + ret = dotbot.util.shell_command( cmd, cwd=self._context.base_directory(), @@ -48,8 +52,13 @@ class Shell(dotbot.Plugin): if ret != 0: success = False self._log.warning('Command [%s] failed' % cmd) - if success: + + if dry_run: + self._log.verbose('shell/dry run: commands not executed') + success = False + elif success: self._log.verbose('All commands have been executed') else: self._log.error('Some commands were not successfully executed') + return success -- cgit v1.2.3