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