1// Copyright © 2013-2021 Wei Shen <shenwei356@gmail.com>
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19// THE SOFTWARE.
20
21package main
22
23import (
24	"bufio"
25	"fmt"
26	"io"
27	"io/ioutil"
28	"net/http"
29	"os"
30	"path/filepath"
31	"regexp"
32	"runtime"
33	"strings"
34
35	"github.com/fatih/color"
36	"github.com/mattn/go-colorable"
37	"github.com/shenwei356/breader"
38	"github.com/shenwei356/go-logging"
39	"github.com/shenwei356/natsort"
40	"github.com/shenwei356/util/pathutil"
41	"github.com/spf13/cobra"
42)
43
44var log *logging.Logger
45
46var version = "2.11.1"
47var app = "brename"
48
49// for detecting one case where two or more files are renamed to same new path
50var pathTree map[string]struct{}
51
52// Options is the struct containing all global options
53type Options struct {
54	Quiet   bool
55	Verbose int
56	Version bool
57	DryRun  bool
58
59	Pattern      string
60	PatternRe    *regexp.Regexp
61	Replacement  string
62	Recursive    bool
63	IncludingDir bool
64	OnlyDir      bool
65	MaxDepth     int
66	IgnoreCase   bool
67	IgnoreExt    bool
68
69	IncludeFilters   []string
70	ExcludeFilters   []string
71	IncludeFilterRes []*regexp.Regexp
72	ExcludeFilterRes []*regexp.Regexp
73
74	ListPath    bool
75	ListPathSep string
76	ListAbsPath bool
77	NatureSort  bool
78
79	ReplaceWithNR bool
80	StartNum      int
81	NRFormat      string
82
83	ReplaceWithKV bool
84	KVs           map[string]string
85	KVFile        string
86	KeepKey       bool
87	KeyCaptIdx    int
88	KeyMissRepl   string
89
90	OverwriteMode int
91
92	Undo             bool
93	ForceUndo        bool
94	LastOpDetailFile string
95}
96
97var reNR = regexp.MustCompile(`\{(NR|nr)\}`)
98var reKV = regexp.MustCompile(`\{(KV|kv)\}`)
99
100func getOptions(cmd *cobra.Command) *Options {
101	quiet := getFlagBool(cmd, "quiet")
102	undo := getFlagBool(cmd, "undo")
103	forceUndo := getFlagBool(cmd, "force-undo")
104	if undo || forceUndo {
105		return &Options{
106			Undo:             true, // set it true even only force-undo given
107			Quiet:            quiet,
108			ForceUndo:        forceUndo,
109			LastOpDetailFile: ".brename_detail.txt",
110		}
111	}
112
113	version := getFlagBool(cmd, "version")
114	if version {
115		checkVersion()
116		return &Options{Version: version}
117	}
118
119	pattern := getFlagString(cmd, "pattern")
120	if pattern == "" {
121		log.Errorf("flag -p/--pattern needed")
122		os.Exit(1)
123	}
124	p := pattern
125	ignoreCase := getFlagBool(cmd, "ignore-case")
126	if ignoreCase {
127		p = "(?i)" + p
128	}
129	re, err := regexp.Compile(p)
130	if err != nil {
131		log.Errorf("illegal regular expression for search pattern: %s", pattern)
132		os.Exit(1)
133	}
134
135	rewildcard := regexp.MustCompile(`^\*`)
136
137	infilters := getFlagStringSlice(cmd, "include-filters")
138	infilterRes := make([]*regexp.Regexp, 0, 10)
139	var infilterRe *regexp.Regexp
140	for _, infilter := range infilters {
141		if infilter == "" {
142			log.Errorf("value of flag -f/--include-filters missing")
143			os.Exit(1)
144		}
145		if rewildcard.MatchString(infilter) {
146			log.Warningf("Are you using wildcard for -f/--include-filters? It should be regular expression: %s", infilter)
147		}
148		if !(infilter == "./" || infilter == "." || infilter == "..") {
149			existed, err := pathutil.Exists(infilter)
150			if err != nil {
151				log.Warningf("something wrong when trying to check whether %s is a existed file", infilter)
152			}
153			if existed {
154				log.Warningf("Seems you are using wildcard for -f/--include-filters? Make sure using regular expression: %s", infilter)
155			}
156		}
157
158		if ignoreCase {
159			infilterRe, err = regexp.Compile("(?i)" + infilter)
160		} else {
161			infilterRe, err = regexp.Compile(infilter)
162		}
163		if err != nil {
164			log.Errorf("illegal regular expression for include filter: %s", infilter)
165			os.Exit(1)
166		}
167		infilterRes = append(infilterRes, infilterRe)
168	}
169
170	exfilters := getFlagStringSlice(cmd, "exclude-filters")
171	exfilterRes := make([]*regexp.Regexp, 0, 10)
172	var exfilterRe *regexp.Regexp
173	for _, exfilter := range exfilters {
174		if exfilter == "" {
175			log.Errorf("value of flag -F/--exclude-filters missing")
176			os.Exit(1)
177		}
178		if rewildcard.MatchString(exfilter) {
179			log.Warningf("Are you using wildcard for -F/--exclude-filters? It should be regular expression: %s", exfilter)
180		}
181		if !(exfilter == "./" || exfilter == "." || exfilter == "..") {
182			existed, err := pathutil.Exists(exfilter)
183			if err != nil {
184				log.Warningf("something wrong when trying to check whether %s is a existed file", exfilter)
185			}
186			if existed {
187				log.Warningf("Seems you are using wildcard for -F/--exclude-filters? Make sure using regular expression: %s", exfilter)
188			}
189		}
190
191		if ignoreCase {
192			exfilterRe, err = regexp.Compile("(?i)" + exfilter)
193		} else {
194			exfilterRe, err = regexp.Compile(exfilter)
195		}
196		if err != nil {
197			log.Errorf("illegal regular expression for exclude filter: %s", exfilter)
198			os.Exit(1)
199		}
200		exfilterRes = append(exfilterRes, exfilterRe)
201	}
202
203	replacement := getFlagString(cmd, "replacement")
204	kvFile := getFlagString(cmd, "kv-file")
205
206	if kvFile != "" {
207		if len(replacement) == 0 {
208			checkError(fmt.Errorf("flag -r/--replacement needed when given flag -k/--kv-file"))
209		}
210		if !reKV.MatchString(replacement) {
211			checkError(fmt.Errorf(`replacement symbol "{kv}"/"{KV}" not found in value of flag -r/--replacement when flag -k/--kv-file given`))
212		}
213	}
214
215	var replaceWithNR bool
216	if reNR.MatchString(replacement) {
217		replaceWithNR = true
218	}
219
220	var replaceWithKV bool
221	var kvs map[string]string
222	keepKey := getFlagBool(cmd, "keep-key")
223	keyMissRepl := getFlagString(cmd, "key-miss-repl")
224	if reKV.MatchString(replacement) {
225		replaceWithKV = true
226		if !regexp.MustCompile(`\(.+\)`).MatchString(pattern) {
227			checkError(fmt.Errorf(`value of -p/--pattern must contains "(" and ")" to capture data which is used specify the KEY`))
228		}
229		if kvFile == "" {
230			checkError(fmt.Errorf(`since replacement symbol "{kv}"/"{KV}" found in value of flag -r/--replacement, tab-delimited key-value file should be given by flag -k/--kv-file`))
231		}
232
233		if keepKey && keyMissRepl != "" && !quiet {
234			log.Warning("flag -m/--key-miss-repl ignored when flag -K/--keep-key given")
235		}
236		if !quiet {
237			log.Infof("read key-value file: %s", kvFile)
238		}
239		kvs, err = readKVs(kvFile, ignoreCase)
240		if err != nil {
241			checkError(fmt.Errorf("read key-value file: %s", err))
242		}
243		if len(kvs) == 0 {
244			checkError(fmt.Errorf("no valid data in key-value file: %s", kvFile))
245		}
246
247		if !quiet {
248			log.Infof("%d pairs of key-value loaded", len(kvs))
249		}
250	}
251
252	verbose := getFlagNonNegativeInt(cmd, "verbose")
253	if verbose > 2 {
254		log.Errorf("illegal value of flag --verbose: %d, only 0/1/2 allowed", verbose)
255		os.Exit(1)
256	}
257
258	overwriteMode := getFlagNonNegativeInt(cmd, "overwrite-mode")
259	if overwriteMode > 2 {
260		log.Errorf("illegal value of flag -o/--overwrite-mode: %d, only 0/1/2 allowed", overwriteMode)
261		os.Exit(1)
262	}
263
264	if !quiet {
265		log.Info("main options:")
266		log.Infof("  ignore case: %v", ignoreCase)
267		log.Infof("  search pattern: %s", p)
268		if len(infilters) > 0 {
269			log.Infof("  include filters: %s", strings.Join(infilters, ", "))
270		}
271		if len(exfilters) > 0 {
272			log.Infof("  exclude filters: %s", strings.Join(exfilters, ", "))
273		}
274	}
275
276	return &Options{
277		Quiet:   quiet,
278		Verbose: verbose,
279		Version: version,
280		DryRun:  getFlagBool(cmd, "dry-run"),
281
282		Pattern:      pattern,
283		PatternRe:    re,
284		Replacement:  replacement,
285		Recursive:    getFlagBool(cmd, "recursive"),
286		IncludingDir: getFlagBool(cmd, "including-dir"),
287		OnlyDir:      getFlagBool(cmd, "only-dir"),
288		MaxDepth:     getFlagNonNegativeInt(cmd, "max-depth"),
289		IgnoreCase:   ignoreCase,
290		IgnoreExt:    getFlagBool(cmd, "ignore-ext"),
291
292		IncludeFilters:   infilters,
293		IncludeFilterRes: infilterRes,
294		ExcludeFilters:   infilters,
295		ExcludeFilterRes: exfilterRes,
296
297		ListPath:    getFlagBool(cmd, "list"),
298		ListPathSep: getFlagString(cmd, "list-sep"),
299		ListAbsPath: getFlagBool(cmd, "list-abs"),
300		NatureSort:  getFlagBool(cmd, "nature-sort"),
301
302		ReplaceWithNR: replaceWithNR,
303		StartNum:      getFlagNonNegativeInt(cmd, "start-num"),
304		NRFormat:      fmt.Sprintf("%%0%dd", getFlagPositiveInt(cmd, "nr-width")),
305		ReplaceWithKV: replaceWithKV,
306
307		KVs:         kvs,
308		KVFile:      kvFile,
309		KeepKey:     keepKey,
310		KeyCaptIdx:  getFlagPositiveInt(cmd, "key-capt-idx"),
311		KeyMissRepl: keyMissRepl,
312
313		OverwriteMode: overwriteMode,
314
315		Undo:             false,
316		LastOpDetailFile: ".brename_detail.txt",
317	}
318}
319
320func init() {
321	logFormat := logging.MustStringFormatter(`%{color}[%{level:.4s}]%{color:reset} %{message}`)
322	var stderr io.Writer = os.Stderr
323	if runtime.GOOS == "windows" {
324		stderr = colorable.NewColorableStderr()
325	}
326	backend := logging.NewLogBackend(stderr, "", 0)
327	backendFormatter := logging.NewBackendFormatter(backend, logFormat)
328	logging.SetBackend(backendFormatter)
329	log = logging.MustGetLogger(app)
330
331	RootCmd.Flags().BoolP("quiet", "q", false, "be quiet, do not show information and warning")
332	RootCmd.Flags().IntP("verbose", "v", 0, "verbose level (0 for all, 1 for warning and error, 2 for only error) (default 0)")
333	RootCmd.Flags().BoolP("version", "V", false, "print version information and check for update")
334	RootCmd.Flags().BoolP("dry-run", "d", false, "print rename operations but do not run")
335
336	RootCmd.Flags().StringP("pattern", "p", "", "search pattern (regular expression)")
337	RootCmd.Flags().StringP("replacement", "r", "", `replacement. capture variables supported.  e.g. $1 represents the first submatch. ATTENTION: for *nix OS, use SINGLE quote NOT double quotes or use the \ escape character. Ascending integer is also supported by "{nr}"`)
338	RootCmd.Flags().BoolP("recursive", "R", false, "rename recursively")
339	RootCmd.Flags().BoolP("including-dir", "D", false, "rename directories")
340	RootCmd.Flags().BoolP("only-dir", "", false, "only rename directories")
341	RootCmd.Flags().IntP("max-depth", "", 0, "maximum depth for recursive search (0 for no limit)")
342	RootCmd.Flags().BoolP("ignore-case", "i", false, "ignore case of -p/--pattern, -f/--include-filters and -F/--exclude-filters")
343	RootCmd.Flags().BoolP("ignore-ext", "e", false, "ignore file extension. i.e., replacement does not change file extension")
344
345	RootCmd.Flags().StringSliceP("include-filters", "f", []string{"."}, `include file filter(s) (regular expression, NOT wildcard). multiple values supported, e.g., -f ".html" -f ".htm", but ATTENTION: comma in filter is treated as separator of multiple filters`)
346	RootCmd.Flags().StringSliceP("exclude-filters", "F", []string{}, `exclude file filter(s) (regular expression, NOT wildcard). multiple values supported, e.g., -F ".html" -F ".htm", but ATTENTION: comma in filter is treated as separator of multiple filters`)
347
348	RootCmd.Flags().BoolP("list", "l", false, `only list paths that match pattern`)
349	RootCmd.Flags().StringP("list-sep", "s", "\n", `separator for list of found paths`)
350	RootCmd.Flags().BoolP("list-abs", "a", false, `list absolute path, using along with -l/--list`)
351	RootCmd.Flags().BoolP("nature-sort", "N", false, `list paths in nature sort, using along with -l/--list`)
352
353	RootCmd.Flags().StringP("kv-file", "k", "",
354		`tab-delimited key-value file for replacing key with value when using "{kv}" in -r (--replacement)`)
355	RootCmd.Flags().BoolP("keep-key", "K", false, "keep the key as value when no value found for the key")
356	RootCmd.Flags().IntP("key-capt-idx", "I", 1, "capture variable index of key (1-based)")
357	RootCmd.Flags().StringP("key-miss-repl", "m", "", "replacement for key with no corresponding value")
358	RootCmd.Flags().IntP("start-num", "n", 1, `starting number when using {nr} in replacement`)
359	RootCmd.Flags().IntP("nr-width", "", 1, `minimum width for {nr} in flag -r/--replacement. e.g., formating "1" to "001" by --nr-width 3`)
360
361	RootCmd.Flags().IntP("overwrite-mode", "o", 0, "overwrite mode (0 for reporting error, 1 for overwrite, 2 for not renaming) (default 0)")
362
363	RootCmd.Flags().BoolP("undo", "u", false, "undo the LAST successful operation")
364	RootCmd.Flags().BoolP("force-undo", "U", false, "continue undo even when some operations failed")
365
366	RootCmd.Example = `  1. dry run and showing potential dangerous operations
367      brename -p "abc" -d
368  2. dry run and only show operations that will cause error
369      brename -p "abc" -d -v 2
370  3. only renaming specific paths via include filters
371      brename -p ":" -r "-" -f ".htm$" -f ".html$"
372  4. renaming all .jpeg files to .jpg in all subdirectories
373      brename -p "\.jpeg" -r ".jpg" -R   dir
374  5. using capture variables, e.g., $1, $2 ...
375      brename -p "(a)" -r "\$1\$1"
376      or brename -p "(a)" -r '$1$1' in Linux/Mac OS X
377  6. renaming directory too
378      brename -p ":" -r "-" -R -D   pdf-dirs
379  7. using key-value file
380      brename -p "(.+)" -r "{kv}" -k kv.tsv
381  8. do not touch file extension
382      brename -p ".+" -r "{nr}" -f .mkv -f .mp4 -e
383  9. only list paths that match pattern (-l)
384      brename -i -f '.docx?$' -p . -R -l
385  10. undo the LAST successful operation
386      brename -u
387
388  More examples: https://github.com/shenwei356/brename`
389
390	RootCmd.SetUsageTemplate(`Usage:{{if .Runnable}}
391  {{if .HasAvailableFlags}}{{appendIfNotPresent .UseLine "[path ...]"}}{{else}}{{.UseLine}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
392  {{ .CommandPath}} [command]{{end}} {{if gt .Aliases 0}}
393
394Aliases:
395  {{.NameAndAliases}}
396{{end}}{{if .HasExample}}
397
398Examples:
399{{ .Example }}{{end}}{{ if .HasAvailableSubCommands}}
400
401Available Commands:{{range .Commands}}{{if .IsAvailableCommand}}
402  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{ if .HasAvailableLocalFlags}}
403
404Flags:
405{{.LocalFlags.FlagUsages | trimRightSpace}}{{end}}{{ if .HasAvailableInheritedFlags}}
406
407Global Flags:
408{{.InheritedFlags.FlagUsages | trimRightSpace}}{{end}}{{if .HasHelpSubCommands}}
409
410Additional help topics:{{range .Commands}}{{if .IsHelpCommand}}
411  {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{ if .HasAvailableSubCommands }}
412
413Use "{{.CommandPath}} --help" for more information about a command.{{end}}
414`)
415
416	pathTree = make(map[string]struct{}, 1000)
417}
418
419func main() {
420	if err := RootCmd.Execute(); err != nil {
421		log.Error(err)
422		os.Exit(1)
423	}
424}
425
426func checkError(err error) {
427	if err != nil {
428		log.Error(err)
429		os.Exit(1)
430	}
431}
432
433func getFileList(args []string) []string {
434	files := []string{}
435	if len(args) == 0 {
436		files = append(files, "./")
437	} else {
438		for _, file := range args {
439			if file == "./" || file == "." || file == ".." {
440				continue
441			}
442			if _, err := os.Stat(file); os.IsNotExist(err) {
443				log.Errorf("given search paths not existed: %s", file)
444			}
445
446			files = append(files, file)
447		}
448	}
449	return files
450}
451
452func getFlagBool(cmd *cobra.Command, flag string) bool {
453	value, err := cmd.Flags().GetBool(flag)
454	checkError(err)
455	return value
456}
457
458func getFlagString(cmd *cobra.Command, flag string) string {
459	value, err := cmd.Flags().GetString(flag)
460	checkError(err)
461	return value
462}
463
464func getFlagStringSlice(cmd *cobra.Command, flag string) []string {
465	value, err := cmd.Flags().GetStringSlice(flag)
466	checkError(err)
467	return value
468}
469
470func getFlagPositiveInt(cmd *cobra.Command, flag string) int {
471	value, err := cmd.Flags().GetInt(flag)
472	checkError(err)
473	if value <= 0 {
474		checkError(fmt.Errorf("value of flag --%s should be greater than 0", flag))
475	}
476	return value
477}
478
479func getFlagNonNegativeInt(cmd *cobra.Command, flag string) int {
480	value, err := cmd.Flags().GetInt(flag)
481	checkError(err)
482	if value < 0 {
483		checkError(fmt.Errorf("value of flag --%s should be greater than or equal to 0", flag))
484	}
485	return value
486}
487
488func checkVersion() {
489	fmt.Printf("%s v%s\n", app, version)
490	fmt.Println("\nChecking new version...")
491
492	resp, err := http.Get(fmt.Sprintf("https://github.com/shenwei356/%s/releases/latest", app))
493	if err != nil {
494		checkError(fmt.Errorf("Network error"))
495	}
496	items := strings.Split(resp.Request.URL.String(), "/")
497	var v string
498	if items[len(items)-1] == "" {
499		v = items[len(items)-2]
500	} else {
501		v = items[len(items)-1]
502	}
503	if v == "v"+version {
504		fmt.Printf("You are using the latest version of %s\n", app)
505	} else {
506		fmt.Printf("New version available: %s %s at %s\n", app, v, resp.Request.URL.String())
507	}
508}
509
510// RootCmd represents the base command when called without any subcommands
511var RootCmd = &cobra.Command{
512	Use:   app,
513	Short: "a cross-platform command-line tool for safely batch renaming files/directories via regular expression",
514	Long: fmt.Sprintf(`
515brename -- a practical cross-platform command-line tool for safely batch renaming files/directories via regular expression
516
517Version: %s
518
519Author: Wei Shen <shenwei356@gmail.com>
520
521Homepage: https://github.com/shenwei356/brename
522
523Attention:
524  1. Paths starting with "." are ignored.
525  2. Flag -f/--include-filters and -F/--exclude-filters support multiple values,
526     e.g., -f ".html" -f ".htm".
527     But ATTENTION: comma in filter is treated as separator of multiple filters.
528
529Special replacement symbols:
530
531  {nr}    Ascending integer
532  {kv}    Corresponding value of the key (captured variable $n) by key-value file,
533          n can be specified by flag -I/--key-capt-idx (default: 1)
534
535
536`, version),
537	Run: func(cmd *cobra.Command, args []string) {
538		// var err error
539		opt := getOptions(cmd)
540
541		if opt.Version {
542			return
543		}
544
545		var delimiter = "\t_shenwei356-brename_\t"
546		if opt.Undo {
547			existed, err := pathutil.Exists(opt.LastOpDetailFile)
548			checkError(err)
549			if !existed {
550				if !opt.Quiet {
551					log.Infof("no brename operation to undo")
552				}
553				return
554			}
555
556			history := make([]operation, 0, 1000)
557
558			fn := func(line string) (interface{}, bool, error) {
559				line = strings.TrimRight(line, "\n")
560				if line == "" || line[0] == '#' { // ignoring blank line and comment line
561					return "", false, nil
562				}
563				items := strings.Split(line, delimiter)
564				if len(items) != 2 {
565					return items, false, nil
566				}
567				return operation{source: items[0], target: items[1], code: 0}, true, nil
568			}
569
570			var reader *breader.BufferedReader
571			reader, err = breader.NewBufferedReader(opt.LastOpDetailFile, 2, 100, fn)
572			checkError(err)
573
574			var op operation
575			for chunk := range reader.Ch {
576				checkError(chunk.Err)
577				for _, data := range chunk.Data {
578					op = data.(operation)
579					history = append(history, op)
580				}
581			}
582			if len(history) == 0 {
583				if !opt.Quiet {
584					log.Infof("no brename operation to undo")
585				}
586				return
587			}
588
589			n := 0
590			for i := len(history) - 1; i >= 0; i-- {
591				op = history[i]
592
593				err = os.Rename(op.target, op.source)
594				if err != nil {
595					log.Errorf(`fail to rename: '%s' -> '%s': %s`, op.source, op.target, err)
596					if !opt.ForceUndo {
597						if !opt.Quiet {
598							log.Infof("%d path(s) renamed", n)
599						}
600						os.Exit(1)
601					}
602				}
603				n++
604				if !opt.Quiet {
605					log.Infof("rename back: '%s' -> '%s'", op.target, op.source)
606				}
607			}
608			if !opt.Quiet {
609				log.Infof("%d path(s) renamed", n)
610			}
611
612			checkError(os.Remove(opt.LastOpDetailFile))
613			return
614		}
615
616		ops := make([]operation, 0, 1000)
617		opCH := make(chan operation, 100)
618		done := make(chan int)
619
620		var hasErr bool
621		var n, nErr int
622		var outPath string
623		var err error
624
625		go func() {
626			first := true
627			for op := range opCH {
628				if opt.ListPath {
629					if opt.ListAbsPath {
630						outPath, err = filepath.Abs(op.source)
631						checkError(err)
632					} else {
633						outPath = op.source
634					}
635					if first {
636						fmt.Print(outPath)
637						first = false
638					} else {
639						fmt.Print(opt.ListPathSep + outPath)
640					}
641					continue
642				}
643				if int(op.code) >= opt.Verbose {
644					switch op.code {
645					case codeOK:
646						if !opt.Quiet {
647							log.Infof("checking: %s\n", op)
648						}
649					case codeUnchanged:
650						if !opt.Quiet {
651							log.Warningf("checking: %s\n", op)
652						}
653					case codeExisted, codeOverwriteNewPath:
654						switch opt.OverwriteMode {
655						case 0: // report error
656							log.Errorf("checking: %s\n", op)
657						case 1: // overwrite
658							if !opt.Quiet {
659								log.Warningf("checking: %s (will be overwrited)\n", op)
660							}
661						case 2: // no renaming
662							if !opt.Quiet {
663								log.Warningf("checking: %s (will NOT be overwrited)\n", op)
664							}
665						}
666					case codeMissingTarget:
667						log.Errorf("checking: %s\n", op)
668					}
669				}
670
671				switch op.code {
672				case codeOK:
673					ops = append(ops, op)
674					n++
675				case codeUnchanged:
676				case codeExisted, codeOverwriteNewPath:
677					switch opt.OverwriteMode {
678					case 0: // report error
679						hasErr = true
680						nErr++
681						continue
682					case 1: // overwrite
683						ops = append(ops, op)
684						n++
685					case 2: // no renaming
686
687					}
688				default:
689					hasErr = true
690					nErr++
691					continue
692				}
693			}
694			if opt.ListPath {
695				fmt.Println()
696			}
697			done <- 1
698		}()
699
700		paths := getFileList(args)
701
702		if !opt.Quiet {
703			log.Infof("  search paths: %s", strings.Join(paths, ", "))
704			log.Info()
705		}
706
707		for _, path := range paths {
708			err = walk(opt, opCH, path, 1)
709			if err != nil {
710				close(opCH)
711				checkError(err)
712			}
713		}
714		close(opCH)
715		<-done
716
717		if hasErr {
718			log.Errorf("%d potential error(s) detected, please check", nErr)
719			os.Exit(1)
720		}
721
722		if opt.ListPath {
723			return
724		}
725		if !opt.Quiet {
726			log.Infof("%d path(s) to be renamed", n)
727		}
728		if n == 0 {
729			return
730		}
731
732		if opt.DryRun {
733			return
734		}
735
736		var fh *os.File
737		fh, err = os.Create(opt.LastOpDetailFile)
738		checkError(err)
739		bfh := bufio.NewWriter(fh)
740		defer func() {
741			checkError(bfh.Flush())
742			fh.Close()
743		}()
744
745		var n2 int
746		var targetDir string
747		var targetDirExisted bool
748		for _, op := range ops {
749			targetDir = filepath.Dir(op.target)
750			targetDirExisted, err = pathutil.DirExists(targetDir)
751			if err != nil {
752				log.Errorf(`fail to rename: '%s' -> '%s'`, op.source, op.target)
753				os.Exit(1)
754			}
755			if !targetDirExisted {
756				os.MkdirAll(targetDir, 0755)
757			}
758
759			err = os.Rename(op.source, op.target)
760			if err != nil {
761				log.Errorf(`fail to rename: '%s' -> '%s': %s`, op.source, op.target, err)
762				os.Exit(1)
763			}
764			if !opt.Quiet {
765				log.Infof("renamed: '%s' -> '%s'", op.source, op.target)
766			}
767			bfh.WriteString(fmt.Sprintf("%s%s%s\n", op.source, delimiter, op.target))
768			n2++
769		}
770
771		if !opt.Quiet {
772			log.Infof("%d path(s) renamed", n2)
773		}
774	},
775}
776
777type code int
778
779const (
780	codeOK code = iota
781	codeUnchanged
782	codeExisted
783	codeOverwriteNewPath
784	codeMissingTarget
785)
786
787var yellow = color.New(color.FgYellow).SprintFunc()
788var red = color.New(color.FgRed).SprintFunc()
789var green = color.New(color.FgGreen).SprintFunc()
790
791func (c code) String() string {
792	switch c {
793	case codeOK:
794		return green("ok")
795	case codeUnchanged:
796		return yellow("unchanged")
797	case codeExisted:
798		return red("new path existed")
799	case codeOverwriteNewPath:
800		return red("overwriting newly renamed path")
801	case codeMissingTarget:
802		return red("missing target")
803	}
804
805	return "undefined code"
806}
807
808type operation struct {
809	source string
810	target string
811	code   code
812}
813
814func (op operation) String() string {
815	return fmt.Sprintf(`[ %s ] '%s' -> '%s'`, op.code, op.source, op.target)
816}
817
818func checkOperation(opt *Options, path string) (bool, operation) {
819	dir, filename := filepath.Split(path)
820	var ext string
821	if opt.IgnoreExt {
822		ext = filepath.Ext(path)
823		filename = filename[0 : len(filename)-len(ext)]
824	}
825
826	if !opt.PatternRe.MatchString(filename) {
827		return false, operation{}
828	}
829
830	r := opt.Replacement
831
832	if opt.ReplaceWithNR {
833		r = reNR.ReplaceAllString(r, fmt.Sprintf(opt.NRFormat, opt.StartNum))
834		opt.StartNum++
835	}
836
837	if opt.ReplaceWithKV {
838		founds := opt.PatternRe.FindAllStringSubmatch(filename, -1)
839		if len(founds) > 0 {
840			found := founds[0]
841			if opt.KeyCaptIdx > len(found)-1 {
842				checkError(fmt.Errorf("value of flag -I/--key-capt-idx overflows"))
843			}
844			k := found[opt.KeyCaptIdx]
845			if opt.IgnoreCase {
846				k = strings.ToLower(k)
847			}
848			if _, ok := opt.KVs[k]; ok {
849				r = reKV.ReplaceAllString(r, opt.KVs[k])
850			} else if opt.KeepKey {
851				r = reKV.ReplaceAllString(r, found[opt.KeyCaptIdx])
852			} else if opt.KeyMissRepl != "" {
853				r = reKV.ReplaceAllString(r, opt.KeyMissRepl)
854			} else {
855				return false, operation{path, path, codeUnchanged}
856			}
857		}
858	}
859
860	filename2 := opt.PatternRe.ReplaceAllString(filename, r) + ext
861	target := filepath.Join(dir, filename2)
862
863	if filename2 == "" {
864		return true, operation{path, target, codeMissingTarget}
865	}
866
867	if filename2 == filename+ext {
868		return true, operation{path, target, codeUnchanged}
869	}
870
871	if _, err := os.Stat(target); err == nil {
872		return true, operation{path, target, codeExisted}
873	}
874
875	if _, ok := pathTree[target]; ok {
876		return true, operation{path, target, codeOverwriteNewPath}
877	}
878	pathTree[target] = struct{}{}
879
880	return true, operation{path, target, codeOK}
881}
882
883func ignore(opt *Options, path string) bool {
884	for _, re := range opt.ExcludeFilterRes {
885		if re.MatchString(path) {
886			return true
887		}
888	}
889	for _, re := range opt.IncludeFilterRes {
890		if re.MatchString(path) {
891			return false
892		}
893	}
894	return true
895}
896
897func walk(opt *Options, opCh chan<- operation, path string, depth int) error {
898	if opt.MaxDepth > 0 && depth > opt.MaxDepth {
899		return nil
900	}
901	_, err := ioutil.ReadFile(path)
902	// it's a file
903	if err == nil {
904		if ignore(opt, filepath.Base(path)) {
905			return nil
906		}
907		if ok, op := checkOperation(opt, path); ok {
908			opCh <- op
909		}
910		return nil
911	}
912
913	// it's a directory
914	files, err := ioutil.ReadDir(path)
915	if err != nil {
916		return fmt.Errorf("err on reading dir: %s", path)
917	}
918
919	var filename string
920	_files := make([]string, 0, len(files))
921	_dirs := make([]string, 0, len(files))
922	for _, file := range files {
923		filename = file.Name()
924
925		if filename[0] == '.' {
926			continue
927		}
928
929		if file.IsDir() {
930			_dirs = append(_dirs, filename)
931		} else {
932			_files = append(_files, filename)
933		}
934	}
935
936	if !opt.OnlyDir {
937		if opt.ListPath && opt.NatureSort {
938			natsort.Sort(_files)
939		}
940		for _, filename := range _files {
941			if ignore(opt, filename) {
942				continue
943			}
944			fileFullPath := filepath.Join(path, filename)
945			if ok, op := checkOperation(opt, fileFullPath); ok {
946				opCh <- op
947			}
948		}
949	}
950
951	// sub directory
952	if opt.ListPath && opt.NatureSort {
953		natsort.Sort(_dirs)
954	}
955	for _, filename := range _dirs {
956		if (opt.OnlyDir || opt.IncludingDir) && ignore(opt, filename) {
957			continue
958		}
959
960		fileFullPath := filepath.Join(path, filename)
961		if opt.Recursive {
962			err := walk(opt, opCh, fileFullPath, depth+1)
963			if err != nil {
964				return err
965			}
966		}
967		// rename directories
968		if (opt.OnlyDir || opt.IncludingDir) && !ignore(opt, filename) {
969			if ok, op := checkOperation(opt, fileFullPath); ok {
970				opCh <- op
971			}
972		}
973	}
974
975	if depth > 1 {
976		return nil
977	}
978
979	// rename the given root directory
980	if (opt.OnlyDir || opt.IncludingDir) && !ignore(opt, path) {
981		if ok, op := checkOperation(opt, path); ok {
982			opCh <- op
983		}
984	}
985
986	return nil
987}
988
989func readKVs(file string, ignoreCase bool) (map[string]string, error) {
990	type KV [2]string
991	fn := func(line string) (interface{}, bool, error) {
992		line = strings.TrimRight(line, "\r\n")
993		if len(line) == 0 {
994			return nil, false, nil
995		}
996		items := strings.Split(line, "\t")
997		if len(items) < 2 {
998			return nil, false, nil
999		}
1000		if ignoreCase {
1001			return KV([2]string{strings.ToLower(items[0]), items[1]}), true, nil
1002		}
1003		return KV([2]string{items[0], items[1]}), true, nil
1004	}
1005	kvs := make(map[string]string)
1006	reader, err := breader.NewBufferedReader(file, 2, 10, fn)
1007	if err != nil {
1008		return kvs, err
1009	}
1010	var items KV
1011	for chunk := range reader.Ch {
1012		if chunk.Err != nil {
1013			return kvs, err
1014		}
1015		for _, data := range chunk.Data {
1016			items = data.(KV)
1017			kvs[items[0]] = items[1]
1018		}
1019	}
1020	return kvs, nil
1021}
1022