From c2719f80bdb1c949441a5cef30f1e50f7477a187 Mon Sep 17 00:00:00 2001 From: Fatih Akca Date: Thu, 30 Apr 2026 16:17:01 +0100 Subject: [PATCH] copier: add RemoveOptions.AllowWildcard Add AllowWildcard as a boolean field to copier.RemoveOptions. When set, the path is expanded as a glob pattern and each match is removed. Signed-off-by: Fatih Akca --- copier/copier.go | 35 ++++++++++++++-------- copier/copier_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/copier/copier.go b/copier/copier.go index 048585d9b2..dbb4ba1401 100644 --- a/copier/copier.go +++ b/copier/copier.go @@ -520,6 +520,7 @@ func Mkdir(root string, directory string, options MkdirOptions) error { type RemoveOptions struct { All bool // if Directory is a directory, remove its contents as well AllowNotFound bool // don't return an error if the item is already not present + AllowWildcard bool // expand the path as a glob pattern, removing each match. All must be set to remove matched non-empty directories } // Remove removes the specified directory or item, traversing any intermediate @@ -2297,20 +2298,30 @@ func copierHandlerRemove(req request) *response { errorResponse := func(fmtspec string, args ...any) *response { return &response{Error: fmt.Sprintf(fmtspec, args...), Remove: removeResponse{}} } - resolvedTarget, err := resolvePath(req.Root, req.Directory, false, nil) - if err != nil { - return errorResponse("copier: remove: %v", err) - } - if req.RemoveOptions.All { - err = os.RemoveAll(resolvedTarget) - } else { - err = os.Remove(resolvedTarget) - if req.RemoveOptions.AllowNotFound && errors.Is(err, os.ErrNotExist) { - err = nil + targets := []string{req.Directory} + if req.RemoveOptions.AllowWildcard { + var err error + targets, err = extendedGlob(req.Directory) + if err != nil { + return errorResponse("copier: remove: glob %q: %v", req.Directory, err) } } - if err != nil { - return errorResponse("copier: remove %q: %v", req.Directory, err) + for _, target := range targets { + resolvedTarget, err := resolvePath(req.Root, target, false, nil) + if err != nil { + return errorResponse("copier: remove: %v", err) + } + if req.RemoveOptions.All { + err = os.RemoveAll(resolvedTarget) + } else { + err = os.Remove(resolvedTarget) + if req.RemoveOptions.AllowNotFound && errors.Is(err, os.ErrNotExist) { + err = nil + } + } + if err != nil { + return errorResponse("copier: remove %q: %v", target, err) + } } return &response{Error: "", Remove: removeResponse{}} } diff --git a/copier/copier_test.go b/copier/copier_test.go index 926bdafc21..f87bdb9a00 100644 --- a/copier/copier_test.go +++ b/copier/copier_test.go @@ -1954,6 +1954,7 @@ func testRemove(t *testing.T) { remove string all bool allowNotFound bool + allowWildcard bool fail bool removed []string } @@ -2095,6 +2096,73 @@ func testRemove(t *testing.T) { all: true, removed: []string{"subdir-a/subdir-e", "subdir-a/subdir-e/subdir-f"}, }, + { + name: "wildcard-files", + remove: "subdir-a/file-*", + allowWildcard: true, + removed: []string{"subdir-a/file-a", "subdir-a/file-b"}, + }, + { + name: "wildcard-all-in-dir", + remove: "subdir-a/subdir-b/*", + allowWildcard: true, + all: true, + removed: []string{ + "subdir-a/subdir-b/subdir-c", + "subdir-a/subdir-b/subdir-c/parent", + "subdir-a/subdir-b/subdir-c/link-b", + "subdir-a/subdir-b/subdir-c/root", + }, + }, + { + name: "wildcard-no-match", + remove: "subdir-a/nonexistent-*", + allowWildcard: true, + }, + { + name: "wildcard-allow-not-found", + remove: "subdir-a/file-*", + allowWildcard: true, + allowNotFound: true, + removed: []string{"subdir-a/file-a", "subdir-a/file-b"}, + }, + { + name: "wildcard-dir-without-all", + remove: "subdir-a/subdir-*", + allowWildcard: true, + all: false, + fail: true, + }, + { + name: "wildcard-empty-dir", + remove: "subdir-a/subdir-d", + allowWildcard: true, + all: false, + removed: []string{"subdir-a/subdir-d"}, + }, + { + name: "wildcard-literal-path", + remove: "subdir-a/file-a", + allowWildcard: true, + removed: []string{"subdir-a/file-a"}, + }, + { + name: "wildcard-literal-missing", + remove: "subdir-a/file-nonexistent", + allowWildcard: true, + }, + { + name: "wildcard-char-class", + remove: "subdir-a/file-[ab]", + allowWildcard: true, + removed: []string{"subdir-a/file-a", "subdir-a/file-b"}, + }, + { + name: "wildcard-question-mark", + remove: "subdir-a/file-?", + allowWildcard: true, + removed: []string{"subdir-a/file-a", "subdir-a/file-b"}, + }, }, }, } @@ -2105,7 +2173,7 @@ func testRemove(t *testing.T) { dir, err := makeContextFromArchive(t, makeArchive(testArchives[i].headers, nil), "") require.NoErrorf(t, err, "error creating context from archive %q, topdir=%q", testArchives[i].name, "") root := dir - options := RemoveOptions{All: testCase.all, AllowNotFound: testCase.allowNotFound} + options := RemoveOptions{All: testCase.all, AllowNotFound: testCase.allowNotFound, AllowWildcard: testCase.allowWildcard} beforeNames := make(map[string]struct{}) err = filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error { if err != nil {