-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create command 'tmpl-expand', for document production
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
Showing
3 changed files
with
337 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |