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}