diff --git a/dotbot/plugins/link.py b/dotbot/plugins/link.py index 6799cc1..152fd91 100644 --- a/dotbot/plugins/link.py +++ b/dotbot/plugins/link.py @@ -59,63 +59,39 @@ class Link(Plugin): self._log.lowinfo("Skipping %s" % destination) continue path = os.path.normpath(os.path.expandvars(os.path.expanduser(path))) - if use_glob: + if use_glob and self._has_glob_chars(path): glob_results = self._create_glob_results(path, exclude_paths) - if len(glob_results) == 0: - self._log.lowinfo("Globbing couldn't find anything matching " + str(path)) - continue - if len(glob_results) == 1 and destination[-1] == "/": - self._log.error("Ambiguous action requested.") - self._log.error( - "No wildcard in glob, directory use undefined: " - + destination - + " -> " - + str(glob_results) + self._log.lowinfo("Globs from '" + path + "': " + str(glob_results)) + for glob_full_item in glob_results: + # Find common dirname between pattern and the item: + glob_dirname = os.path.dirname(os.path.commonprefix([path, glob_full_item])) + glob_item = ( + glob_full_item + if len(glob_dirname) == 0 + else glob_full_item[len(glob_dirname) + 1 :] ) - self._log.warning("Did you want to link the directory or into it?") - success = False - continue - elif len(glob_results) == 1 and destination[-1] != "/": - # perform a normal link operation + # Add prefix to basepath, if provided + if base_prefix: + glob_item = base_prefix + glob_item + # where is it going + glob_link_destination = os.path.join(destination, glob_item) if create: - success &= self._create(destination) + success &= self._create(glob_link_destination) if force or relink: - 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)) - for glob_full_item in glob_results: - # Find common dirname between pattern and the item: - glob_dirname = os.path.dirname(os.path.commonprefix([path, glob_full_item])) - glob_item = ( - glob_full_item - if len(glob_dirname) == 0 - else glob_full_item[len(glob_dirname) + 1 :] - ) - # Add prefix to basepath, if provided - if base_prefix: - glob_item = base_prefix + glob_item - # where is it going - glob_link_destination = os.path.join(destination, glob_item) - if create: - success &= self._create(glob_link_destination) - if force or relink: - success &= self._delete( - glob_full_item, - glob_link_destination, - relative, - canonical_path, - force, - ) - success &= self._link( + success &= self._delete( glob_full_item, glob_link_destination, relative, canonical_path, - ignore_missing, + force, ) + success &= self._link( + glob_full_item, + glob_link_destination, + relative, + canonical_path, + ignore_missing, + ) else: if create: success &= self._create(destination) @@ -154,6 +130,9 @@ class Link(Plugin): else: return source + def _has_glob_chars(self, path): + return any(i in path for i in "?*[") + def _glob(self, path): """ Wrap `glob.glob` in a python agnostic way, catching errors in usage. diff --git a/tests/test_link.py b/tests/test_link.py index 86bc7c9..e577f4d 100644 --- a/tests/test_link.py +++ b/tests/test_link.py @@ -270,7 +270,7 @@ def test_link_glob_4(home, dotfiles, run_dotbot): @pytest.mark.parametrize("path", ("foo", "foo/")) -def test_link_glob_ambiguous_failure(path, home, dotfiles, run_dotbot): +def test_link_glob_ignore_no_glob_chars(path, home, dotfiles, run_dotbot): """Verify ambiguous link globbing fails.""" dotfiles.makedirs("foo") @@ -286,28 +286,8 @@ def test_link_glob_ambiguous_failure(path, home, dotfiles, run_dotbot): } ] ) - with pytest.raises(SystemExit): - run_dotbot() - assert not os.path.exists(os.path.join(home, "foo")) - - -def test_link_glob_ambiguous_success(home, dotfiles, run_dotbot): - """Verify the case where ambiguous link globbing succeeds.""" - - dotfiles.makedirs("foo") - dotfiles.write_config( - [ - { - "link": { - "~/foo": { - "path": "foo", - "glob": True, - } - } - } - ] - ) run_dotbot() + assert os.path.islink(os.path.join(home, "foo")) assert os.path.exists(os.path.join(home, "foo")) @@ -600,6 +580,26 @@ def test_link_glob_no_match(home, dotfiles, run_dotbot): run_dotbot() +def test_link_glob_single_match(home, dotfiles, run_dotbot): + """Verify linking works even when glob matches exactly one file.""" + # regression test for https://github.com/anishathalye/dotbot/issues/282 + + dotfiles.write("foo/a", "apple") + dotfiles.write_config( + [ + {"defaults": {"link": {"glob": True, "create": True}}}, + {"link": {"~/.config/foo": "foo/*"}}, + ] + ) + run_dotbot() + + assert not os.path.islink(os.path.join(home, ".config")) + assert not os.path.islink(os.path.join(home, ".config", "foo")) + assert os.path.islink(os.path.join(home, ".config", "foo", "a")) + with open(os.path.join(home, ".config", "foo", "a")) as file: + assert file.read() == "apple" + + @pytest.mark.skipif( "sys.platform[:5] == 'win32'", reason="These if commands won't run on Windows",