From 138fdbc8d7f42bd54e0a1d0c07d5858b1055ea50 Mon Sep 17 00:00:00 2001 From: Robin Schneider Date: Mon, 21 May 2018 19:10:17 +0200 Subject: Add 'canonicalize-path' option to link Dotbot had a hardcoded behaviour that the BASEDIR was always passed to os.path.realpath which "returns the canonical path of the specified filename, eliminating any symbolic links encountered in the path". This might not always be desirable so this commit makes it configurable. The use case where `canonicalize-path` comes in handy is the following: You want to provide dotfiles in the Filesystem Hierarchy Standard under `/usr/local/share/ypid_dotfiles/`. Now you want to provide `.config/dotfiles` as a default in `/etc/skel`. When you now pre-configure `/etc/skel` by running dotbot in it set has HOME, dotfiles will refer to `/usr/local/share/ypid_dotfiles/` and not `/etc/skel/.config/dotfiles` which does not look nice. This is related to but not the same as the `relative` parameter used with link commands. --- README.md | 1 + dotbot/context.py | 8 ++++++-- dotbot/dispatcher.py | 4 ++-- dotbot/plugins/link.py | 23 +++++++++++++---------- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ca7242d..6f6db79 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,7 @@ Available extended configuration parameters: | `relink` | Removes the old target if it's a symlink (default:false) | | `force` | Force removes the old target, file or folder, and forces a new link (default:false) | | `relative` | Use a relative path to the source when creating the symlink (default:false, absolute links) | +| `canonicalize-path` | Resolve any symbolic links encountered in the source to symlink to the canonical path (default:true, real paths) | | `glob` | Treat a `*` character as a wildcard, and perform link operations on all of those matches (default:false) | | `if` | Execute this in your `$SHELL` and only link if it is successful. | | `ignore-missing` | Do not fail if the source is missing and create the link anyway (default:false) | diff --git a/dotbot/context.py b/dotbot/context.py index b2dbd6c..8c42d47 100644 --- a/dotbot/context.py +++ b/dotbot/context.py @@ -1,4 +1,5 @@ import copy +import os class Context(object): ''' @@ -13,8 +14,11 @@ class Context(object): def set_base_directory(self, base_directory): self._base_directory = base_directory - def base_directory(self): - return self._base_directory + def base_directory(self, canonical_path=True): + base_directory = self._base_directory + if canonical_path: + base_directory = os.path.realpath(base_directory) + return base_directory def set_defaults(self, defaults): self._defaults = defaults diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py index d1a4f95..36eac02 100644 --- a/dotbot/dispatcher.py +++ b/dotbot/dispatcher.py @@ -10,8 +10,8 @@ class Dispatcher(object): self._load_plugins() def _setup_context(self, base_directory): - path = os.path.abspath(os.path.realpath( - os.path.expanduser(base_directory))) + path = os.path.abspath( + os.path.expanduser(base_directory)) if not os.path.exists(path): raise DispatchError('Nonexistent base directory') self._context = Context(path) diff --git a/dotbot/plugins/link.py b/dotbot/plugins/link.py index bf3db3e..9ba5540 100644 --- a/dotbot/plugins/link.py +++ b/dotbot/plugins/link.py @@ -26,6 +26,7 @@ class Link(dotbot.Plugin): for destination, source in links.items(): destination = os.path.expandvars(destination) relative = defaults.get('relative', False) + canonical_path = defaults.get('canonicalize-path', True) force = defaults.get('force', False) relink = defaults.get('relink', False) create = defaults.get('create', False) @@ -36,6 +37,7 @@ class Link(dotbot.Plugin): # extended config test = source.get('if', test) relative = source.get('relative', relative) + canonical_path = source.get('canonicalize-path', canonical_path) force = source.get('force', force) relink = source.get('relink', relink) create = source.get('create', create) @@ -68,8 +70,8 @@ class Link(dotbot.Plugin): if create: success &= self._create(destination) if force or relink: - success &= self._delete(path, destination, relative, force) - success &= self._link(path, destination, relative, ignore_missing) + success &= self._delete(path, destination, relative, canonical_path, force) + success &= self._link(path, destination, relative, canonical_path, ignore_missing) else: self._log.lowinfo("Globs from '" + path + "': " + str(glob_results)) glob_base = path[:glob_star_loc] @@ -79,8 +81,8 @@ class Link(dotbot.Plugin): if create: success &= self._create(glob_link_destination) if force or relink: - success &= self._delete(glob_full_item, glob_link_destination, relative, force) - success &= self._link(glob_full_item, glob_link_destination, relative, ignore_missing) + success &= self._delete(glob_full_item, glob_link_destination, relative, canonical_path, force) + success &= self._link(glob_full_item, glob_link_destination, relative, canonical_path, ignore_missing) else: if create: success &= self._create(destination) @@ -94,8 +96,8 @@ class Link(dotbot.Plugin): (destination, path)) continue if force or relink: - success &= self._delete(path, destination, relative, force) - success &= self._link(path, destination, relative, ignore_missing) + success &= self._delete(path, destination, relative, canonical_path, force) + success &= self._link(path, destination, relative, canonical_path, ignore_missing) if success: self._log.info('All links have been set up') else: @@ -159,9 +161,9 @@ class Link(dotbot.Plugin): self._log.lowinfo('Creating directory %s' % parent) return success - def _delete(self, source, path, relative, force): + def _delete(self, source, path, relative, canonical_path, force): success = True - source = os.path.join(self._context.base_directory(), source) + source = os.path.join(self._context.base_directory(canonical_path=canonical_path), source) fullpath = os.path.expanduser(path) if relative: source = self._relative_path(source, fullpath) @@ -195,7 +197,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, ignore_missing): + def _link(self, source, link_name, relative, canonical_path, ignore_missing): ''' Links link_name to source. @@ -203,7 +205,8 @@ class Link(dotbot.Plugin): ''' success = False destination = os.path.expanduser(link_name) - absolute_source = os.path.join(self._context.base_directory(), source) + base_directory = self._context.base_directory(canonical_path=canonical_path) + absolute_source = os.path.join(base_directory, source) if relative: source = self._relative_path(absolute_source, destination) else: -- cgit v1.2.3 From 320d5d0123b045fe922cf576d8d65e2db0517910 Mon Sep 17 00:00:00 2001 From: Anish Athalye Date: Fri, 3 Jan 2020 16:07:44 -0500 Subject: Add tests for canonicalize-path --- dotbot/cli.py | 4 ++-- test/tests/link-canonicalize.bash | 20 ++++++++++++++++++++ test/tests/link-no-canonicalize.bash | 23 +++++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 test/tests/link-canonicalize.bash create mode 100644 test/tests/link-no-canonicalize.bash diff --git a/dotbot/cli.py b/dotbot/cli.py index 2680acf..77bd439 100644 --- a/dotbot/cli.py +++ b/dotbot/cli.py @@ -73,10 +73,10 @@ def main(): if not isinstance(tasks, list): raise ReadingError('Configuration file must be a list of tasks') if options.base_directory: - base_directory = options.base_directory + base_directory = os.path.abspath(options.base_directory) else: # default to directory of config file - base_directory = os.path.dirname(os.path.realpath(options.config_file)) + base_directory = os.path.dirname(os.path.abspath(options.config_file)) os.chdir(base_directory) dispatcher = Dispatcher(base_directory) success = dispatcher.dispatch(tasks) diff --git a/test/tests/link-canonicalize.bash b/test/tests/link-canonicalize.bash new file mode 100644 index 0000000..34015c8 --- /dev/null +++ b/test/tests/link-canonicalize.bash @@ -0,0 +1,20 @@ +test_description='linking canonicalizes path by default' +. '../test-lib.bash' + +test_expect_success 'setup' ' +echo "apple" > ${DOTFILES}/f && +ln -s dotfiles dotfiles-symlink +' + +test_expect_success 'run' ' +cat > "${DOTFILES}/${INSTALL_CONF}" < ${DOTFILES}/f && +ln -s dotfiles dotfiles-symlink +' + +test_expect_success 'run' ' +cat > "${DOTFILES}/${INSTALL_CONF}" <