├── .gitignore ├── .travis.yml ├── README.md ├── action.go ├── action_test.go ├── actor.go ├── changelog_writer.go ├── console_printer.go ├── contains_filter.go ├── dir_filter.go ├── file_entry.go ├── file_entry_sorters.go ├── file_ext_filter.go ├── file_filter.go ├── file_stream.go ├── fsrename └── main.go ├── glob_scanner.go ├── prefix_filter.go ├── proxy.go ├── regexp_filter.go ├── renamer.go ├── replacer.go ├── sequence.go ├── sequence_test.go ├── suffix_filter.go ├── tests ├── CamelCase.php ├── autoload.php ├── class-name.php ├── file.html ├── file.html.twig ├── foo-bar.php ├── php_files │ ├── test1.php │ ├── test2.php │ ├── test2.txt │ └── test2.xml ├── scanner │ ├── bar │ ├── foo │ └── zoo │ │ ├── x │ │ ├── y │ │ └── z ├── test1.txt ├── test2.txt └── test3.txt ├── utils.go ├── worker.go └── worker_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /fsrename/test* 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.5 4 | - 1.6 5 | install: 6 | - go get golang.org/x/tools/cmd/cover 7 | - go get github.com/mattn/goveralls 8 | - go get github.com/stretchr/testify/assert 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FSRename v2 - Fast & Simple Rename 2 | ================================ 3 | 4 | [![Build Status](https://travis-ci.org/c9s/fsrename.svg?branch=master)](https://travis-ci.org/c9s/fsrename) 5 | 6 | A simple, powerful rename tool supports complex filtering, written in GO. 7 | 8 | `fsrename` separates the pattern/replace options, therefore you can specify the 9 | pattern without typing these escaping characters and the slashes. 10 | 11 | pre-filtering, extension filtering, prefix trimming, suffix trimming , 12 | camelcase conversion, underscore conversion are all supported. 13 | 14 | > This tool is different from `gorename`, `gorename` is for refactoring your code, rename variables. 15 | 16 | 17 | SYNOPSIS 18 | -------------- 19 | 20 | fsrename -file -match "[a-z]+" -rr "foo_+" -with bar test 21 | fsrename -ext cpp -replace-ext cc src 22 | 23 | INSTALL 24 | -------------- 25 | 26 | go get -u -x github.com/c9s/fsrename/fsrename 27 | 28 | USAGE 29 | --------------------- 30 | 31 | fsrename [options] [path...] 32 | 33 | > Note: When `[path...]` is not given, "./" will be used as the default path for scanning files. 34 | 35 | To see the documentation in console: 36 | 37 | go doc github.com/c9s/fsrename/fsrename 38 | 39 | You can create a link to the package under your GOPATH to create an doc alias 40 | 41 | ln -s $(go list -f "{{.Dir}}" github.com/c9s/fsrename/fsrename) \ 42 | $(go list -f "{{.Root}}" github.com/c9s/fsrename/fsrename)/src/fsrename 43 | 44 | # see the document 45 | go doc fsrename 46 | 47 | 48 | 49 | ## OPTIONS 50 | 51 | ### FILTER OPTIONS 52 | 53 | `-match [regexp pattern]` pre-filter the files and directories based on the given regular pattern. 54 | 55 | `-contains [string]` pre-filter the files and directories based on the given string needle. 56 | 57 | `-f`,`-file` only for files. 58 | 59 | `-d`, `-dir` only for directories. 60 | 61 | `-ext [extension name]` find files with matched file extension. 62 | 63 | ### REPLACEMENT OPTIONS 64 | 65 | Please note the replacement target only works for the basename of a path. 66 | `-replace*` and `-with*` should be combined together to replace the substrings. 67 | 68 | #### SPECIFYING REPLACE TARGET 69 | 70 | `-r [search string]`, `-replace [search string]` specify target substring with normal string matching. 71 | 72 | `-rr [regexp pattern]`, `-replace-re [regexp pattern]`, `-replace-regexp [regexp pattern]` specify target substring with regular expression matching. 73 | 74 | #### SPECIFYING REPLACEMENT 75 | 76 | `-w [string]`, `-with [string]` replacement for the target substring. 77 | 78 | `-wf [format string]`, `-with-format [format string]` replacement with fmt.Sprintf format for the target substring. 79 | 80 | ### REPLACING RULE BUILDER OPTIONS 81 | 82 | `-trim-prefix [prefix]` trim filename prefix. 83 | 84 | `-trim-suffix [suffix]` trim filename suffix (this option removes suffix even for filename extensions). 85 | 86 | `-camel` converts dash/underscore separated filenames into camelcase filenames. 87 | 88 | `-underscore` converts camelcase filesnames into underscore separated filenames. 89 | 90 | ### ADDING PREFIX / SUFFIX 91 | 92 | `-add-prefix [prefix]` - prepend prefix to filename of the matched entries. 93 | 94 | `-add-suffix [suffix]` - add suffix to filename (before the extension name) of the matched entries. 95 | 96 | ### REPLACING EXTENSION 97 | 98 | `-replace-ext [ext]` replace the extension fomr the matched entries. usually this 99 | option is combined with `-ext [ext]` to replace extension names. 100 | 101 | ### COMMON OPTIONS 102 | 103 | `-dryrun` dry run, don't rename, just preview the result. 104 | 105 | `-changelog [changelog file]` records the rename actions in CSV format file. 106 | 107 | `-rollback [changelog file]` rollback the renames from a changelog file. 108 | 109 | 110 | 111 | ## QUICK EXAMPLES 112 | 113 | Find files with extension `.php` and replace the substring from the filename. 114 | 115 | fsrename -ext "php" -replace "some" -with "others" src/ 116 | 117 | Replace `Stmt.go` with "_stmt.go" under the current directory: 118 | 119 | fsrename -replace "Stmt.go" -with "_stmt.go" 120 | 121 | Replace `Stmt.go` with "_stmt.go" under directory `src/c6`: 122 | 123 | fsrename -replace "Stmt.go" -with "_stmt.go" src/c6 124 | 125 | # -r is a shorthand of -replace 126 | fsrename -r "Stmt.go" -with "_stmt.go" src/c6 127 | 128 | Replace `foo` with `bar` from files contains `prefix_` 129 | 130 | fsrename -file -contains prefix_ -replace foo -with bar test 131 | 132 | Or use `-match` to pre-filter the files with regular expression 133 | 134 | fsrename -file -match "[a-z]+" -replace foo -with bar test 135 | 136 | Use regular expression without escaping: 137 | 138 | fsrename -replace-regexp "_[a-z]*.go" -with ".go" src/c6 139 | 140 | # -rre is a shorthand of -replace-regexp 141 | fsrename -rre "_[a-z]*.go" -with ".go" src/c6 142 | 143 | fsrename -file -replace-regexp "_[a-z]*.go" -with ".go" src/c6 144 | 145 | fsrename -file -ext go -replace-regexp "[a-z]*" -with "123" src/c6 146 | 147 | fsrename -dir -replace "_xxx" -with "_aaa" src/c6 148 | 149 | fsrename -replace "_xxx" -with "_aaa" -dryrun src/c6 150 | 151 | Replace `.cpp` to `.cc` from the files under `src/` directory: 152 | 153 | fsrename -ext cpp -replace-ext cc src 154 | 155 | ## API 156 | 157 | Build your own file stream workers. 158 | 159 | The fsrename API is pretty straight forward and simple, you can create your own 160 | filters, actors in just few lines: 161 | 162 | ```go 163 | import "github.com/c9s/fsrename" 164 | 165 | 166 | input := fsrename.NewFileStream() 167 | pipe := fsrename.NewGlobScanner() 168 | pipe.SetInput(input) 169 | pipe.Start() 170 | pipe := pipe. 171 | Chain(fsrename.NewFileFilter()). 172 | Chain(fsrename.NewReverseSorter()) 173 | 174 | // start sending file entries into the input stream 175 | input <- fsrename.MustNewFileEntry("tests") 176 | 177 | // send EOS (end of stream) 178 | input <- nil 179 | 180 | 181 | // get entries from output 182 | output := pipe.Output() 183 | entry := <-output 184 | .... 185 | ``` 186 | 187 | 188 | ## ChangeLog 189 | 190 | v2.1 191 | 192 | - Added rename log printer to support rollback. 193 | - Supported ignoring .git/.svn/.hg directories. 194 | - Renamed camelcase option names to dash-separated option names. 195 | 196 | v2.2 197 | - [x] Added Proxy worker. 198 | - [x] Added rollback support: read changelog and rollback the actions 199 | 200 | ## Roadmap 201 | 202 | - [ ] Add `-list` to print the filtered file paths instead of renaming the files. 203 | - [ ] Add `-cleanup` to clean up non-ascii characters 204 | - [ ] Add `--real` to solve symlink reference 205 | 206 | ## LICENSE 207 | 208 | MIT License 209 | 210 | -------------------------------------------------------------------------------- /action.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "strings" 4 | import "regexp" 5 | import "fmt" 6 | import "path" 7 | import "strconv" 8 | 9 | type Action interface { 10 | Act(entry *FileEntry) bool 11 | } 12 | 13 | type StrReplaceAction struct { 14 | Search string 15 | Replace string 16 | N int 17 | } 18 | 19 | func NewStrReplaceAction(search, replace string, n int) *StrReplaceAction { 20 | return &StrReplaceAction{search, replace, n} 21 | } 22 | 23 | func (s *StrReplaceAction) Act(entry *FileEntry) bool { 24 | if strings.Contains(entry.base, s.Search) { 25 | newbase := strings.Replace(entry.base, s.Search, s.Replace, s.N) 26 | entry.newpath = path.Join(entry.dir, newbase) 27 | return true 28 | } 29 | return false 30 | } 31 | 32 | type StrFormatReplaceAction struct { 33 | Search string 34 | ReplaceFormat string 35 | N int 36 | Seq *Sequence 37 | } 38 | 39 | func NewStrFormatReplaceAction(search, replaceFormat string) *StrFormatReplaceAction { 40 | return &StrFormatReplaceAction{search, replaceFormat, 1, NewSequence(0, 1)} 41 | } 42 | 43 | func (s *StrFormatReplaceAction) Act(entry *FileEntry) bool { 44 | if strings.Contains(entry.base, s.Search) { 45 | format := fmt.Sprintf(s.ReplaceFormat, s.Seq.Next()) 46 | newbase := strings.Replace(entry.base, s.Search, format, s.N) 47 | entry.newpath = path.Join(entry.dir, newbase) 48 | return true 49 | } 50 | return false 51 | } 52 | 53 | type UnderscoreAction struct { 54 | spliter *regexp.Regexp 55 | } 56 | 57 | func NewUnderscoreAction() *UnderscoreAction { 58 | spliter := regexp.MustCompile("[A-Z]") 59 | return &UnderscoreAction{spliter} 60 | } 61 | 62 | func (a *UnderscoreAction) Act(entry *FileEntry) bool { 63 | newbase := a.spliter.ReplaceAllStringFunc(entry.base, func(w string) string { 64 | return "_" + strings.ToLower(w) 65 | }) 66 | newbase = strings.TrimLeft(newbase, "_") 67 | entry.newpath = path.Join(entry.dir, newbase) 68 | return true 69 | } 70 | 71 | type CamelCaseAction struct { 72 | spliter *regexp.Regexp 73 | } 74 | 75 | func NewCamelCaseAction(splitstr string) *CamelCaseAction { 76 | spliter := regexp.MustCompile(splitstr) 77 | return &CamelCaseAction{spliter} 78 | } 79 | 80 | func (a *CamelCaseAction) Act(entry *FileEntry) bool { 81 | substrings := a.spliter.Split(entry.base, -1) 82 | for i, str := range substrings { 83 | if i == 0 { 84 | substrings[i] = strings.ToLower(str) 85 | } else { 86 | substrings[i] = strings.ToUpper(str[:1]) + str[1:] 87 | } 88 | } 89 | entry.newpath = path.Join(entry.dir, strings.Join(substrings, "")) 90 | return true 91 | } 92 | 93 | // PrefixAction adds prefix to the matched filenames 94 | type PrefixAction struct { 95 | Prefix string 96 | } 97 | 98 | func NewPrefixAction(prefix string) *PrefixAction { 99 | return &PrefixAction{prefix} 100 | } 101 | 102 | func (a *PrefixAction) Act(entry *FileEntry) bool { 103 | if strings.HasPrefix(entry.base, a.Prefix) { 104 | return false 105 | } 106 | entry.newpath = path.Join(entry.dir, a.Prefix+entry.base) 107 | return true 108 | } 109 | 110 | // SuffixAction adds prefix to the matched filenames 111 | type SuffixAction struct { 112 | Suffix string 113 | } 114 | 115 | func NewSuffixAction(suffix string) *SuffixAction { 116 | return &SuffixAction{suffix} 117 | } 118 | 119 | func (a *SuffixAction) Act(entry *FileEntry) bool { 120 | strs := strings.Split(entry.base, ".") 121 | if len(strs) == 1 { 122 | entry.newpath = path.Join(entry.dir, strs[0]+a.Suffix) 123 | } else { 124 | fn := strings.Join(strs[:len(strs)-1], ".") 125 | if strings.HasSuffix(fn, a.Suffix) { 126 | return false 127 | } 128 | ext := strs[len(strs)-1] 129 | entry.newpath = path.Join(entry.dir, fn+a.Suffix+"."+ext) 130 | } 131 | return true 132 | } 133 | 134 | type ExtReplaceAction struct { 135 | Ext string 136 | } 137 | 138 | func NewExtReplaceAction(ext string) *ExtReplaceAction { 139 | return &ExtReplaceAction{ext} 140 | } 141 | 142 | func (a *ExtReplaceAction) Act(entry *FileEntry) bool { 143 | strs := strings.Split(entry.base, ".") 144 | if len(strs) == 1 { 145 | entry.newpath = path.Join(entry.dir, strs[0]+"."+a.Ext) 146 | } else { 147 | fn := strings.Join(strs[:len(strs)-1], ".") 148 | if strings.HasSuffix(fn, "."+a.Ext) { 149 | return false 150 | } 151 | entry.newpath = path.Join(entry.dir, fn+"."+a.Ext) 152 | } 153 | return true 154 | } 155 | 156 | type RegExpReplaceAction struct { 157 | Matcher *regexp.Regexp 158 | Replace string 159 | } 160 | 161 | func NewRegExpReplaceAction(matcher *regexp.Regexp, replace string) *RegExpReplaceAction { 162 | return &RegExpReplaceAction{matcher, replace} 163 | } 164 | 165 | func NewRegExpReplaceActionWithPattern(pattern string, replace string) *RegExpReplaceAction { 166 | matcher := regexp.MustCompile(pattern) 167 | return &RegExpReplaceAction{matcher, replace} 168 | } 169 | 170 | func (s *RegExpReplaceAction) Act(entry *FileEntry) bool { 171 | if s.Matcher.MatchString(entry.base) { 172 | newbase := s.Matcher.ReplaceAllString(entry.base, s.Replace) 173 | entry.newpath = path.Join(entry.dir, newbase) 174 | return true 175 | } 176 | return false 177 | } 178 | 179 | type RegExpFormatReplaceAction struct { 180 | Matcher *regexp.Regexp 181 | ReplaceFormat string 182 | Seq *Sequence 183 | } 184 | 185 | func NewRegExpFormatReplaceAction(matcher *regexp.Regexp, replaceFormat string) *RegExpFormatReplaceAction { 186 | return &RegExpFormatReplaceAction{matcher, replaceFormat, NewSequence(0, 1)} 187 | } 188 | 189 | func (s *RegExpFormatReplaceAction) Act(entry *FileEntry) bool { 190 | if s.Matcher.MatchString(entry.base) { 191 | format := strings.Replace(s.ReplaceFormat, "%i", strconv.Itoa(int(s.Seq.Next())), -1) 192 | newbase := s.Matcher.ReplaceAllString(entry.base, format) 193 | entry.newpath = path.Join(entry.dir, newbase) 194 | return true 195 | } 196 | return false 197 | } 198 | -------------------------------------------------------------------------------- /action_test.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "testing" 4 | import "github.com/stretchr/testify/assert" 5 | 6 | func TestUnderscoreAction(t *testing.T) { 7 | act := NewUnderscoreAction() 8 | assert.NotNil(t, act) 9 | entry, err := NewFileEntry("tests/CamelCase.php") 10 | assert.Nil(t, err) 11 | act.Act(entry) 12 | assert.Equal(t, "tests/camel_case.php", entry.newpath) 13 | } 14 | 15 | func TestExtReplaceAction(t *testing.T) { 16 | act := NewExtReplaceAction("twig") 17 | assert.NotNil(t, act) 18 | entry, err := NewFileEntry("tests/file.html") 19 | assert.Nil(t, err) 20 | assert.True(t, act.Act(entry)) 21 | assert.Equal(t, "tests/file.twig", entry.newpath) 22 | } 23 | 24 | func TestExtReplaceAction2(t *testing.T) { 25 | act := NewExtReplaceAction("twig") 26 | assert.NotNil(t, act) 27 | entry, err := NewFileEntry("tests/file.html.twig") 28 | assert.Nil(t, err) 29 | assert.True(t, act.Act(entry)) 30 | assert.Equal(t, "tests/file.html.twig", entry.newpath) 31 | } 32 | 33 | func TestCamelCaseAction(t *testing.T) { 34 | act := NewCamelCaseAction("[-_]+") 35 | assert.NotNil(t, act) 36 | entry, err := NewFileEntry("tests/foo-bar.php") 37 | assert.Nil(t, err) 38 | act.Act(entry) 39 | assert.Equal(t, "tests/fooBar.php", entry.newpath) 40 | } 41 | 42 | func TestRegExpAction(t *testing.T) { 43 | act := NewRegExpReplaceActionWithPattern("\\.php$", ".txt") 44 | assert.NotNil(t, act) 45 | entry, err := NewFileEntry("tests/autoload.php") 46 | assert.Nil(t, err) 47 | act.Act(entry) 48 | assert.Equal(t, "tests/autoload.txt", entry.newpath) 49 | } 50 | 51 | func TestStrReplaceAction(t *testing.T) { 52 | act := NewStrReplaceAction(".php", ".txt", 1) 53 | assert.NotNil(t, act) 54 | entry, err := NewFileEntry("tests/autoload.php") 55 | assert.Nil(t, err) 56 | act.Act(entry) 57 | assert.Equal(t, "tests/autoload.txt", entry.newpath) 58 | } 59 | -------------------------------------------------------------------------------- /actor.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | type Actor struct { 4 | *BaseWorker 5 | Action Action 6 | } 7 | 8 | func NewActor(a Action) *Actor { 9 | return &Actor{NewBaseWorker(), a} 10 | } 11 | func (a *Actor) Start() { 12 | go a.Run() 13 | } 14 | 15 | func (a *Actor) Run() { 16 | for { 17 | select { 18 | case <-a.stop: 19 | return 20 | case entry := <-a.input: 21 | // end of data 22 | if entry == nil { 23 | a.emitEnd() 24 | return 25 | } 26 | a.Action.Act(entry) 27 | a.output <- entry 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /changelog_writer.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import ( 4 | "encoding/csv" 5 | "log" 6 | "os" 7 | ) 8 | 9 | type ChangeLogWriter struct { 10 | *BaseWorker 11 | logfile string 12 | } 13 | 14 | func NewChangeLogWriter(logfile string) *ChangeLogWriter { 15 | return &ChangeLogWriter{NewBaseWorker(), logfile} 16 | } 17 | 18 | func (w *ChangeLogWriter) Start() { 19 | go w.Run() 20 | } 21 | 22 | func (w *ChangeLogWriter) Run() { 23 | f, err := os.Create(w.logfile) 24 | if err != nil { 25 | panic(err) 26 | } 27 | defer f.Close() 28 | 29 | writer := csv.NewWriter(f) 30 | defer writer.Flush() 31 | 32 | for { 33 | select { 34 | case <-w.stop: 35 | return 36 | case entry := <-w.input: 37 | // end of data 38 | if entry == nil { 39 | w.emitEnd() 40 | return 41 | } 42 | if err := writer.Write([]string{entry.path, entry.newpath, entry.message}); err != nil { 43 | log.Fatalln("Error writing files to csv:", err) 44 | } 45 | w.output <- entry 46 | } 47 | } 48 | if err := writer.Error(); err != nil { 49 | log.Fatalln("Error writing files to csv", err) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /console_printer.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "fmt" 4 | import "strings" 5 | import "os" 6 | 7 | type ConsolePrinter struct { 8 | *BaseWorker 9 | } 10 | 11 | func NewConsolePrinter() *ConsolePrinter { 12 | return &ConsolePrinter{NewBaseWorker()} 13 | } 14 | 15 | func (w *ConsolePrinter) Start() { 16 | go w.Run() 17 | } 18 | 19 | func (w *ConsolePrinter) Run() { 20 | pwd, err := os.Getwd() 21 | if err != nil { 22 | fmt.Println(err) 23 | os.Exit(1) 24 | } 25 | for { 26 | select { 27 | case <-w.stop: 28 | return 29 | case entry := <-w.input: 30 | // end of data 31 | if entry == nil { 32 | w.emitEnd() 33 | return 34 | } 35 | // trim pwd paths 36 | if strings.HasPrefix(entry.path, pwd) { 37 | var oldpath = strings.TrimLeft(strings.Replace(entry.path, pwd, "", 1), "/") 38 | var newpath = strings.TrimLeft(strings.Replace(entry.newpath, pwd, "", 1), "/") 39 | fmt.Printf("./%s", oldpath) 40 | if newpath != "" { 41 | fmt.Printf(" -> ./%s", newpath) 42 | } 43 | if entry.message != "" { 44 | fmt.Printf(" => [%s]", entry.message) 45 | } 46 | fmt.Printf("\n") 47 | } else { 48 | fmt.Printf("./%s", entry.path) 49 | if entry.newpath != "" { 50 | fmt.Printf(" -> ./%s", entry.newpath) 51 | } 52 | if entry.message != "" { 53 | fmt.Printf(" => [%s]", entry.message) 54 | } 55 | fmt.Printf("\n") 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /contains_filter.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "strings" 4 | 5 | type StrContainsFilter struct { 6 | *BaseWorker 7 | Search string 8 | } 9 | 10 | func NewStrContainsFilter(search string) *StrContainsFilter { 11 | return &StrContainsFilter{NewBaseWorker(), search} 12 | } 13 | 14 | func (w *StrContainsFilter) Start() { 15 | go w.Run() 16 | } 17 | 18 | func (w *StrContainsFilter) Run() { 19 | for { 20 | select { 21 | case <-w.stop: 22 | return 23 | case entry := <-w.input: 24 | // end of data 25 | if entry == nil { 26 | w.emitEnd() 27 | return 28 | } 29 | if strings.Contains(entry.base, w.Search) { 30 | w.output <- entry 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /dir_filter.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | type DirFilter struct { 4 | *BaseWorker 5 | } 6 | 7 | func NewDirFilter() *DirFilter { 8 | return &DirFilter{NewBaseWorker()} 9 | } 10 | 11 | func (w *DirFilter) Start() { 12 | go w.Run() 13 | } 14 | 15 | func (w *DirFilter) Run() { 16 | for { 17 | select { 18 | case <-w.stop: 19 | return 20 | break 21 | case entry := <-w.input: 22 | // end of data 23 | if entry == nil { 24 | w.emitEnd() 25 | return 26 | } 27 | if entry.info.Mode().IsDir() { 28 | w.output <- entry 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /file_entry.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "os" 4 | import "path" 5 | 6 | type FileEntry struct { 7 | path string 8 | dir string 9 | base string 10 | info os.FileInfo 11 | newpath string 12 | message string 13 | } 14 | 15 | type FileEntries []FileEntry 16 | 17 | func MustNewFileEntry(filepath string) *FileEntry { 18 | entry, err := NewFileEntry(filepath) 19 | if err != nil { 20 | panic(err) 21 | } 22 | return entry 23 | } 24 | 25 | func NewFileEntryWithInfo(filepath string, info os.FileInfo) *FileEntry { 26 | return &FileEntry{ 27 | path: filepath, 28 | info: info, 29 | base: path.Base(filepath), 30 | dir: path.Dir(filepath), 31 | } 32 | } 33 | 34 | func NewFileEntry(filepath string) (*FileEntry, error) { 35 | info, err := os.Stat(filepath) 36 | if err != nil { 37 | return nil, err 38 | } 39 | e := FileEntry{ 40 | path: filepath, 41 | info: info, 42 | base: path.Base(filepath), 43 | dir: path.Dir(filepath), 44 | } 45 | return &e, nil 46 | } 47 | -------------------------------------------------------------------------------- /file_entry_sorters.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | func (files FileEntries) Len() int { return len(files) } 8 | func (files FileEntries) Less(i, j int) bool { return files[i].path < files[j].path } 9 | func (files FileEntries) Swap(i, j int) { 10 | files[i], files[j] = files[j], files[i] 11 | } 12 | func (files FileEntries) Sort() { sort.Sort(files) } 13 | 14 | type ReverseSort struct{ FileEntries } 15 | 16 | func (self ReverseSort) Less(i, j int) bool { 17 | return self.FileEntries[i].path > self.FileEntries[j].path 18 | } 19 | 20 | type MtimeSort struct{ FileEntries } 21 | 22 | func (self MtimeSort) Less(i, j int) bool { 23 | return self.FileEntries[i].info.ModTime().UnixNano() < self.FileEntries[j].info.ModTime().UnixNano() 24 | } 25 | 26 | type MtimeReverseSort struct{ FileEntries } 27 | 28 | func (self MtimeReverseSort) Less(i, j int) bool { 29 | return self.FileEntries[i].info.ModTime().UnixNano() > self.FileEntries[j].info.ModTime().UnixNano() 30 | } 31 | 32 | type SizeSort struct{ FileEntries } 33 | 34 | func (self SizeSort) Less(i, j int) bool { 35 | return self.FileEntries[i].info.Size() > self.FileEntries[i].info.Size() 36 | } 37 | 38 | type SizeReverseSort struct{ FileEntries } 39 | 40 | func (self SizeReverseSort) Less(i, j int) bool { 41 | return self.FileEntries[i].info.Size() < self.FileEntries[i].info.Size() 42 | } 43 | 44 | type ReverseSorter struct { 45 | *BaseWorker 46 | } 47 | 48 | func NewReverseSorter() *ReverseSorter { 49 | return &ReverseSorter{NewBaseWorker()} 50 | } 51 | 52 | func (s *ReverseSorter) Run() { 53 | var entries = s.BufferEntries() 54 | sorter := ReverseSort{entries} 55 | sorter.Sort() 56 | s.FlushEntries(entries) 57 | s.emitEnd() 58 | } 59 | 60 | func (s *ReverseSorter) Start() { 61 | go s.Run() 62 | } 63 | 64 | type MtimeSorter struct { 65 | *BaseWorker 66 | } 67 | 68 | func NewMtimeSorter() *MtimeSorter { 69 | return &MtimeSorter{NewBaseWorker()} 70 | } 71 | 72 | func (s *MtimeSorter) Run() { 73 | var entries = s.BufferEntries() 74 | sorter := MtimeSort{entries} 75 | sorter.Sort() 76 | s.FlushEntries(entries) 77 | s.emitEnd() 78 | } 79 | 80 | func (s *MtimeSorter) Start() { 81 | go s.Run() 82 | } 83 | 84 | type MtimeReverseSorter struct { 85 | *BaseWorker 86 | } 87 | 88 | func NewMtimeReverseSorter() *MtimeReverseSorter { 89 | return &MtimeReverseSorter{NewBaseWorker()} 90 | } 91 | 92 | func (s *MtimeReverseSorter) Start() { 93 | go s.Run() 94 | } 95 | 96 | func (s *MtimeReverseSorter) Run() { 97 | var entries = s.BufferEntries() 98 | sorter := MtimeReverseSort{entries} 99 | sorter.Sort() 100 | s.FlushEntries(entries) 101 | s.emitEnd() 102 | } 103 | 104 | type SizeSorter struct { 105 | *BaseWorker 106 | } 107 | 108 | func NewSizeSorter() *SizeSorter { 109 | return &SizeSorter{NewBaseWorker()} 110 | } 111 | 112 | func (s *SizeSorter) Start() { 113 | go s.Run() 114 | } 115 | 116 | func (s *SizeSorter) Run() { 117 | var entries = s.BufferEntries() 118 | sorter := SizeSort{entries} 119 | sorter.Sort() 120 | s.FlushEntries(entries) 121 | s.emitEnd() 122 | } 123 | 124 | type SizeReverseSorter struct { 125 | *BaseWorker 126 | } 127 | 128 | func NewSizeReverseSorter() *SizeReverseSorter { 129 | return &SizeReverseSorter{NewBaseWorker()} 130 | } 131 | 132 | func (s *SizeReverseSorter) Start() { 133 | go s.Run() 134 | } 135 | 136 | func (s *SizeReverseSorter) Run() { 137 | var entries = s.BufferEntries() 138 | sorter := SizeReverseSort{entries} 139 | sorter.Sort() 140 | s.FlushEntries(entries) 141 | s.emitEnd() 142 | } 143 | -------------------------------------------------------------------------------- /file_ext_filter.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | /* 4 | FileExtFilter is actually a regexp filter that generates the pattern from the extension name. 5 | */ 6 | func NewFileExtFilter(ext string) *RegExpFilter { 7 | return NewRegExpFilterWithPattern("\\." + ext + "$") 8 | } 9 | -------------------------------------------------------------------------------- /file_filter.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | type FileFilter struct { 4 | *BaseWorker 5 | } 6 | 7 | func NewFileFilter() *FileFilter { 8 | return &FileFilter{NewBaseWorker()} 9 | } 10 | 11 | func (f *FileFilter) Start() { 12 | go f.Run() 13 | } 14 | 15 | func (f *FileFilter) Run() { 16 | for { 17 | select { 18 | case <-f.stop: 19 | return 20 | break 21 | case entry := <-f.input: 22 | // end of data 23 | if entry == nil { 24 | f.emitEnd() 25 | return 26 | } 27 | if entry.info.Mode().IsRegular() { 28 | f.output <- entry 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /file_stream.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import ( 4 | "encoding/csv" 5 | "io" 6 | "log" 7 | "os" 8 | ) 9 | 10 | // A channel of file entries 11 | type FileStream chan *FileEntry 12 | 13 | // Create a channel for sending file entries 14 | func NewFileStream() FileStream { 15 | return make(FileStream, 10) 16 | } 17 | 18 | func FileStreamFromChangeLog(input FileStream, file string) { 19 | f, err := os.Open(file) 20 | if err != nil { 21 | panic(err) 22 | } 23 | defer f.Close() 24 | 25 | r := csv.NewReader(f) 26 | for { 27 | columns, err := r.Read() 28 | // Stop at EOF. 29 | if err == io.EOF { 30 | break 31 | } 32 | 33 | // columns: entry.path, entry.newpath, entry.message 34 | entry, err := NewFileEntry(columns[1]) 35 | if err != nil { 36 | log.Println(columns[1], err) 37 | // get next row 38 | continue 39 | } 40 | // reverse the rename 41 | entry.newpath = columns[0] 42 | input <- entry 43 | } 44 | // end the list 45 | input <- nil 46 | } 47 | -------------------------------------------------------------------------------- /fsrename/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014-2016 Yo-An Lin. All rights reserved. 2 | // license that can be found in the LICENSE file. 3 | 4 | /* 5 | NAME 6 | 7 | fsrename 8 | 9 | SYNOPSIS 10 | 11 | fsrename [options] [path...] 12 | 13 | DESCRIPTION 14 | 15 | When `[path...]` is not given, "./" will be used as the default path for scanning files. 16 | 17 | OPTIONS 18 | 19 | FILTER OPTIONS 20 | 21 | -match [regexp] 22 | 23 | pre-filter the files and directories based on the given regular 24 | pattern. 25 | 26 | -contains [string] 27 | 28 | pre-filter the files and directories based on the given string needle. 29 | 30 | -file, -f 31 | 32 | only for files. 33 | 34 | -dir, -d 35 | 36 | only for directories. 37 | 38 | -ext 39 | 40 | find files with matched file extension. 41 | 42 | 43 | REPLACEMENT OPTIONS 44 | 45 | Please note the replacement target only works for the basename of a path. 46 | -replace* and -with* should be combined together to replace the substrings. 47 | 48 | -replace [search], -r [search] 49 | 50 | specify target substring with normal string matching. 51 | 52 | -replace-regexp [regexp], -rr [regexp] 53 | 54 | specify target substring with regular expression matching. 55 | 56 | -with [string], -w [string] 57 | 58 | replacement for the target substring. 59 | 60 | -with-format, -wf [format string] 61 | 62 | replacement with fmt.Sprintf format for the target substring. 63 | 64 | ADDING PREFIX & SUFFIX 65 | 66 | -add-prefix [prefix string] 67 | 68 | -add-suffix [suffix string] 69 | 70 | REPLACE RULE BUILDER OPTIONS 71 | 72 | -trim-prefix [prefix] 73 | 74 | trim filename prefix. 75 | 76 | -trim-suffix [suffix] 77 | 78 | trim filename suffix (this option removes suffix even for filename 79 | extensions). 80 | 81 | -camel 82 | 83 | converts dash/underscore separated filenames into camelcase filenames. 84 | 85 | -underscore 86 | 87 | converts camelcase filesnames into underscore separated filenames. 88 | 89 | COMMON OPTIONS 90 | 91 | -dryrun 92 | 93 | dry run, don't rename, just preview the result. 94 | 95 | -changelog [changelog file] 96 | 97 | records the rename actions in CSV format file. 98 | 99 | -rollback [changelog file] 100 | 101 | rollback the renames from a changelog file. 102 | 103 | QUICK EXAMPLES 104 | 105 | Find files with extension `.php` and replace the substring from the filename. 106 | 107 | fsrename -ext "php" -replace "some" -with "others" src/ 108 | 109 | Replace `Stmt.go` with "_stmt.go" under the current directory: 110 | 111 | fsrename -replace "Stmt.go" -with "_stmt.go" 112 | 113 | Replace `Stmt.go` with "_stmt.go" under directory `src/c6`: 114 | 115 | fsrename -replace "Stmt.go" -with "_stmt.go" src/c6 116 | 117 | Replace `foo` with `bar` from files contains `prefix_` 118 | 119 | fsrename -file -contains prefix_ -replace foo -with bar test 120 | 121 | Or use `-match` to pre-filter the files with regular expression 122 | 123 | fsrename -file -match "[a-z]+" -replace foo -with bar test 124 | 125 | Use regular expression without escaping: 126 | 127 | fsrename -replace-regexp "_[a-z]*.go" -with ".go" src/c6 128 | 129 | fsrename -file -replace-regexp "_[a-z]*.go" -with ".go" src/c6 130 | 131 | fsrename -file -ext go -replace-regexp "[a-z]*" -with "123" src/c6 132 | 133 | fsrename -dir -replace "_xxx" -with "_aaa" src/c6 134 | 135 | fsrename -replace "_xxx" -with "_aaa" -dryrun src/c6 136 | */ 137 | package main 138 | 139 | import ( 140 | "flag" 141 | "fmt" 142 | "log" 143 | "os" 144 | "regexp" 145 | ) 146 | 147 | import "github.com/c9s/fsrename" 148 | 149 | // filter options 150 | var matchOpt = flag.String("match", "", "pre-filter (regular expression without slash '/')") 151 | var containsOpt = flag.String("contains", "", "strings.contains filter") 152 | var extOpt = flag.String("ext", "", "extension name filter") 153 | var fileOnlyOpt = flag.Bool("file", false, "file only") 154 | var fOpt = flag.Bool("f", false, "an alias of file only") 155 | var dirOnlyOpt = flag.Bool("dir", false, "directory only") 156 | var dOpt = flag.Bool("d", false, "an alias of dir only") 157 | 158 | var changelogOpt = flag.String("changelog", "", "the changelog file") 159 | 160 | var addPrefixOpt = flag.String("add-prefix", "", "add prefix") 161 | var addSuffixOpt = flag.String("add-suffix", "", "add suffix") 162 | 163 | // replacement options 164 | var replaceOpt = flag.String("replace", "{nil}", "search") 165 | var rOpt = flag.String("r", "{nil}", "search") 166 | var replaceRegexpOpt = flag.String("replace-regexp", "{nil}", "regular expression replace target") 167 | var replaceReOpt = flag.String("replace-re", "{nil}", "regular expression replace target") 168 | var rrOpt = flag.String("rr", "{nil}", "regular expression replace target") 169 | 170 | var replaceExtOpt = flag.String("replace-ext", "", "replace file extension") 171 | 172 | var withOpt = flag.String("with", "{nil}", "replacement") 173 | var wOpt = flag.String("w", "{nil}", "replacement") 174 | var withFormatOpt = flag.String("with-format", "{nil}", "replacement format") 175 | var wfOpt = flag.String("wf", "{nil}", "replacement format") 176 | 177 | // rule builders 178 | var trimPrefixOpt = flag.String("trim-prefix", "", "trim prefix") 179 | var trimSuffixOpt = flag.String("trim-suffix", "", "trim suffix") 180 | var camelOpt = flag.Bool("camel", false, "Convert substrings to camel cases") 181 | var underscoreOpt = flag.Bool("underscore", false, "Convert substrings to underscore cases") 182 | 183 | var rollbackOpt = flag.String("rollback", "", "rollback renames from a changelog file") 184 | 185 | // runtime option 186 | var dryRunOpt = flag.Bool("dryrun", false, "dry run only") 187 | 188 | var orderOpt = flag.String("order", "", "order by") 189 | 190 | /* 191 | var seqStart = flag.Int("seqstart", 0, "sequence number start with") 192 | */ 193 | func main() { 194 | flag.Parse() 195 | 196 | var chain fsrename.Worker 197 | 198 | input := fsrename.NewFileStream() 199 | 200 | if *rollbackOpt != "" { 201 | chain = fsrename.NewProxy() 202 | } else { 203 | chain = fsrename.NewGlobScanner() 204 | } 205 | chain.SetInput(input) 206 | chain.Start() 207 | 208 | // copy short option to long option 209 | if *rOpt != "{nil}" { 210 | *replaceOpt = *rOpt 211 | } 212 | if *rrOpt != "{nil}" { 213 | *replaceRegexpOpt = *rrOpt 214 | } else if *replaceReOpt != "{nil}" { 215 | *replaceRegexpOpt = *replaceReOpt 216 | } 217 | 218 | if *wOpt != "{nil}" { 219 | *withOpt = *wOpt 220 | } 221 | if *wfOpt != "{nil}" { 222 | *withFormatOpt = *wfOpt 223 | } 224 | 225 | if *fOpt == true { 226 | *fileOnlyOpt = true 227 | } 228 | if *dOpt == true { 229 | *dirOnlyOpt = true 230 | } 231 | 232 | if *fileOnlyOpt == true { 233 | chain = chain.Chain(fsrename.NewFileFilter()) 234 | } 235 | if *dirOnlyOpt == true { 236 | chain = chain.Chain(fsrename.NewDirFilter()) 237 | } 238 | if *extOpt != "" { 239 | chain = chain.Chain(fsrename.NewFileExtFilter(*extOpt)) 240 | } 241 | 242 | if *matchOpt != "" { 243 | chain = chain.Chain(fsrename.NewRegExpFilterWithPattern(*matchOpt)) 244 | } 245 | if *containsOpt != "" { 246 | chain = chain.Chain(fsrename.NewStrContainsFilter(*containsOpt)) 247 | } 248 | 249 | if *fileOnlyOpt && *orderOpt != "" { 250 | switch *orderOpt { 251 | case "reverse": 252 | chain = chain.Chain(fsrename.NewReverseSorter()) 253 | break 254 | case "mtime": 255 | chain = chain.Chain(fsrename.NewMtimeSorter()) 256 | break 257 | case "reverse-mtime": 258 | chain = chain.Chain(fsrename.NewMtimeReverseSorter()) 259 | break 260 | case "size": 261 | chain = chain.Chain(fsrename.NewSizeSorter()) 262 | break 263 | case "reverse-size": 264 | chain = chain.Chain(fsrename.NewSizeReverseSorter()) 265 | break 266 | } 267 | } 268 | 269 | if *trimPrefixOpt != "" { 270 | *replaceRegexpOpt = "^" + regexp.QuoteMeta(*trimPrefixOpt) 271 | *withOpt = "" 272 | } 273 | if *trimSuffixOpt != "" { 274 | *replaceRegexpOpt = regexp.QuoteMeta(*trimSuffixOpt) + "$" 275 | *withOpt = "" 276 | } 277 | 278 | if *camelOpt == true { 279 | chain = chain.Chain(fsrename.NewCamelCaseReplacer()) 280 | } else if *underscoreOpt == true { 281 | chain = chain.Chain(fsrename.NewUnderscoreReplacer()) 282 | } 283 | 284 | if *addPrefixOpt != "" { 285 | chain = chain.Chain(fsrename.NewPrefixAdder(*addPrefixOpt)) 286 | } 287 | if *addSuffixOpt != "" { 288 | chain = chain.Chain(fsrename.NewSuffixAdder(*addSuffixOpt)) 289 | } 290 | 291 | // string replace is enabled 292 | if *replaceOpt != "{nil}" || *replaceRegexpOpt != "{nil}" { 293 | if *withOpt == "{nil}" && *withFormatOpt == "{nil}" { 294 | log.Fatalln("replacement option is required. use -with 'replacement' or -with-format 'format'.") 295 | } 296 | 297 | if *replaceRegexpOpt != "{nil}" { 298 | if *withOpt != "{nil}" { 299 | chain = chain.Chain(fsrename.NewRegExpReplacer(*replaceRegexpOpt, *withOpt)) 300 | } else if *withFormatOpt != "{nil}" { 301 | chain = chain.Chain(fsrename.NewRegExpFormatReplacer(*replaceRegexpOpt, *withFormatOpt)) 302 | } 303 | } else { 304 | if *withOpt != "{nil}" { 305 | chain = chain.Chain(fsrename.NewStrReplacer(*replaceOpt, *withOpt, -1)) 306 | } else if *withFormatOpt != "{nil}" { 307 | chain = chain.Chain(fsrename.NewFormatReplacer(*replaceOpt, *withFormatOpt)) 308 | } 309 | } 310 | 311 | } 312 | 313 | if *replaceExtOpt != "" { 314 | chain = chain.Chain(fsrename.NewExtReplacer(*replaceExtOpt)) 315 | } 316 | 317 | // Always run renamer at the end 318 | chain = chain.Chain(fsrename.NewRenamer(*dryRunOpt)) 319 | 320 | if *changelogOpt != "" { 321 | chain = chain.Chain(fsrename.NewChangeLogWriter(*changelogOpt)) 322 | } 323 | 324 | chain = chain.Chain(fsrename.NewConsolePrinter()) 325 | 326 | if *rollbackOpt != "" { 327 | // send file rename entries from csv files 328 | fsrename.FileStreamFromChangeLog(input, *rollbackOpt) 329 | } else { 330 | var pathArgs = flag.Args() 331 | if len(pathArgs) == 0 { 332 | // runs without any arguments, find files under the current directory 333 | pwd, err := os.Getwd() 334 | if err != nil { 335 | fmt.Println(err) 336 | os.Exit(1) 337 | } 338 | pathArgs = []string{pwd} 339 | } 340 | // send paths 341 | for _, path := range pathArgs { 342 | input <- fsrename.MustNewFileEntry(path) 343 | } 344 | input <- nil 345 | } 346 | 347 | // TODO: use condvar instead receiving the paths... 348 | out := chain.Output() 349 | for { 350 | entry := <-out 351 | if entry == nil { 352 | break 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /glob_scanner.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "path/filepath" 4 | import "os" 5 | 6 | type GlobScanner struct { 7 | *BaseWorker 8 | } 9 | 10 | func NewGlobScanner() *GlobScanner { 11 | return &GlobScanner{NewBaseWorker()} 12 | } 13 | 14 | func (s *GlobScanner) Start() { 15 | go s.Run() 16 | } 17 | 18 | func (s *GlobScanner) Run() { 19 | for { 20 | select { 21 | case <-s.stop: 22 | return 23 | break 24 | case entry := <-s.input: 25 | // end of data 26 | if entry == nil { 27 | s.emitEnd() 28 | return 29 | } 30 | matches, err := filepath.Glob(entry.path) 31 | if err != nil { 32 | return 33 | } 34 | for _, match := range matches { 35 | var err = filepath.Walk(match, func(path string, info os.FileInfo, err error) error { 36 | // ignore the parent path 37 | if path == entry.path { 38 | return err 39 | } 40 | base := filepath.Base(path) 41 | if base == ".svn" || base == ".git" || base == ".hg" { 42 | return filepath.SkipDir 43 | } 44 | if err != nil { 45 | panic(err) 46 | } 47 | s.output <- NewFileEntryWithInfo(path, info) 48 | return err 49 | }) 50 | if err != nil { 51 | panic(err) 52 | } 53 | } 54 | break 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /prefix_filter.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "regexp" 4 | 5 | /* 6 | PrefixFilter is actually a regexp filter that generates the pattern from the prefix 7 | */ 8 | func PrefixFilter(prefix string) *RegExpFilter { 9 | return NewRegExpFilterWithPattern("^" + regexp.QuoteMeta(prefix)) 10 | } 11 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | type Proxy struct { 4 | *BaseWorker 5 | } 6 | 7 | func NewProxy() *Proxy { 8 | return &Proxy{NewBaseWorker()} 9 | } 10 | 11 | func (s *Proxy) Start() { 12 | go s.Run() 13 | } 14 | 15 | func (s *Proxy) Run() { 16 | for { 17 | select { 18 | case <-s.stop: 19 | return 20 | break 21 | case entry := <-s.input: 22 | // end of data 23 | if entry == nil { 24 | s.emitEnd() 25 | return 26 | } 27 | if entry.info == nil { 28 | break 29 | } 30 | s.output <- entry 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /regexp_filter.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "regexp" 4 | 5 | type RegExpFilter struct { 6 | *BaseWorker 7 | Matcher *regexp.Regexp 8 | } 9 | 10 | func NewRegExpFilter(matcher *regexp.Regexp) *RegExpFilter { 11 | return &RegExpFilter{NewBaseWorker(), matcher} 12 | } 13 | 14 | func NewRegExpFilterWithPattern(pattern string) *RegExpFilter { 15 | matcher := regexp.MustCompile(pattern) 16 | return &RegExpFilter{NewBaseWorker(), matcher} 17 | } 18 | func (w *RegExpFilter) Start() { 19 | go w.Run() 20 | } 21 | 22 | func (w *RegExpFilter) Run() { 23 | for { 24 | select { 25 | case <-w.stop: 26 | return 27 | case entry := <-w.input: 28 | // end of data 29 | if entry == nil { 30 | w.emitEnd() 31 | return 32 | } 33 | if w.Matcher.MatchString(entry.base) { 34 | w.output <- entry 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /renamer.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "os" 4 | 5 | // The rename action 6 | type Rename struct { 7 | dryrun bool 8 | } 9 | 10 | func (r *Rename) Act(entry *FileEntry) bool { 11 | if entry.newpath == "" || entry.newpath == entry.path { 12 | entry.message = "unchanged" 13 | return false 14 | } 15 | 16 | stat, err := os.Stat(entry.newpath) 17 | if os.IsExist(err) { 18 | entry.message = "file exists, ignore" 19 | return false 20 | } 21 | _ = stat 22 | 23 | if r.dryrun == false { 24 | os.Rename(entry.path, entry.newpath) 25 | } 26 | entry.message = "success" 27 | return true 28 | } 29 | 30 | func NewRenamer(dryrun bool) *Actor { 31 | return NewActor(&Rename{dryrun}) 32 | } 33 | -------------------------------------------------------------------------------- /replacer.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "regexp" 4 | 5 | func NewStrReplacer(search, replace string, n int) *Actor { 6 | return NewActor(NewStrReplaceAction(search, replace, n)) 7 | } 8 | 9 | func NewFormatReplacer(search, replaceFormat string) *Actor { 10 | return NewActor(NewStrFormatReplaceAction(search, replaceFormat)) 11 | } 12 | 13 | func NewCamelCaseReplacer() *Actor { 14 | return NewActor(NewCamelCaseAction("[-_]+")) 15 | } 16 | 17 | func NewUnderscoreReplacer() *Actor { 18 | return NewActor(NewUnderscoreAction()) 19 | } 20 | 21 | func NewPrefixAdder(prefix string) *Actor { 22 | return NewActor(NewPrefixAction(prefix)) 23 | } 24 | 25 | func NewSuffixAdder(suffix string) *Actor { 26 | return NewActor(NewSuffixAction(suffix)) 27 | } 28 | 29 | func NewExtReplacer(ext string) *Actor { 30 | return NewActor(NewExtReplaceAction(ext)) 31 | } 32 | 33 | func NewRegExpReplacer(pattern, replace string) *Actor { 34 | matcher := regexp.MustCompile(pattern) 35 | return NewActor(NewRegExpReplaceAction(matcher, replace)) 36 | } 37 | 38 | func NewRegExpFormatReplacer(pattern, replace string) *Actor { 39 | matcher := regexp.MustCompile(pattern) 40 | return NewActor(NewRegExpFormatReplaceAction(matcher, replace)) 41 | } 42 | -------------------------------------------------------------------------------- /sequence.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "sync/atomic" 4 | 5 | type Sequence struct { 6 | Start int32 7 | Last int32 8 | Step int32 9 | } 10 | 11 | func NewSequence(start int32, step int32) *Sequence { 12 | return &Sequence{start, start, step} 13 | } 14 | 15 | func (s *Sequence) Reset() { 16 | atomic.StoreInt32(&s.Last, 0) 17 | } 18 | 19 | func (s *Sequence) Next() int32 { 20 | atomic.AddInt32(&s.Last, 1) 21 | return s.Last 22 | } 23 | -------------------------------------------------------------------------------- /sequence_test.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "testing" 4 | import "github.com/stretchr/testify/assert" 5 | 6 | func TestSequence(t *testing.T) { 7 | seq := NewSequence(0, 1) 8 | var id int32 9 | id = seq.Next() 10 | assert.Equal(t, id, int32(1)) 11 | 12 | id = seq.Next() 13 | assert.Equal(t, id, int32(2)) 14 | 15 | id = seq.Next() 16 | assert.Equal(t, id, int32(3)) 17 | 18 | seq.Reset() 19 | id = seq.Next() 20 | assert.Equal(t, id, int32(1)) 21 | 22 | id = seq.Next() 23 | assert.Equal(t, id, int32(2)) 24 | } 25 | -------------------------------------------------------------------------------- /suffix_filter.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "regexp" 4 | 5 | /* 6 | SuffixFilter is actually a regexp filter that generates the pattern from the prefix 7 | */ 8 | func SuffixFilter(suffix string) *RegExpFilter { 9 | return NewRegExpFilterWithPattern(regexp.QuoteMeta(suffix) + "$") 10 | } 11 | -------------------------------------------------------------------------------- /tests/CamelCase.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/CamelCase.php -------------------------------------------------------------------------------- /tests/autoload.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/autoload.php -------------------------------------------------------------------------------- /tests/class-name.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/class-name.php -------------------------------------------------------------------------------- /tests/file.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/file.html -------------------------------------------------------------------------------- /tests/file.html.twig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/file.html.twig -------------------------------------------------------------------------------- /tests/foo-bar.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/foo-bar.php -------------------------------------------------------------------------------- /tests/php_files/test1.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/php_files/test1.php -------------------------------------------------------------------------------- /tests/php_files/test2.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/php_files/test2.php -------------------------------------------------------------------------------- /tests/php_files/test2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/php_files/test2.txt -------------------------------------------------------------------------------- /tests/php_files/test2.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/php_files/test2.xml -------------------------------------------------------------------------------- /tests/scanner/bar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/scanner/bar -------------------------------------------------------------------------------- /tests/scanner/foo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/scanner/foo -------------------------------------------------------------------------------- /tests/scanner/zoo/x: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/scanner/zoo/x -------------------------------------------------------------------------------- /tests/scanner/zoo/y: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/scanner/zoo/y -------------------------------------------------------------------------------- /tests/scanner/zoo/z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/scanner/zoo/z -------------------------------------------------------------------------------- /tests/test1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/test1.txt -------------------------------------------------------------------------------- /tests/test2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/test2.txt -------------------------------------------------------------------------------- /tests/test3.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9s/fsrename/f33953aeac4b11431158e43860a70424e5453107/tests/test3.txt -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | -------------------------------------------------------------------------------- /worker.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | // CondVar is used in Done, Stop signals 4 | type CondVar chan bool 5 | 6 | // Worker interface defines the implementation for 7 | // streaming the file entries. 8 | type Worker interface { 9 | // Set the input stream 10 | SetInput(input FileStream) 11 | 12 | // Set the output stream 13 | SetOutput(output FileStream) 14 | 15 | // Get the input stream 16 | Input() FileStream 17 | 18 | // Get the output stream 19 | Output() FileStream 20 | 21 | // Send stop signal to the listener go routine 22 | Stop() 23 | 24 | Start() 25 | 26 | // Start listening the input channel in the background 27 | Run() 28 | 29 | // Append a next worker to the next worker list 30 | // Returns the connected worker object 31 | Chain(nextWorker Worker) Worker 32 | } 33 | 34 | // Base Worker implements the common methods of a worker 35 | type BaseWorker struct { 36 | input FileStream 37 | output FileStream 38 | stop CondVar 39 | nextWorkers []Worker 40 | } 41 | 42 | // NewBaseWorker creates a base worker object with a default stop signal 43 | // channel 44 | func NewBaseWorker() *BaseWorker { 45 | return &BaseWorker{stop: make(CondVar, 1)} 46 | } 47 | 48 | // Stop a worker 49 | func (w *BaseWorker) Stop() { 50 | // send stop signal to self 51 | w.stop <- true 52 | if len(w.nextWorkers) > 0 { 53 | for _, worker := range w.nextWorkers { 54 | worker.Stop() 55 | } 56 | } 57 | } 58 | 59 | // emitEnd sends nil to the next workers 60 | func (w *BaseWorker) emitEnd() { 61 | if len(w.nextWorkers) > 0 { 62 | for _, worker := range w.nextWorkers { 63 | if nin := worker.Input(); nin != nil { 64 | nin <- nil 65 | } 66 | } 67 | } 68 | w.output <- nil 69 | } 70 | 71 | func (w *BaseWorker) Input() FileStream { 72 | return w.input 73 | } 74 | 75 | func (w *BaseWorker) Output() FileStream { 76 | return w.output 77 | } 78 | 79 | func (w *BaseWorker) SetInput(input FileStream) { 80 | w.input = input 81 | } 82 | 83 | func (w *BaseWorker) SetOutput(output FileStream) { 84 | w.output = output 85 | } 86 | 87 | // Chain() connects the next worker to children workers 88 | // setup the parent output channel and the child input channel. 89 | func (w *BaseWorker) Chain(nextWorker Worker) Worker { 90 | if w.output == nil { 91 | w.output = NewFileStream() 92 | } 93 | // override the input in the worker 94 | nextWorker.SetInput(w.output) 95 | 96 | if o := nextWorker.Output(); o == nil { 97 | nextWorker.SetOutput(NewFileStream()) 98 | } 99 | w.nextWorkers = append(w.nextWorkers, nextWorker) 100 | nextWorker.Start() 101 | return nextWorker 102 | } 103 | 104 | // Buffer all entries for further operations 105 | // Returns FileEntries 106 | func (w *BaseWorker) BufferEntries() []FileEntry { 107 | var entries []FileEntry 108 | for { 109 | select { 110 | case <-w.stop: 111 | return entries 112 | case entry := <-w.input: 113 | // end of data 114 | if entry == nil { 115 | return entries 116 | } 117 | entries = append(entries, *entry) 118 | } 119 | } 120 | return entries 121 | } 122 | 123 | func (w *BaseWorker) FlushEntries(entries []FileEntry) { 124 | for i, _ := range entries { 125 | w.output <- &entries[i] 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /worker_test.go: -------------------------------------------------------------------------------- 1 | package fsrename 2 | 3 | import "testing" 4 | import "github.com/stretchr/testify/assert" 5 | 6 | func TestScanner(t *testing.T) { 7 | input := NewFileStream() 8 | output := NewFileStream() 9 | worker := NewGlobScanner() 10 | worker.SetInput(input) 11 | worker.SetOutput(output) 12 | worker.Start() 13 | 14 | input <- MustNewFileEntry("tests/scanner") 15 | input <- nil 16 | assert.NotNil(t, output) 17 | 18 | entries := readEntries(t, output) 19 | assert.Equal(t, 6, len(entries)) 20 | } 21 | 22 | func TestFilterWorkerEmtpy(t *testing.T) { 23 | input := NewFileStream() 24 | output := NewFileStream() 25 | worker := NewFileFilter() 26 | worker.SetInput(input) 27 | worker.SetOutput(output) 28 | worker.Start() 29 | 30 | e, err := NewFileEntry("tests/autoload.php") 31 | assert.Nil(t, err) 32 | input <- e 33 | input <- nil 34 | assert.NotNil(t, output) 35 | entries := readEntries(t, output) 36 | assert.Equal(t, 1, len(entries)) 37 | } 38 | 39 | func TestSimpleRegExpOnPHPFiles(t *testing.T) { 40 | input := NewFileStream() 41 | scanner := NewGlobScanner() 42 | scanner.SetInput(input) 43 | scanner.Start() 44 | chain := scanner.Chain(NewFileFilter()).Chain(NewRegExpFilterWithPattern("\\.php$")) 45 | 46 | input <- MustNewFileEntry("tests/php_files") 47 | input <- nil 48 | 49 | output := chain.Output() 50 | assert.NotNil(t, output) 51 | entries := readEntries(t, output) 52 | assert.Equal(t, 2, len(entries)) 53 | } 54 | 55 | func TestSimpleFilePipe(t *testing.T) { 56 | input := NewFileStream() 57 | scanner := NewGlobScanner() 58 | scanner.SetInput(input) 59 | scanner.Start() 60 | 61 | filter := scanner.Chain(&FileFilter{ 62 | &BaseWorker{stop: make(CondVar, 1)}, 63 | }) 64 | 65 | input <- MustNewFileEntry("tests/scanner") 66 | input <- nil 67 | output := filter.Output() 68 | assert.NotNil(t, output) 69 | entries := readEntries(t, output) 70 | assert.Equal(t, 5, len(entries)) 71 | } 72 | 73 | func TestSimpleReverseSorter(t *testing.T) { 74 | input := NewFileStream() 75 | scanner := NewGlobScanner() 76 | scanner.SetInput(input) 77 | scanner.Start() 78 | chain := scanner. 79 | Chain(NewFileFilter()). 80 | Chain(NewReverseSorter()) 81 | 82 | input <- MustNewFileEntry("tests") 83 | input <- nil 84 | 85 | output := chain.Output() 86 | assert.NotNil(t, output) 87 | readEntries(t, output) 88 | } 89 | 90 | func readEntries(t *testing.T, input chan *FileEntry) []FileEntry { 91 | var entries []FileEntry 92 | for { 93 | select { 94 | case entry := <-input: 95 | t.Log("entry", entry) 96 | if entry == nil { 97 | return entries 98 | } 99 | entries = append(entries, *entry) 100 | break 101 | } 102 | } 103 | return entries 104 | } 105 | --------------------------------------------------------------------------------