Repository: otiai10/copy
Pull request #129 (2023-11-08 23:51:26)

Feature: Add option to rename file
Let user decide how to match files and generate the new file name.  Add feature for issue #109 

tomaswarynyca: Hi @dreamjz thanks for this work!
@otiai10 what do you think of this code?
dreamjz: Thank you for your review. I will make the modifications according to your suggestions this weekend (●'◡'●)
dreamjz: Thank you very much. Please take over this. 😊


To be honest, I've been working overtime every weekend in the past few weeks, and it looks like it might continue this month as well. 🥲
otiai10: To keep a record of your contribution and recreate a pull-req, let me squash merge here.

Patch:
diff --git a/README.md b/README.md
index 7c8b4e0..f1d42bc 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,11 @@ type Options struct {
 	// OnError can let users decide how to handle errors (e.g., you can suppress specific error).
 	OnError func(src, dest, string, err error) error
 
+    	// OnNameMatch returns new destination file name
+	// when source file name matches the specified pattern
+	// that has been parsed by NameRegexp
+	OnNameMatch func(re *regexp.Regexp, src string) string
+    
 	// Skip can specify which files should be skipped
 	Skip func(srcinfo os.FileInfo, src, dest string) (bool, error)
 
@@ -93,6 +98,9 @@ type Options struct {
 	// Please refer to https://pkg.go.dev/golang.org/x/sync/semaphore for more details.
 	NumOfWorkers int64
 
+    	// NameRegexp is used for matching source file name.
+	NameRegexp *regexp.Regexp
+
 	// PreferConcurrent is a function to determine whether or not
 	// to use goroutine for copying contents of directories.
 	// If PreferConcurrent is nil, which is default, it does concurrent
@@ -110,6 +118,13 @@ opt := Options{
 	},
 }
 err := Copy("your/directory", "your/directory.copy", opt)
+
+// Rename files when copying...
+re := regexp.MustCompile(`(?m)(\.stub)$`)
+opt := Options{NameRegexp: re, OnNameMatch: func(re *regexp.Regexp, src string) string {
+	return re.ReplaceAllString(src, "")
+}}
+err := Copy("your/directory", "your/directory.copy", opt)
 ```
 
 # Issues
diff --git a/copy.go b/copy.go
index f6134c4..10193ab 100644
--- a/copy.go
+++ b/copy.go
@@ -100,6 +100,15 @@ func fcopy(src, dest string, info os.FileInfo, opt Options) (err error) {
 		return
 	}
 
+	if opt.NameRegexp != nil && opt.OnNameMatch != nil {
+		destName := filepath.Base(dest)
+		destDir := filepath.Dir(dest)
+		if opt.NameRegexp.MatchString(destName) {
+			newName := opt.OnNameMatch(opt.NameRegexp, destName)
+			dest = filepath.Join(destDir, newName)
+		}
+	}
+
 	f, err := os.Create(dest)
 	if err != nil {
 		return
diff --git a/options.go b/options.go
index 1fbfcb1..251c995 100644
--- a/options.go
+++ b/options.go
@@ -5,6 +5,7 @@ import (
 	"io"
 	"io/fs"
 	"os"
+	"regexp"
 
 	"golang.org/x/sync/semaphore"
 )
@@ -21,6 +22,11 @@ type Options struct {
 	// OnErr lets called decide whether or not to continue on particular copy error.
 	OnError func(src, dest string, err error) error
 
+	// OnNameMatch returns new destination file name
+	// when source file name matches the specified pattern
+	// that has been parsed by NameRegexp
+	OnNameMatch func(re *regexp.Regexp, src string) string
+
 	// Skip can specify which files should be skipped
 	Skip func(srcinfo os.FileInfo, src, dest string) (bool, error)
 
@@ -81,6 +87,9 @@ type Options struct {
 	// If NumOfWorkers is 0 or 1, this function will be ignored.
 	PreferConcurrent func(srcdir, destdir string) (bool, error)
 
+	// NameRegexp is used for matching source file name.
+	NameRegexp *regexp.Regexp
+
 	// Internal use only
 	intent intent
 }


Test patch:
diff --git a/all_test.go b/all_test.go
index b2de8e5..afac6af 100644
--- a/all_test.go
+++ b/all_test.go
@@ -6,6 +6,7 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"regexp"
 	"runtime"
 	"strings"
 	"testing"
@@ -454,3 +455,12 @@ func TestOptions_PreferConcurrent(t *testing.T) {
 	err := Copy("test/data/case19", "test/data.copy/case19_preferconcurrent", opt)
 	Expect(t, err).ToBe(nil)
 }
+
+func TestOptions_OnNameMatch(t *testing.T) {
+	re := regexp.MustCompile(`(?m)(\.stub)$`)
+	opt := Options{NameRegexp: re, OnNameMatch: func(re *regexp.Regexp, src string) string {
+		return re.ReplaceAllString(src, "")
+	}}
+	err := Copy("test/data/case20", "test/data.copy/case20", opt)
+	Expect(t, err).ToBe(nil)
+}
diff --git a/test/data/case20/file_not_match.txt b/test/data/case20/file_not_match.txt
new file mode 100644
index 0000000..0f1a9a4
--- /dev/null
+++ b/test/data/case20/file_not_match.txt
@@ -0,0 +1,1 @@
+source: file_not_match.txt
\ No newline at end of file
diff --git a/test/data/case20/foo/bar/bar.py.stub b/test/data/case20/foo/bar/bar.py.stub
new file mode 100644
index 0000000..357d3c4
--- /dev/null
+++ b/test/data/case20/foo/bar/bar.py.stub
@@ -0,0 +1,1 @@
+source: bar.py.stub
\ No newline at end of file
diff --git a/test/data/case20/foo/bar/bar_not_match.txt b/test/data/case20/foo/bar/bar_not_match.txt
new file mode 100644
index 0000000..d883f02
--- /dev/null
+++ b/test/data/case20/foo/bar/bar_not_match.txt
@@ -0,0 +1,1 @@
+source: bar_not_match.txt
\ No newline at end of file
diff --git a/test/data/case20/foo/foo.c.stub b/test/data/case20/foo/foo.c.stub
new file mode 100644
index 0000000..75702b2
--- /dev/null
+++ b/test/data/case20/foo/foo.c.stub
@@ -0,0 +1,1 @@
+source: foo.c.stub
\ No newline at end of file
diff --git a/test/data/case20/foo/foo_not_match.txt b/test/data/case20/foo/foo_not_match.txt
new file mode 100644
index 0000000..0be312d
--- /dev/null
+++ b/test/data/case20/foo/foo_not_match.txt
@@ -0,0 +1,1 @@
+source: foo_not_match.txt
\ No newline at end of file
diff --git a/test/data/case20/main.go.stub b/test/data/case20/main.go.stub
new file mode 100644
index 0000000..042c8ff
--- /dev/null
+++ b/test/data/case20/main.go.stub
@@ -0,0 +1,1 @@
+source: main.go.stub
\ No newline at end of file