telegraf/tools/readme_config_includer/generator.go

171 lines
5.1 KiB
Go
Raw Normal View History

// This is a tool to embedd configuration files into the README.md of all plugins
// It searches for TOML sections in the plugins' README.md and detects includes specified in the form
// ```toml [@includeA.conf[ @includeB[ @...]]
// Whatever is in here gets replaced.
// ```
// Then it will replace everything in this section by the concatenation of the file `includeA.conf`, `includeB` etc.
// content. The tool is not stateful, so it can be run multiple time with a stable result as long
// as the included files do not change.
package main
import (
"bytes"
"errors"
"fmt"
"io"
"log" //nolint:revive
"os"
"regexp"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
)
type includeBlock struct {
Includes []string
Start int
Stop int
}
func (b *includeBlock) extractBlockBorders(node *ast.FencedCodeBlock) {
// The node info starts at the language tag and stops right behind it
b.Start = node.Info.Segment.Stop + 1
b.Stop = b.Start
// To determine the end of the block, we need to iterate to the last line
// and take its stop-offset as the end of the block.
lines := node.Lines()
for i := 0; i < lines.Len(); i++ {
b.Stop = lines.At(i).Stop
}
}
func insertInclude(buf *bytes.Buffer, include string) error {
file, err := os.Open(include)
if err != nil {
return fmt.Errorf("opening include %q failed: %v", include, err)
}
defer file.Close()
// Write the include and make sure we get a newline
if _, err := io.Copy(buf, file); err != nil {
return fmt.Errorf("inserting include %q failed: %v", include, err)
}
return nil
}
func insertIncludes(buf *bytes.Buffer, b includeBlock) error {
// Insert all includes in the order they occured
for _, include := range b.Includes {
if err := insertInclude(buf, include); err != nil {
return err
}
}
// Make sure we add a trailing newline
if !bytes.HasSuffix(buf.Bytes(), []byte("\n")) {
if _, err := buf.Write([]byte("\n")); err != nil {
return errors.New("adding newline failed")
}
}
return nil
}
func main() {
// Finds all TOML sections of the form `toml @includefile` and extracts the `includefile` part
tomlIncludesEx := regexp.MustCompile(`^toml\s+(@.+)+$`)
tomlIncludeMatch := regexp.MustCompile(`(?:@([^\s]+))+`)
// Get the file permission of the README for later use
inputFilename := "README.md"
inputFileInfo, err := os.Lstat(inputFilename)
if err != nil {
log.Fatalf("Cannot get file permissions: %v", err)
}
perm := inputFileInfo.Mode().Perm()
// Read and parse the README markdown file
readme, err := os.ReadFile(inputFilename)
if err != nil {
log.Fatalf("Reading README failed: %v", err)
}
parser := goldmark.DefaultParser()
root := parser.Parse(text.NewReader(readme))
// Walk the markdown to identify the (TOML) parts to replace
blocksToReplace := make([]includeBlock, 0)
for node := root.FirstChild(); node != nil; node = node.NextSibling() {
// Only match TOML code nodes
codeNode, ok := node.(*ast.FencedCodeBlock)
if !ok || string(codeNode.Language(readme)) != "toml" {
// Ignore any other node type or language
continue
}
// Extract the includes from the node
includes := tomlIncludesEx.FindSubmatch(codeNode.Info.Text(readme))
if len(includes) != 2 {
continue
}
block := includeBlock{}
for _, inc := range tomlIncludeMatch.FindAllSubmatch(includes[1], -1) {
if len(inc) != 2 {
continue
}
include := string(inc[1])
// Safeguards to avoid directory traversals and other bad things
if strings.ContainsRune(include, os.PathSeparator) {
log.Printf("Ignoring include %q for containing a path...", include)
continue
}
if fi, err := os.Stat(include); err != nil || !fi.Mode().IsRegular() {
log.Printf("Ignoring include %q as it cannot be found or is not a regular file...", include)
continue
}
block.Includes = append(block.Includes, string(inc[1]))
}
// Extract the block boarders
block.extractBlockBorders(codeNode)
blocksToReplace = append(blocksToReplace, block)
}
// Replace the content of the TOML blocks with includes
var output bytes.Buffer
output.Grow(len(readme))
offset := 0
for _, b := range blocksToReplace {
// Copy everything up to the beginning of the block we want to replace and make sure we get a newline
if _, err := output.Write(readme[offset:b.Start]); err != nil {
log.Fatalf("Writing non-replaced content failed: %v", err)
}
if !bytes.HasSuffix(output.Bytes(), []byte("\n")) {
if _, err := output.Write([]byte("\n")); err != nil {
log.Fatalf("Writing failed: %v", err)
}
}
offset = b.Stop
// Insert the include file
if err := insertIncludes(&output, b); err != nil {
log.Fatal(err)
}
}
// Copy the remainings of the original file...
if _, err := output.Write(readme[offset:]); err != nil {
log.Fatalf("Writing remaining content failed: %v", err)
}
// Write output with same permission as input
file, err := os.OpenFile(inputFilename, os.O_CREATE|os.O_WRONLY, perm)
if err != nil {
log.Fatalf("Opening output file failed: %v", err)
}
defer file.Close()
if _, err := output.WriteTo(file); err != nil {
log.Fatalf("Writing output file failed: %v", err)
}
}