Skip to content

Commit 975b273

Browse files
author
dmullis
committed
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.
1 parent d083a69 commit 975b273

File tree

3 files changed

+337
-0
lines changed

3 files changed

+337
-0
lines changed

‎cmd/tmpl-expand/main.go‎

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
// Copyright 2022 Donald Mullis. All rights reserved.
2+
3+
// See `tmpl-expand -help` for abstract.
4+
package main
5+
6+
import (
7+
"flag"
8+
"fmt"
9+
"io"
10+
"log"
11+
"os"
12+
"path"
13+
"regexp"
14+
"sort"
15+
"strings"
16+
"text/template"
17+
)
18+
19+
type (
20+
KvpArg struct {
21+
Key string
22+
Value string
23+
}
24+
TemplateContext struct {
25+
// https://golang.org/pkg/text/template/#hdr-Arguments
26+
tmpl *template.Template
27+
substitutionsMap map[string]string
28+
}
29+
)
30+
31+
// General args
32+
var (
33+
writeMarkdown = flag.Bool("markdown", false,
34+
`Write out usage doc in Github-flavored Markdown format`)
35+
36+
exitStatus int
37+
)
38+
39+
func init() {
40+
log.SetFlags(log.Lshortfile)
41+
}
42+
43+
func main() {
44+
flag.Usage = func() {
45+
UsageDump()
46+
os.Exit(1)
47+
}
48+
flag.Parse()
49+
if !flag.Parsed() {
50+
log.Fatalln("flag.Parsed() == false")
51+
}
52+
53+
if *writeMarkdown {
54+
UsageMarkdown()
55+
return
56+
}
57+
58+
kvpArgs, defFileNameArgs := scanForKVArgs(flag.Args())
59+
for _, filename := range defFileNameArgs {
60+
kvpArg := scanValueFile(filename)
61+
kvpArgs[kvpArg.Key] = kvpArg.Value
62+
63+
}
64+
templateText := getTemplate(os.Stdin)
65+
ExpandTemplate(kvpArgs, templateText)
66+
os.Exit(exitStatus)
67+
}
68+
69+
var usageAbstract = `
70+
Key=Value
71+
Sh-style name=value definition string pairs. The Key name must be
72+
valid as a Go map Key acceptable to Go's template
73+
package https://pkg.go.dev/text/template
74+
75+
ValueFilePath
76+
File named on the command line containing a possibly multi-line
77+
definition of a single 'Value', with its 'Key' derived from the base name of the file.
78+
All non-alphanumeric characters in the basename are mapped to "_", to ensure their acceptability as
79+
Go template keys.
80+
81+
TemplateFile
82+
A stream read from stdin format template containing references to
83+
the 'Key' side of the above pairs.
84+
85+
ExpansionFile
86+
Written to stdout, the expansion of the input template read from stdin.
87+
88+
---
89+
Example:
90+
91+
echo >/tmp/valueFile.txt '
92+
. +-------+
93+
. | a box |
94+
. +-------+'
95+
echo '
96+
. A sentence referencing Key 'boxShape' with Value '{{.boxShape}}', read
97+
. from the command line.
98+
.
99+
. An introductory clause followed by a multi-line block of text,
100+
. read from a file:
101+
. {{.valueFile}}' |
102+
tmpl-expand boxShape='RECTANGULAR' /tmp/valueFile.txt
103+
104+
Result:
105+
. A sentence referencing Key boxShape with Value RECTANGULAR, read
106+
. from the command line.
107+
.
108+
. An introductory clause followed by a multi-line block of text,
109+
. read from a file:
110+
.
111+
. +-------+
112+
. | a box |
113+
. +-------+
114+
`
115+
116+
func writeUsage(out io.Writer, premable string) {
117+
fmt.Fprintf(out, "%s%s", premable,
118+
`Usage:
119+
tmpl-expand [-markdown] [ Key=Value | ValueFilePath ] ... <TemplateFile >ExpansionFile
120+
`)
121+
flag.PrintDefaults()
122+
fmt.Fprintf(out, "%s\n", usageAbstract)
123+
}
124+
125+
func UsageDump() {
126+
writeUsage(os.Stderr, "")
127+
}
128+
129+
func scanForKVArgs(args []string) (
130+
kvpArgs map[string]string, filenameArgs []string) {
131+
kvpArgs = make(map[string]string)
132+
for _, arg := range args {
133+
kvp := strings.Split(arg, "=")
134+
if len(kvp) != 2 {
135+
filenameArgs = append(filenameArgs, kvp[0])
136+
continue
137+
}
138+
newKvpArg := newKVPair(kvp)
139+
140+
// Search earlier Keys for duplicates.
141+
// XX N^2 in number of Keys -- use a map instead?
142+
for k := range kvpArgs {
143+
if k == newKvpArg.Key {
144+
log.Printf("Duplicate key specified: '%v', '%v'", kvp, newKvpArg)
145+
exitStatus = 1
146+
}
147+
}
148+
kvpArgs[newKvpArg.Key] = newKvpArg.Value
149+
}
150+
return
151+
}
152+
153+
func newKVPair(newKvp []string) KvpArg {
154+
vetKVstring(newKvp)
155+
return KvpArg{
156+
Key: newKvp[0],
157+
Value: newKvp[1],
158+
}
159+
}
160+
161+
func vetKVstring(kv []string) {
162+
reportFatal := func(format string) {
163+
// X X Caller disappears from stack, apparently due to inlining, despite
164+
// disabling Go optimizer
165+
//caller := func(howHigh int) string {
166+
// pc, file, line, ok := runtime.Caller(howHigh)
167+
// _ = pc
168+
// if !ok {
169+
// return ""
170+
// }
171+
// baseFileName := file[strings.LastIndex(file, "/")+1:]
172+
// return baseFileName + ":" + strconv.Itoa(line)
173+
//}
174+
log.Printf(format, kv)
175+
log.Fatalln("FATAL")
176+
}
177+
if len(kv[0]) <= 0 {
178+
reportFatal("Key side of Key=Value pair empty: %#v\n")
179+
}
180+
if len(kv[1]) <= 0 {
181+
reportFatal("Value side of Key=Value pair empty: %#v\n")
182+
}
183+
}
184+
185+
var alnumOnlyRE = regexp.MustCompile(`[^a-zA-Z0-9]`)
186+
187+
func scanValueFile(keyPath string) KvpArg {
188+
valueFile, err := os.Open(keyPath)
189+
if err != nil {
190+
log.Fatalln(err)
191+
}
192+
bytes, err := io.ReadAll(valueFile)
193+
if err != nil {
194+
log.Fatalln(err)
195+
}
196+
197+
basename := path.Base(keyPath)
198+
return KvpArg{
199+
Key: alnumOnlyRE.ReplaceAllLiteralString(basename, "_"),
200+
Value: string(bytes),
201+
}
202+
}
203+
204+
//func getTemplate(infile *os.File) (int, string) {
205+
func getTemplate(infile *os.File) string {
206+
var err error
207+
var stat os.FileInfo
208+
stat, err = infile.Stat()
209+
if err != nil {
210+
log.Fatalln(err)
211+
}
212+
templateText := make([]byte, stat.Size())
213+
var nRead int
214+
templateText, err = io.ReadAll(infile)
215+
nRead = len(templateText)
216+
if nRead <= 0 {
217+
log.Fatalf("os.Read returned %d bytes", nRead)
218+
}
219+
if err = infile.Close(); err != nil {
220+
log.Fatalf("Could not close %v, err=%v", infile, err)
221+
}
222+
return string(templateText)
223+
}
224+
225+
func ExpandTemplate(kvpArgs map[string]string, templateText string) {
226+
227+
ctx := TemplateContext{
228+
substitutionsMap: kvpArgs,
229+
}
230+
231+
var err error
232+
ctx.tmpl, err = template.New("" /*baseFile*/).Option("missingkey=error").
233+
Parse(templateText)
234+
if err != nil {
235+
log.Printf("Failed to parse '%s'", templateText)
236+
log.Fatalln(err)
237+
}
238+
ctx.writeFile()
239+
}
240+
241+
func (ctx *TemplateContext) writeFile() {
242+
if err := ctx.tmpl.Execute(os.Stdout, ctx.substitutionsMap); err != nil {
243+
fmt.Fprintf(os.Stderr, "Template.Execute(outfile, map) returned err=\n %v\n",
244+
err)
245+
fmt.Fprintf(os.Stderr, "Contents of failing map:\n%s", ctx.formatMap())
246+
exitStatus = 1
247+
}
248+
if err := os.Stdout.Close(); err != nil {
249+
log.Fatal(err)
250+
}
251+
return
252+
}
253+
254+
// Sort the output, for deterministic comparisons of build failures.
255+
func (ctx *TemplateContext) formatMap() (out string) {
256+
alphaSortMap(ctx.substitutionsMap,
257+
func(s string) {
258+
v := ctx.substitutionsMap[s]
259+
const TRIM = 80
260+
if len(v) > TRIM {
261+
v = v[:TRIM] + "..."
262+
}
263+
out += fmt.Sprintf(" % 20s '%v'\n\n", s, v)
264+
})
265+
return
266+
}
267+
268+
func alphaSortMap(m map[string]string, next func(s string)) {
269+
var h sort.StringSlice
270+
for k, _ := range m {
271+
h = append(h, k)
272+
}
273+
h.Sort()
274+
for _, s := range h {
275+
next(s)
276+
}
277+
}

‎cmd/tmpl-expand/make.sh‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#! /bin/sh
2+
3+
set -e
4+
build_variant=build
5+
if [ "$1" ]
6+
then
7+
build_variant="$1"
8+
shift
9+
fi
10+
11+
go ${build_variant}
12+
go run . --markdown >README.md
13+
marked -gfm README.md >README.html
14+

‎cmd/tmpl-expand/markdown.go‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2022 Donald Mullis. All rights reserved.
2+
3+
package main
4+
5+
import (
6+
"bufio"
7+
"flag"
8+
"fmt"
9+
"regexp"
10+
"strings"
11+
)
12+
13+
func UsageMarkdown() {
14+
var bytes strings.Builder
15+
flag.CommandLine.SetOutput(&bytes)
16+
17+
writeUsage(&bytes, `<!-- Automatically generated Markdown, do not edit -->
18+
<style type="text/css">
19+
h3 {margin-block-end: -0.5em;}
20+
h4 {margin-block-end: -0.5em;}
21+
code {font-size: larger;}
22+
</style>
23+
`)
24+
indentedTextToMarkdown(bytes)
25+
}
26+
27+
var column1Regex = regexp.MustCompile(`^[A-Z]`)
28+
const column1AtxHeading = " ### "
29+
30+
var column3Regex = regexp.MustCompile(`^ [^ ]`)
31+
const column3AtxHeading = " #### "
32+
// https://github.github.com/gfm/#atx-headings
33+
34+
// writes to stdout
35+
func indentedTextToMarkdown(bytes strings.Builder) {
36+
scanner := bufio.NewScanner(strings.NewReader(bytes.String()))
37+
for scanner.Scan() {
38+
line := scanner.Text()
39+
if column1Regex.MatchString(line) {
40+
line = column1AtxHeading + line
41+
} else if column3Regex.MatchString(line) {
42+
line = column3AtxHeading + line
43+
}
44+
fmt.Println(line)
45+
}
46+
}

0 commit comments

Comments
 (0)