github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/utils/man/descriptions_posix.go (about) 1 //go:build !windows 2 // +build !windows 3 4 package man 5 6 import ( 7 "bufio" 8 "bytes" 9 "fmt" 10 "io" 11 "regexp" 12 "strings" 13 14 "github.com/lmorg/murex/debug" 15 "github.com/lmorg/murex/lang" 16 "github.com/lmorg/murex/lang/stdio" 17 "github.com/lmorg/murex/lang/types" 18 "github.com/lmorg/murex/utils" 19 "github.com/lmorg/murex/utils/lists" 20 "github.com/lmorg/murex/utils/rmbs" 21 ) 22 23 func GetManPage(command string, width int) stdio.Io { 24 fork := lang.ShellProcess.Fork(lang.F_FUNCTION | lang.F_NO_STDIN | lang.F_CREATE_STDOUT | lang.F_NO_STDERR) 25 fork.Name.Set("(man)") 26 err := fork.Variables.Set(fork.Process, "command", command, types.String) 27 if err != nil { 28 if debug.Enabled { 29 panic(err) 30 } 31 return nil 32 } 33 34 _, err = fork.Execute(ManPageExecBlock(width)) 35 if err != nil { 36 if debug.Enabled { 37 panic(err) 38 } 39 return nil 40 } 41 42 return fork.Stdout 43 } 44 45 func parseDescriptions(command string, fMap *map[string]string) { 46 stdout := GetManPage(command, 1000) 47 parseDescriptionsLines(stdout, fMap) 48 } 49 50 var rxHeading = regexp.MustCompile(`^[A-Z]+$`) 51 52 var validSections = []string{ 53 "DESCRIPTION", 54 "OPTIONS", 55 "PRIMARIES", // required for `find` on macOS 56 "EXPRESSION", // required for `find` on GNU 57 } 58 59 func parseDescriptionsLines(r io.Reader, fMap *map[string]string) { 60 var pl *parsedLineT 61 var section string 62 63 scanner := bufio.NewScanner(r) 64 65 for scanner.Scan() { 66 b := scanner.Bytes() 67 //b := append(scanner.Bytes(), utils.NewLineByte...) 68 69 b = []byte(rmbs.Remove(string(b))) 70 b = utils.CrLfTrim(b) 71 72 heading := rxHeading.Find(b) 73 if len(heading) > 0 { 74 section = string(heading) 75 } 76 77 if !lists.Match(validSections, section) { 78 continue 79 } 80 81 ws := countWhiteSpace(b) 82 switch { 83 case ws == 0: 84 fallthrough 85 86 case ws == len(b)-1: 87 updateFlagMap(pl, fMap) 88 pl = nil 89 90 case b[ws] == '-': 91 updateFlagMap(pl, fMap) 92 pl = parseLineFlags(b[ws:]) 93 94 case pl == nil: 95 continue 96 97 case pl.Description != "" && len(pl.Description) < 30 && ws >= 8: // kludge for `find` style flags 98 pl.Example += " " + pl.Description 99 pl.Description = "" 100 fallthrough 101 102 case pl.Description == "": 103 pl.Position = ws 104 fallthrough 105 106 case ws == pl.Position: 107 pl.Description += " " + string(b[ws:]) 108 109 default: 110 updateFlagMap(pl, fMap) 111 pl = nil 112 } 113 } 114 if err := scanner.Err(); err != nil && debug.Enabled { 115 panic(err) 116 } 117 } 118 119 func updateFlagMap(pl *parsedLineT, fMap *map[string]string) { 120 if pl == nil { 121 return 122 } 123 124 pl.Description = strings.TrimSpace(pl.Description) 125 pl.Description = strings.ReplaceAll(pl.Description, " ", " ") 126 127 for i := range pl.Flags { 128 if pl.Example == "" { 129 (*fMap)[pl.Flags[i]] = pl.Description 130 } else { 131 (*fMap)[pl.Flags[i]] = fmt.Sprintf( 132 "eg: %s -- %s", 133 strings.TrimSpace(pl.Example), pl.Description) 134 } 135 } 136 } 137 138 func countWhiteSpace(b []byte) int { 139 for i := range b { 140 if b[i] == ' ' || b[i] == '\t' { 141 continue 142 } 143 return i 144 } 145 return 0 146 } 147 148 var ( 149 rxLineMatchFlag = regexp.MustCompile(`^-[-_a-zA-Z0-9]+`) 150 rxExampleCaps = regexp.MustCompile(`^[A-Z]+([\t, ]|$)`) 151 ) 152 153 type parsedLineT struct { 154 Position int 155 Description string 156 Example string 157 Flags []string 158 } 159 160 func parseLineFlags(b []byte) *parsedLineT { 161 //defer recover() 162 163 pl := new(parsedLineT) 164 165 for { 166 start: 167 //fmt.Println(json.LazyLoggingPretty(*pl), "-->"+string(b)+"<--") 168 if pl.Position == len(b) { 169 return pl 170 } 171 172 switch b[pl.Position] { 173 case ',': 174 pl.Position += countWhiteSpace(b[pl.Position+1:]) + 1 175 //fallthrough 176 177 case '-': 178 match := rxLineMatchFlag.Find(b[pl.Position:]) 179 if len(match) == 0 { 180 pl.Description = string(b[pl.Position:]) 181 return pl 182 } 183 //pl.Flags = append(pl.Flags, string(match)) 184 //pl.Position += len(match) 185 i := parseFlag(b[pl.Position:], pl) 186 pl.Position += i 187 188 case '=', '[', '<': 189 start := pl.Position 190 group := true 191 grpC := b[pl.Position] 192 for ; pl.Position < len(b); pl.Position++ { 193 switch b[pl.Position] { 194 case '[', '<': 195 switch grpC { 196 case '=': 197 grpC = b[pl.Position] 198 } 199 case ']': 200 switch grpC { 201 case '[': 202 group = false 203 } 204 case '>': 205 switch grpC { 206 case '<': 207 group = false 208 } 209 case ' ', '\t', ',': 210 if grpC == '[' || grpC == '<' { 211 continue 212 } 213 group = false 214 } 215 if !group { 216 break 217 } 218 } 219 pl.Example = string(b[start:pl.Position]) 220 goto start 221 222 case ' ', '\t': 223 example := rxExampleCaps.Find(b[pl.Position+1:]) 224 switch { 225 case len(example) == 0: 226 // start of description 227 pl.Description = string(b[pl.Position+1:]) 228 return pl 229 case pl.Position+len(example) == len(b)-1: 230 // end of line 231 pl.Example = string(b[pl.Position:]) 232 pl.Position += len(example) + 1 233 return pl 234 default: 235 pl.Example = string(b[pl.Position : pl.Position+len(example)]) 236 pl.Position += len(example) 237 } 238 239 default: 240 pl.Description = string(b[pl.Position:]) 241 return pl 242 } 243 } 244 } 245 246 func parseFlag(b []byte, pl *parsedLineT) int { 247 //fmt.Println("parseFlag", string(b)) 248 var ( 249 split bool 250 bracket byte = 0 251 ) 252 253 for i, c := range b { 254 //fmt.Printf("i==%d c=='%s' bracket=%d\n", i, string(c), bracket) 255 switch { 256 case isValidFlagChar(c): 257 continue 258 259 case c == '[', c == '<': 260 switch { 261 case bracket == c: 262 return 0 263 case i+1 == len(b): 264 return 0 265 case b[i+1] == '=': 266 splitFlags(b[:i], split, pl) 267 return i 268 //case isValidFlagChar(b[i+1]), b[i+1] == '=', b[i+1] == '<': 269 // bracket = true 270 case bracket != 0: 271 continue 272 default: 273 // return 0 274 bracket = c 275 } 276 277 case c == ']': 278 switch bracket { 279 case 0: 280 return 0 281 case '[': 282 bracket = 0 283 split = true 284 default: 285 continue 286 } 287 288 case c == '>': 289 switch bracket { 290 case 0: 291 return 0 292 case '<': 293 bracket = 0 294 split = true 295 default: 296 continue 297 } 298 299 default: 300 if bracket != 0 { 301 continue 302 } 303 splitFlags(b[:i], split, pl) 304 return i 305 } 306 } 307 308 splitFlags(b, split, pl) 309 return len(b) 310 } 311 312 var ( 313 empty = []byte{} 314 braceOpen = []byte{'['} 315 braceClose = []byte{']'} 316 rxNoBrace = regexp.MustCompile(`\[.*?\]`) 317 ) 318 319 func splitFlags(b []byte, split bool, pl *parsedLineT) { 320 if !split { 321 pl.Flags = append(pl.Flags, string(b)) 322 return 323 } 324 325 full := bytes.ReplaceAll(b, braceOpen, empty) 326 full = bytes.ReplaceAll(full, braceClose, empty) 327 328 removed := rxNoBrace.ReplaceAll(b, empty) 329 330 pl.Flags = append(pl.Flags, string(full), string(removed)) 331 } 332 333 func isValidFlagChar(c byte) bool { 334 return c == '-' || 335 (c >= 'a' && c <= 'z') || 336 (c >= 'A' && c <= 'Z') || 337 (c >= '0' && c <= '9') 338 }