Bulk renaming in a single pass with Go
NameChanger

© Lead Image © Denis Tevekov, 123RF.com
Renaming multiple files following a pattern often requires small shell scripts. Mike Schilli looks to simplify this task with a Go program.
One popular interview question for system administrators is what is the easiest way to give a set of files a new extension. Take a directory of *.log
files, for example: How do you rename them all in one go to *.log.old
? It has reportedly happened that candidates suggested the shell command mv *.log *.log.old
for this – however, they were then not hired.
There are already quite a few tools lurking around on GitHub that handle such tasks, such as the Renamer tool written in Rust [1]. But such simple utilities make for great illustrative examples, so I wanted to explore some Go techniques for bulk renaming. Paying tribute to the original, the Go variant presented below will also go by the name of Renamer. For example, to rename an entire set of logfiles ending in .log
to .log.bak
, just use the call shown in line 1 of Listing 1.
Listing 1
Renaming Files
01 $ renamer -v '.log$/.log.bak' *.log 02 out.log -> out.log.bak 03 [...] 04 $ renamer -v '/hawaii2020-{seq}.jpg' *.JPG 05 IMG_8858.JPG -> hawaii2020-0001.jpg 06 IMG_8859.JPG -> hawaii2020-0002.jpg
Or how about renaming vacation photos currently named IMG_8858.JPG
through IMG_9091.JPG
to hawaii-2020-0001.jpg
through hawaii-2020-0234.jpg
? My Go program does that too with the call from line 4, replacing the placeholder {seq}
with a counter incremented by one for each renamed file, which it pads with leading zeros to four digits.
Mass Production
The renamer
main program (Listing 2) processes its command-line options -d
for a test run without consequences (dryrun
) and -v
for chatty (verbose
) status messages in lines 19 and 20. The standard flag package used for this purpose not only assigns the dryrun
and verbose
pointer variables the values true
or false
, respectively, but it also jumps to a Usage()
function defined in the Usage
attribute if the user tries to slip in an option that the program doesn't know.
Listing 2
renamer.go
01 package main 02 03 import ( 04 "flag" 05 "fmt" 06 "os" 07 "path" 08 ) 09 10 func usage() { 11 fmt.Fprintf(os.Stderr, 12 "Usage: %s 'search/replace' file ...\n", 13 path.Base(os.Args[0])) 14 flag.PrintDefaults() 15 os.Exit(1) 16 } 17 18 func main() { 19 dryrun := flag.Bool("d", false, "dryrun only") 20 verbose := flag.Bool("v", false, "verbose mode") 21 flag.Usage = usage 22 flag.Parse() 23 24 if *dryrun { 25 fmt.Printf("Dryrun mode\n") 26 } 27 28 if len(flag.Args()) < 2 { 29 usage() 30 } 31 32 cmd := flag.Args()[0] 33 files := flag.Args()[1:] 34 modifier, err := mkmodifier(cmd) 35 if err != nil { 36 fmt.Fprintf(os.Stderr, 37 "Invalid command: %s\n", cmd) 38 usage() 39 } 40 41 for _, file := range files { 42 modfile := modifier(file) 43 if file == modfile { 44 continue 45 } 46 if *verbose || *dryrun { 47 fmt.Printf("%s -> %s\n", file, modfile) 48 } 49 if *dryrun { 50 continue 51 } 52 err := os.Rename(file, modfile) 53 if err != nil { 54 fmt.Printf("Renaming %s -> %s failed: %v\n", 55 file, modfile, err) 56 break 57 } 58 } 59 }
In any case, the program expects a command to manipulate the file names and one or more files to rename later. Line 12 informs the user of the correct call syntax of the renamer
binary compiled from the source code.
The array slice arithmetic assigns the first command-line parameter with index number
to the cmd
variable. This is followed by one or more file names, which the shell is also welcome to expand using wildcards before passing them to the program. The arguments from the second to last position are fetched from the array slice by the expression [1:]
; line 33 assigns the list of files to the variable files
.
The instruction passed in at the command line to manipulate the file names (e.g., '.log$/.log.old'
) gets sent to the mkmodifier()
function defined further down in Listing 3. This turns the instruction into a Go function that manipulates input file names according to the user's instructions and returns a modified name.
Listing 3
mkmodifier.go
01 package main 02 03 import ( 04 "errors" 05 "fmt" 06 "regexp" 07 "strings" 08 ) 09 10 func mkmodifier(cmd string) (func(string) string, error) { 11 parts := strings.Split(cmd, "/") 12 if len(parts) != 2 { 13 return nil, errors.New("Invalid repl command") 14 } 15 search := parts[0] 16 repltmpl := parts[1] 17 seq := 1 18 19 var rex *regexp.Regexp 20 21 if len(search) == 0 { 22 search = ".*" 23 } 24 25 rex = regexp.MustCompile(search) 26 27 modifier := func(org string) string { 28 repl := strings.Replace(repltmpl, 29 "{seq}", fmt.Sprintf("%04d", seq), -1) 30 seq++ 31 res := rex.ReplaceAllString(org, repl) 32 return string(res) 33 } 34 35 return modifier, nil 36 }
Function Returns Function
You've read that correctly: The mkmodifier()
function actually returns a function in line 34 of Listing 2, which is assigned to the modifier
variable there. A few lines down, in the for
loop that iterates over all the files to be manipulated, the main program simply calls this function by referencing modifier
. With every call, the main program passes the returned file name the original name of the file and, in line 42, picks up the new name and stores it in modfile
.
If the user has chosen dryrun
mode (-d
), line 47 simply prints the intended rename action, and line 50 rings in the next round of the for
loop with continue
, skipping the call of rename()
in line 52.
In production mode, however, line 52 calls the Unix system rename()
function from the standard os package and renames the file to the new name from modfile
. If access rights prevent this, the function fails and os.Rename()
returns an error, which line 53 fields. The associated if
block prints a message and breaks the for
loop with break
, because in that case the end of the world is nigh.
Regular Expressions
Instead of requesting a plain vanilla string replacement, the user can also specify regular expressions to remodel file names. For example, the .log$
search expression illustrated earlier specifies that the .log
suffix must actually be at the end of the name – it would ignore foo.log.bak
. To enable this, Listing 3 draws on the standard regexp package and compiles the regular expression from the user input to create a rex
variable of the *regexp.Regexp
type using MustCompile()
in line 25. After that, the modifier defined in line 27 can call the ReplaceAllString()
function. It replaces all matches that match the expression in the original name org
with the replacement string stored in repl
.
Attentive readers may wonder about the mkmodifier()
function in Listing 3: It returns a function to the main program, to be called multiple times, but this function actually seems to maintain state between calls. For example, take a look at the function's local variable seq
: Each new call to the function injects a value incremented by one into the modified file name. How is this possible?
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Direct Download
Read full article as PDF:
Price $2.95
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Find SysAdmin Jobs
News
-
CarbonOS: A New Linux Distro with a Focus on User Experience
CarbonOS is a brand new, built-from-scratch Linux distribution that uses the Gnome desktop and has a special feature that makes it appealing to all types of users.
-
Kubuntu Focus Announces XE Gen 2 Linux Laptop
Another Kubuntu-based laptop has arrived to be your next ultra-portable powerhouse with a Linux heart.
-
MNT Seeks Financial Backing for New Seven-Inch Linux Laptop
MNT Pocket Reform is a tiny laptop that is modular, upgradable, recyclable, reusable, and ships with Debian Linux.
-
Ubuntu Flatpak Remix Adds Flatpak Support Preinstalled
If you're looking for a version of Ubuntu that includes Flatpak support out of the box, there's one clear option.
-
Gnome 44 Release Candidate Now Available
The Gnome 44 release candidate has officially arrived and adds a few changes into the mix.
-
Flathub Vying to Become the Standard Linux App Store
If the Flathub team has any say in the matter, their product will become the default tool for installing Linux apps in 2023.
-
Debian 12 to Ship with KDE Plasma 5.27
The Debian development team has shifted to the latest version of KDE for their testing branch.
-
Planet Computers Launches ARM-based Linux Desktop PCs
The firm that originally released a line of mobile keyboards has taken a different direction and has developed a new line of out-of-the-box mini Linux desktop computers.
-
Ubuntu No Longer Shipping with Flatpak
In a move that probably won’t come as a shock to many, Ubuntu and all of its official spins will no longer ship with Flatpak installed.
-
openSUSE Leap 15.5 Beta Now Available
The final version of the Leap 15 series of openSUSE is available for beta testing and offers only new software versions.