Skip to content

Commit

Permalink
Create command 'tmpl-expand', for document production
Browse files Browse the repository at this point in the history
Command-line utility:  Builds a Key=Value map and passes it
to 'template.Execute()' for read of template source from stdin.
  • Loading branch information
dmullis committed Jul 12, 2022
1 parent d083a69 commit 975b273
Show file tree
Hide file tree
Showing 3 changed files with 337 additions and 0 deletions.
277 changes: 277 additions & 0 deletions cmd/tmpl-expand/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
// Copyright 2022 Donald Mullis. All rights reserved.

// See `tmpl-expand -help` for abstract.
package main

import (
"flag"
"fmt"
"io"
"log"
"os"
"path"
"regexp"
"sort"
"strings"
"text/template"
)

type (
KvpArg struct {
Key string
Value string
}
TemplateContext struct {
// https://golang.org/pkg/text/template/#hdr-Arguments
tmpl *template.Template
substitutionsMap map[string]string
}
)

// General args
var (
writeMarkdown = flag.Bool("markdown", false,
`Write out usage doc in Github-flavored Markdown format`)

exitStatus int
)

func init() {
log.SetFlags(log.Lshortfile)
}

func main() {
flag.Usage = func() {
UsageDump()
os.Exit(1)
}
flag.Parse()
if !flag.Parsed() {
log.Fatalln("flag.Parsed() == false")
}

if *writeMarkdown {
UsageMarkdown()
return
}

kvpArgs, defFileNameArgs := scanForKVArgs(flag.Args())
for _, filename := range defFileNameArgs {
kvpArg := scanValueFile(filename)
kvpArgs[kvpArg.Key] = kvpArg.Value

}
templateText := getTemplate(os.Stdin)
ExpandTemplate(kvpArgs, templateText)
os.Exit(exitStatus)
}

var usageAbstract = `
Key=Value
Sh-style name=value definition string pairs. The Key name must be
valid as a Go map Key acceptable to Go's template
package https://pkg.go.dev/text/template
ValueFilePath
File named on the command line containing a possibly multi-line
definition of a single 'Value', with its 'Key' derived from the base name of the file.
All non-alphanumeric characters in the basename are mapped to "_", to ensure their acceptability as
Go template keys.
TemplateFile
A stream read from stdin format template containing references to
the 'Key' side of the above pairs.
ExpansionFile
Written to stdout, the expansion of the input template read from stdin.
---
Example:
echo >/tmp/valueFile.txt '
. +-------+
. | a box |
. +-------+'
echo '
. A sentence referencing Key 'boxShape' with Value '{{.boxShape}}', read
. from the command line.
.
. An introductory clause followed by a multi-line block of text,
. read from a file:
. {{.valueFile}}' |
tmpl-expand boxShape='RECTANGULAR' /tmp/valueFile.txt
Result:
. A sentence referencing Key boxShape with Value RECTANGULAR, read
. from the command line.
.
. An introductory clause followed by a multi-line block of text,
. read from a file:
.
. +-------+
. | a box |
. +-------+
`

func writeUsage(out io.Writer, premable string) {
fmt.Fprintf(out, "%s%s", premable,
`Usage:
tmpl-expand [-markdown] [ Key=Value | ValueFilePath ] ... <TemplateFile >ExpansionFile
`)
flag.PrintDefaults()
fmt.Fprintf(out, "%s\n", usageAbstract)
}

func UsageDump() {
writeUsage(os.Stderr, "")
}

func scanForKVArgs(args []string) (
kvpArgs map[string]string, filenameArgs []string) {
kvpArgs = make(map[string]string)
for _, arg := range args {
kvp := strings.Split(arg, "=")
if len(kvp) != 2 {
filenameArgs = append(filenameArgs, kvp[0])
continue
}
newKvpArg := newKVPair(kvp)

// Search earlier Keys for duplicates.
// XX N^2 in number of Keys -- use a map instead?
for k := range kvpArgs {
if k == newKvpArg.Key {
log.Printf("Duplicate key specified: '%v', '%v'", kvp, newKvpArg)
exitStatus = 1
}
}
kvpArgs[newKvpArg.Key] = newKvpArg.Value
}
return
}

func newKVPair(newKvp []string) KvpArg {
vetKVstring(newKvp)
return KvpArg{
Key: newKvp[0],
Value: newKvp[1],
}
}

func vetKVstring(kv []string) {
reportFatal := func(format string) {
// X X Caller disappears from stack, apparently due to inlining, despite
// disabling Go optimizer
//caller := func(howHigh int) string {
// pc, file, line, ok := runtime.Caller(howHigh)
// _ = pc
// if !ok {
// return ""
// }
// baseFileName := file[strings.LastIndex(file, "/")+1:]
// return baseFileName + ":" + strconv.Itoa(line)
//}
log.Printf(format, kv)
log.Fatalln("FATAL")
}
if len(kv[0]) <= 0 {
reportFatal("Key side of Key=Value pair empty: %#v\n")
}
if len(kv[1]) <= 0 {
reportFatal("Value side of Key=Value pair empty: %#v\n")
}
}

var alnumOnlyRE = regexp.MustCompile(`[^a-zA-Z0-9]`)

func scanValueFile(keyPath string) KvpArg {
valueFile, err := os.Open(keyPath)
if err != nil {
log.Fatalln(err)
}
bytes, err := io.ReadAll(valueFile)
if err != nil {
log.Fatalln(err)
}

basename := path.Base(keyPath)
return KvpArg{
Key: alnumOnlyRE.ReplaceAllLiteralString(basename, "_"),
Value: string(bytes),
}
}

//func getTemplate(infile *os.File) (int, string) {
func getTemplate(infile *os.File) string {
var err error
var stat os.FileInfo
stat, err = infile.Stat()
if err != nil {
log.Fatalln(err)
}
templateText := make([]byte, stat.Size())
var nRead int
templateText, err = io.ReadAll(infile)
nRead = len(templateText)
if nRead <= 0 {
log.Fatalf("os.Read returned %d bytes", nRead)
}
if err = infile.Close(); err != nil {
log.Fatalf("Could not close %v, err=%v", infile, err)
}
return string(templateText)
}

func ExpandTemplate(kvpArgs map[string]string, templateText string) {

ctx := TemplateContext{
substitutionsMap: kvpArgs,
}

var err error
ctx.tmpl, err = template.New("" /*baseFile*/).Option("missingkey=error").
Parse(templateText)
if err != nil {
log.Printf("Failed to parse '%s'", templateText)
log.Fatalln(err)
}
ctx.writeFile()
}

func (ctx *TemplateContext) writeFile() {
if err := ctx.tmpl.Execute(os.Stdout, ctx.substitutionsMap); err != nil {
fmt.Fprintf(os.Stderr, "Template.Execute(outfile, map) returned err=\n %v\n",
err)
fmt.Fprintf(os.Stderr, "Contents of failing map:\n%s", ctx.formatMap())
exitStatus = 1
}
if err := os.Stdout.Close(); err != nil {
log.Fatal(err)
}
return
}

// Sort the output, for deterministic comparisons of build failures.
func (ctx *TemplateContext) formatMap() (out string) {
alphaSortMap(ctx.substitutionsMap,
func(s string) {
v := ctx.substitutionsMap[s]
const TRIM = 80
if len(v) > TRIM {
v = v[:TRIM] + "..."
}
out += fmt.Sprintf(" % 20s '%v'\n\n", s, v)
})
return
}

func alphaSortMap(m map[string]string, next func(s string)) {
var h sort.StringSlice
for k, _ := range m {
h = append(h, k)
}
h.Sort()
for _, s := range h {
next(s)
}
}
14 changes: 14 additions & 0 deletions cmd/tmpl-expand/make.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#! /bin/sh

set -e
build_variant=build
if [ "$1" ]
then
build_variant="$1"
shift
fi

go ${build_variant}
go run . --markdown >README.md
marked -gfm README.md >README.html

46 changes: 46 additions & 0 deletions cmd/tmpl-expand/markdown.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2022 Donald Mullis. All rights reserved.

package main

import (
"bufio"
"flag"
"fmt"
"regexp"
"strings"
)

func UsageMarkdown() {
var bytes strings.Builder
flag.CommandLine.SetOutput(&bytes)

writeUsage(&bytes, `<!-- Automatically generated Markdown, do not edit -->
<style type="text/css">
h3 {margin-block-end: -0.5em;}
h4 {margin-block-end: -0.5em;}
code {font-size: larger;}
</style>
`)
indentedTextToMarkdown(bytes)
}

var column1Regex = regexp.MustCompile(`^[A-Z]`)
const column1AtxHeading = " ### "

var column3Regex = regexp.MustCompile(`^ [^ ]`)
const column3AtxHeading = " #### "
// https://github.github.com/gfm/#atx-headings

// writes to stdout
func indentedTextToMarkdown(bytes strings.Builder) {
scanner := bufio.NewScanner(strings.NewReader(bytes.String()))
for scanner.Scan() {
line := scanner.Text()
if column1Regex.MatchString(line) {
line = column1AtxHeading + line
} else if column3Regex.MatchString(line) {
line = column3AtxHeading + line
}
fmt.Println(line)
}
}

0 comments on commit 975b273

Please sign in to comment.