github.com/tooploox/oya@v0.0.21-0.20230524103240-1cda1861aad6/pkg/raw/patching.go (about) 1 package raw 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "regexp" 10 "strings" 11 ) 12 13 // flatMap maps each line of Oyafile to possibly multiple lines flattening the result. Does not write to the file. 14 func (raw *Oyafile) flatMap(f func(line string) []string) error { 15 scanner := bufio.NewScanner(bytes.NewReader(raw.oyafileContents)) 16 17 output := bytes.NewBuffer(nil) 18 for scanner.Scan() { 19 line := scanner.Text() 20 lines := f(line) 21 for _, l := range lines { 22 output.WriteString(l) 23 output.WriteString("\n") 24 } 25 } 26 27 raw.oyafileContents = output.Bytes() 28 return nil 29 } 30 31 // insertAfter inserts one or more lines after the first line matching the regular expression. 32 // Does not write to the file. Preserves indentation; the new lines will have the same indentation 33 // as the preceding line. 34 func (raw *Oyafile) insertAfter(rx *regexp.Regexp, lines ...string) (bool, error) { 35 found := false 36 err := raw.flatMap(func(line string) []string { 37 if !found && rx.MatchString(line) { 38 found = true 39 return append([]string{line}, indent(lines, detectIndent(line))...) 40 } else { 41 return []string{line} 42 } 43 }) 44 return found, err 45 } 46 47 // insertBefore inserts one or more lines before the first line matching the regular expression. 48 // Does not write to the file. Preserves indentation; the new lines will have the same indentation 49 // as the preceding line. 50 func (raw *Oyafile) insertBefore(rx *regexp.Regexp, lines ...string) (bool, error) { 51 found := false 52 err := raw.flatMap(func(line string) []string { 53 if !found && rx.MatchString(line) { 54 found = true 55 return append(indent(lines, detectIndent(line)), line) 56 } else { 57 return []string{line} 58 } 59 }) 60 return found, err 61 } 62 63 // replaceAllWhen replaces all lines matching the predicate with one or more lines. 64 // Does not write to the file. Preserves indentation; the new lines will have the same indentation 65 // as the line being replaced. 66 func (raw *Oyafile) replaceAllWhen(pred func(string) bool, lines ...string) (bool, error) { 67 found := false 68 err := raw.flatMap(func(line string) []string { 69 if pred(line) { 70 found = true 71 return indent(lines, detectIndent(line)) 72 } else { 73 return []string{line} 74 } 75 }) 76 return found, err 77 } 78 79 // insertBeforeWithin inserts one or more lines before the first line matching the regular expression 80 // but - unlike insertBefore - only for lines nested under the specified top-level YAML key. 81 // Does not write to the file. Preserves indentation; the new lines will have the same indentation 82 // as the preceding line. 83 func (raw *Oyafile) insertBeforeWithin(key string, rx *regexp.Regexp, lines ...string) (bool, error) { 84 keyRegexp := regexp.MustCompile(fmt.Sprintf("^%v:", key)) 85 topLevelKeyRegexp := regexp.MustCompile("^[\\s]+:") 86 withinKey := false 87 found := false 88 err := raw.flatMap(func(line string) []string { 89 if found { 90 return []string{line} 91 } 92 if withinKey { 93 if topLevelKeyRegexp.MatchString(line) { 94 withinKey = false 95 } 96 97 if rx.MatchString(line) { 98 found = true 99 return append(indent(lines, detectIndent(line)), line) 100 } else { 101 return []string{line} 102 } 103 } else { 104 if keyRegexp.MatchString(line) { 105 withinKey = true 106 } 107 } 108 return []string{line} 109 }) 110 return found, err 111 } 112 113 // concat appends one or more lines to the Oyafile. Does not write to the file. 114 func (raw *Oyafile) concat(lines ...string) error { 115 output := bytes.NewBuffer(raw.oyafileContents) 116 if err := writeLines(lines, output); err != nil { 117 return err 118 } 119 raw.oyafileContents = output.Bytes() 120 return nil 121 } 122 123 // prepend takes an Oyafile and prepends its content with one or more lines. 124 // Does not write to the file. 125 func (raw *Oyafile) prepend(lines ...string) error { 126 output := bytes.NewBuffer(nil) 127 if err := writeLines(lines, output); err != nil { 128 return err 129 } 130 if _, err := output.Write(raw.oyafileContents); err != nil { 131 return err 132 } 133 raw.oyafileContents = output.Bytes() 134 return nil 135 } 136 137 func writeLines(lines []string, output *bytes.Buffer) error { 138 for _, l := range lines { 139 if _, err := output.WriteString(l); err != nil { 140 return err 141 } 142 if _, err := output.WriteString("\n"); err != nil { 143 return err 144 } 145 } 146 return nil 147 } 148 149 // containsLineMatching returns true if Oyafile matches the regular expression. 150 // (?m) directive may be used to match file line by line 151 func (raw *Oyafile) matches(rx *regexp.Regexp) bool { 152 return rx.MatchString(string(raw.oyafileContents)) 153 } 154 155 // write flushes in-memory Oyafile contents to disk. 156 func (raw *Oyafile) write() error { 157 info, err := os.Stat(raw.Path) 158 if err != nil { 159 return err 160 } 161 return ioutil.WriteFile(raw.Path, raw.oyafileContents, info.Mode()) 162 } 163 164 func indent(lines []string, level int) []string { 165 indented := make([]string, len(lines)) 166 for i, line := range lines { 167 indented[i] = fmt.Sprintf("%v%v", strings.Repeat(" ", level), line) 168 } 169 return indented 170 } 171 172 func detectIndent(line string) int { 173 i := 0 174 for _, runeValue := range line { 175 if runeValue == ' ' { 176 i++ 177 } else { 178 break 179 } 180 } 181 return i 182 }