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  }