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