Update file content with golang and regexp

In this article, I will describe how I have done to replace hundreds of files together with program.

Recently I changed the i18n solution on one of our projects, and I need to change hundreds of files, I tried to update the file by hand, but it was too time consuming. So I think about writing a program to update the files automatically.

The project below is what I have written to update the files, I added some demo files to demonstrate what the program do. The demo file was in the demodata folder, in this folder, I put some demo files which includes java, kotlin , objc and swift files, the demo demonstrate how to update the files from StringProvider to StringVaues.

GitHub - swanwish/fileupdater: A tool written with golang, which can update files with regexp and…
You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or…

Problem

In this demo, take the java file for example, the original content is like below:String value1 = StringProvider.getValue("key1", "default value 1");
String value2 = StringProvider.getValue("key2", "default value 2");
String value3 = isAdd ? StringProvider.getValue("key3", "default value 3") : StringProvider.getValue("key4","default value 4");
String value4 = StringProvider.getValue("key5",
               "default value 5");

The code above call StringProvider’s method to get value according to a key, and I want to change it to another class, which has variable named as key name, such as StringValues.key1 . The source code has multiple formats, some of them has no space after comma, some of them has multiple lines. I need to search all of them and replace them with the new method.

Solution

The project I created, which can register update rules, and the application will use the rule to search the text and replace the text according to the rule.

Update Rule

The update rule can config the file exts, regexp patterns and match replacer.

The struct for the update rule is like below:type UpdateRule struct {
Exts             []string
Pattern          string
GetMatchReplacer func(match []string) *string
}

The application will search the files with extension configured in the rule, and search the text with the patten in the rule, the pattern is the regexp pattern, the GetMatchReplacer has the match parameter, which is get from the regexp, and return the replace string to the application, if the return value is nil, the text won’t replaced, when the return value is not nil, it will be used as the destination.

List files in a directory

List file will search the files in the search dir, it will check the extension of the file, add return the matched files, this function is a recursive function, if the item in the folder is dir, then will call the function again with the sub dir. The following code is the implementation of the function.func (updater fileUpdater) listFiles(searchDir string) []string {
items, err := ioutil.ReadDir(searchDir)
if err != nil {
 logs.Errorf("Failed to get items from dir %s, the error is %#v", searchDir, err)
 return nil
}files := make([]string, 0)
for _, item := range items {
 itemName := item.Name()
 subDir := filepath.Join(searchDir, itemName)
 if item.IsDir() {
  subFiles := updater.listFiles(subDir)
  if err != nil {
   logs.Errorf("Failed to get sub file, the error is %#v", err)
   return nil
  }
  files = append(files, subFiles...)
 } else {
  fileExt := path.Ext(itemName)
  for _, ext := range updater.fileExts {
   if fileExt == ext {
    files = append(files, filepath.Join(searchDir, itemName))
   }
  }
 }
}
return files
}

Filter string with regexp

I use the regexp to filter the strings need to replace, I used the function func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string to find the strings, the following is the description to the function.

// FindAllStringSubmatch is the ‘All’ version of FindStringSubmatch; it
// returns a slice of all successive matches of the expression, as defined by
// the ‘All’ description in the package comment.
// A return value of nil indicates no match.

There are something that I learned from the project:

The example patten for java file is:"StringProvider\\.getValue\\(\"(.*?)\",\\s*?\"(.*?)\"\\)"

With this rule, the text below will return three items.String value1 = StringProvider.getValue("key1", "default value 1");

The result of the FindAllStringSubmatch has three items, the first item is StringProvider.getValue("key1", "default value 1") , the second item is key1 and the last item is default value 1 .

In the pattern, I use \\s*? to match the whitespace between the terms, and the placeholder (.*?) I added a ? at the end, this will not use the greedy mode, so If I have two match items, it will return two item.