master xplshn/aruu / scripts / mkman / main.go
  1package main
  2
  3import (
  4	"bufio"
  5	"flag"
  6	"fmt"
  7	"os"
  8	"path/filepath"
  9	"strings"
 10	"time"
 11)
 12
 13// parse config.kv variables and values
 14type Config map[string]string
 15
 16func parseConfigKv(path string) (Config, error) {
 17	f, err := os.Open(path)
 18	if err != nil {
 19		return nil, err
 20	}
 21	defer f.Close()
 22
 23	cfg := make(Config)
 24	sc := bufio.NewScanner(f)
 25	for sc.Scan() {
 26		line := strings.TrimSpace(sc.Text())
 27		if line == "" || strings.HasPrefix(line, "#") {
 28			continue
 29		}
 30		eq := strings.IndexByte(line, '=')
 31		if eq < 0 {
 32			continue
 33		}
 34		k := strings.TrimSpace(line[:eq])
 35		v := strings.TrimSpace(line[eq+1:])
 36		if len(v) >= 2 && ((v[0] == '"' && v[len(v)-1] == '"') || (v[0] == '\'' && v[len(v)-1] == '\'')) {
 37			v = v[1 : len(v)-1]
 38		}
 39		cfg[k] = v
 40	}
 41	if err := sc.Err(); err != nil {
 42		return nil, err
 43	}
 44
 45	return cfg, nil
 46}
 47
 48func (cfg Config) isEnabled(key string) bool {
 49	return cfg[key] == "1"
 50}
 51
 52// preprocessor condition stack for nested feature blocks
 53type ifFrame struct {
 54	active bool
 55	seen   bool
 56}
 57
 58type ifStack struct {
 59	frames []ifFrame
 60}
 61
 62func (s *ifStack) push(active, seen bool) {
 63	s.frames = append(s.frames, ifFrame{active: active, seen: seen})
 64}
 65
 66func (s *ifStack) pop() {
 67	if len(s.frames) > 0 {
 68		s.frames = s.frames[:len(s.frames)-1]
 69	}
 70}
 71
 72func (s *ifStack) parentActive() bool {
 73	for i := 0; i < len(s.frames)-1; i++ {
 74		if !s.frames[i].active {
 75			return false
 76		}
 77	}
 78	return true
 79}
 80
 81func (s *ifStack) globallyActive() bool {
 82	for _, f := range s.frames {
 83		if !f.active {
 84			return false
 85		}
 86	}
 87	return true
 88}
 89
 90func (s *ifStack) top() *ifFrame {
 91	if len(s.frames) == 0 {
 92		return nil
 93	}
 94	return &s.frames[len(s.frames)-1]
 95}
 96
 97// parse and evaluate preprocessor feature conditions
 98func evalCondition(expr string, cfg Config) bool {
 99	expr = strings.TrimSpace(expr)
100	if strings.HasPrefix(expr, "defined(") && strings.HasSuffix(expr, ")") {
101		key := expr[8 : len(expr)-1]
102		_, ok := cfg[key]
103		return ok
104	}
105	if strings.HasPrefix(expr, "!defined(") && strings.HasSuffix(expr, ")") {
106		key := expr[9 : len(expr)-1]
107		_, ok := cfg[key]
108		return !ok
109	}
110	if idx := strings.Index(expr, "=="); idx >= 0 {
111		lhs := strings.TrimSpace(expr[:idx])
112		rhs := strings.TrimSpace(expr[idx+2:])
113		return cfg[lhs] == rhs
114	}
115	if idx := strings.Index(expr, "!="); idx >= 0 {
116		lhs := strings.TrimSpace(expr[:idx])
117		rhs := strings.TrimSpace(expr[idx+2:])
118		return cfg[lhs] != rhs
119	}
120	if strings.HasPrefix(expr, "!") {
121		key := strings.TrimSpace(expr[1:])
122		return cfg[key] != "1"
123	}
124	return cfg[expr] == "1"
125}
126
127// section inference from file path
128func inferSection(path string) int {
129	clean := filepath.ToSlash(path)
130	switch {
131	case strings.Contains(clean, "/linux/"),
132		strings.Contains(clean, "/net/"),
133		strings.Contains(clean, "/xsi/"):
134		return 8
135	case strings.Contains(clean, "/posix/"),
136		strings.Contains(clean, "/pseudo/"),
137		strings.Contains(clean, "/extra/"),
138		strings.Contains(clean, "/dev/"):
139		return 1
140	default:
141		return 1
142	}
143}
144
145// a Renderer turns a parsed Page into one output format on os.Stdout
146type Renderer interface {
147	Render() error
148}
149
150func newRenderer(format string, page *Page) (Renderer, error) {
151	switch strings.ToLower(format) {
152	case "", "mdoc":
153		return NewMdocRenderer(page, os.Stdout), nil
154	case "txt":
155		return NewTxtRenderer(page, os.Stdout), nil
156	default:
157		return nil, fmt.Errorf("unknown format %q (want mdoc or txt)", format)
158	}
159}
160
161// main entry point and flags definition
162func main() {
163	configPath := flag.String("config", "scripts/mk/config.kv", "path to config.kv")
164	sectionFlag := flag.Int("section", 0, "man section override (0 = infer from path)")
165	dateFlag := flag.String("date", "", "date string for TH line (default: current month/year)")
166	formatFlag := flag.String("fmt", "mdoc", "output format: mdoc or txt")
167	flag.Parse()
168
169	if flag.NArg() < 1 {
170		fmt.Fprintf(os.Stderr, "usage: mkman [-config scripts/mk/config.kv] [-section N] [-fmt mdoc|txt] file.c\n")
171		os.Exit(1)
172	}
173	cfile := flag.Arg(0)
174
175	cfg, err := parseConfigKv(*configPath)
176	if err != nil {
177		fmt.Fprintf(os.Stderr, "mkman: config: %v\n", err)
178		os.Exit(1)
179	}
180
181	section := *sectionFlag
182	if section == 0 {
183		section = inferSection(cfile)
184	}
185
186	date := *dateFlag
187	if date == "" {
188		date = time.Now().Format("January 2, 2006")
189	}
190
191	page, err := ParsePage(cfile, cfg, section, date)
192	if err != nil {
193		fmt.Fprintf(os.Stderr, "mkman: scan: %v\n", err)
194		os.Exit(1)
195	}
196
197	if page == nil {
198		fmt.Fprintf(os.Stderr, "mkman: %s: no ?man comments found\n", cfile)
199		os.Exit(0)
200	}
201
202	r, err := newRenderer(*formatFlag, page)
203	if err != nil {
204		fmt.Fprintf(os.Stderr, "mkman: %v\n", err)
205		os.Exit(1)
206	}
207	if err := r.Render(); err != nil {
208		fmt.Fprintf(os.Stderr, "mkman: render: %v\n", err)
209		os.Exit(1)
210	}
211}