diff --git a/README.md b/README.md index d4f379b..7e2dc7a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Goname - Command-line batch renaming tool + [![Go Report Card](https://goreportcard.com/badge/github.com/ayoisaiah/goname)](https://goreportcard.com/report/github.com/ayoisaiah/goname) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/7136493cf477467387381890cb25dc9e)](https://www.codacy.com/manual/ayoisaiah/goname?utm_source=github.com&utm_medium=referral&utm_content=ayoisaiah/goname&utm_campaign=Badge_Grade) [![HitCount](http://hits.dwyl.com/ayoisaiah/goname.svg)](http://hits.dwyl.com/ayoisaiah/goname) @@ -14,8 +15,9 @@ Goname is a cross-platform command-line tool for batch renaming files and direct - Supports piping files through other programs like `find` or `rg`. - Detects potential conflicts and errors and reports them to you. - Supports recursive renaming of both files and directories. - - Supports renaming using a template + - Supports renaming using a template. - Supports using an ascending integer for renaming (e.g 001, 002, 003, e.t.c.). + - Supports undoing the last successful operation. ## Installation @@ -29,7 +31,7 @@ Otherwise, you can download precompiled binaries for Linux, Windows, and macOS [ ## Usage -**Note**: running these commands will only print out the changes to be made. If you want to proceed, use the `-x` flag. +**Note**: running these commands will only print out the changes to be made. If you want to carry out the operation, use the `-x` or `--exec` flag. All the examples below assume the following directory structure: @@ -196,14 +198,24 @@ Use the -F flag to ignore conflicts and rename anyway $ goname --find "pic1-bad.jpg" --replace "" Error detected: Operation resulted in empty filename pic1-bad.jpg ➟ [Empty filename] ❌ +``` + + - If you change your mind regarding a renaming operation, you can undo your changes using the `--undo` or `-U` flag. This only works for the last successful operation. + +```bash +$ goname -U +pic2-bad.png ➟ pic2-good.png ✅ +pic1-bad.jpg ➟ pic1-good.jpg ✅ +morebad/pic4-bad.webp ➟ morebad/pic4-good.webp ✅ +morebad/pic3-bad.jpg ➟ morebad/pic3-good.jpg ✅ +morebad ➟ moregood ✅ ``` ## TODO - [ ] Write tests -- [ ] Add undo support -## Credit and sources +## Credits Goname relies heavily on other open source software listed below: @@ -212,7 +224,7 @@ Goname relies heavily on other open source software listed below: ## Contribute -Bug reports, or pull requests are much welcome! +Bug reports, feature requests, or pull requests are much welcome! ## Licence diff --git a/cmd/goname/main.go b/cmd/goname/main.go index 6019961..88a41df 100644 --- a/cmd/goname/main.go +++ b/cmd/goname/main.go @@ -42,6 +42,11 @@ func main() { Aliases: []string{"x"}, Usage: "By default, goname will do a 'dry run' so that you can inspect the results and confirm that it looks correct. Add this flag to proceed with renaming the files.", }, + &cli.BoolFlag{ + Name: "undo", + Aliases: []string{"U"}, + Usage: "Undo the LAST successful operation", + }, &cli.BoolFlag{ Name: "include-dir", Aliases: []string{"D"}, @@ -59,6 +64,13 @@ func main() { }, }, Action: func(c *cli.Context) error { + if c.Bool("undo") { + op := &Operation{} + op.ignoreConflicts = c.Bool("force") + op.exec = c.Bool("exec") + return op.Undo() + } + op, err := NewOperation(c) if err != nil { return err diff --git a/cmd/goname/operation.go b/cmd/goname/operation.go index 1ff27a1..d518342 100644 --- a/cmd/goname/operation.go +++ b/cmd/goname/operation.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "fmt" "io/ioutil" "os" @@ -19,6 +20,8 @@ var ( yellow = color.FgYellow.Render ) +const opsFile = ".goname-operation.txt" + // Change represents a single filename change type Change struct { source string @@ -39,6 +42,108 @@ type Operation struct { searchRegex *regexp.Regexp } +// WriteToFile writes the details of the last successful operation +// to a file so that it may be reversed if necessary +func (op *Operation) WriteToFile() error { + // Create or truncate file + file, err := os.Create(opsFile) + if err != nil { + return err + } + + defer file.Close() + + writer := bufio.NewWriter(file) + for _, v := range op.matches { + _, err = writer.WriteString(v.target + "|" + v.source + "\n") + if err != nil { + return err + } + } + + return writer.Flush() +} + +// Undo reverses the last successful renaming operation +func (op *Operation) Undo() error { + file, err := os.Open(opsFile) + if err != nil { + return err + } + + defer file.Close() + + fi, err := file.Stat() + if err != nil { + return err + } + + // If file is empty + if fi.Size() == 0 { + return fmt.Errorf("No operation to undo") + } + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + slice := strings.Split(scanner.Text(), "|") + if len(slice) != 2 { + return fmt.Errorf("Corrupted data. Cannot undo") + } + source, target := slice[0], slice[1] + ch := Change{} + ch.source = source + ch.target = target + + op.matches = append(op.matches, ch) + } + + for i, v := range op.matches { + isDir, err := isDirectory(v.source) + if err != nil { + // An error may mean that the path does not exist + // which indicates that the directory containing the file + // was also renamed. + if os.IsNotExist(err) { + dir := filepath.Dir(v.source) + + // Get the directory that is changing + var d Change + for _, m := range op.matches { + if m.target == dir { + d = m + break + } + } + + re, err := regexp.Compile(d.target) + if err != nil { + return err + } + + srcFile, srcDir := filepath.Base(v.source), filepath.Dir(v.source) + targetFile, targetDir := filepath.Base(v.target), filepath.Dir(v.target) + + // Update the directory of the path to the current name + // instead of the old one which no longer exists + srcDir = re.ReplaceAllString(srcDir, d.source) + targetDir = re.ReplaceAllString(targetDir, d.source) + + v.source = filepath.Join(srcDir, srcFile) + v.target = filepath.Join(targetDir, targetFile) + } else { + return err + } + } + + v.isDir = isDir + op.matches[i] = v + } + + op.SortMatches() + + return op.Apply() +} + // Apply will check for conflicts and print // the changes to be made or apply them directly // if in execute mode. Conflicts will be ignored if @@ -65,7 +170,9 @@ func (op *Operation) Apply() error { } } - if !op.exec && len(op.matches) > 0 { + if op.exec && len(op.matches) > 0 { + return op.WriteToFile() + } else if !op.exec && len(op.matches) > 0 { color.Style{color.FgYellow, color.OpBold}.Println("*** Use the -x flag to apply the above changes ***") }