github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/utils/man/man_unix.go (about) 1 //go:build !windows 2 // +build !windows 3 4 package man 5 6 import ( 7 "bufio" 8 "compress/gzip" 9 "io" 10 "os" 11 "os/exec" 12 "regexp" 13 "sort" 14 "strings" 15 16 "github.com/lmorg/murex/utils/cache" 17 "github.com/lmorg/murex/utils/rmbs" 18 ) 19 20 const errPrefix = "error parsing man page: " 21 22 var ( 23 rxMatchManSection = regexp.MustCompile(`/man[1678]/`) 24 rxMatchFlagsEscaped = regexp.MustCompile(`\\f[BI]((\\-|-)[a-zA-Z0-9]|(\\-\\-|--)[\\\-a-zA-Z0-9]+).*?\\f[RP]`) 25 rxMatchFlagsQuoted = regexp.MustCompile(`\.IP "(.*?)"`) 26 rxMatchFlagsDarwin = regexp.MustCompile(`\.It Fl ([a-zA-Z0-9])`) 27 rxMatchFlagsOther = regexp.MustCompile(`\.B (.*?)`) 28 rxMatchFlagsNoFmt = regexp.MustCompile(`(--[\-a-zA-Z0-9]+)=([_\-a-zA-Z0-9]+)`) 29 rxMatchGetFlag = regexp.MustCompile(`(--[\-a-zA-Z0-9]+)`) 30 rxReplaceMarkup = regexp.MustCompile(`\.[a-zA-Z]+(\s|)`) 31 ) 32 33 // GetManPages executes `man -w` to locate the manual files 34 func GetManPages(exe string) []string { 35 var paths []string 36 37 if cache.Read(cache.MAN_PATHS, exe, &paths) { 38 return paths 39 } 40 41 // Get paths 42 cmd := exec.Command("man", "-w", exe) 43 b, err := cmd.Output() 44 if err != nil { 45 return nil 46 } 47 48 s := strings.TrimSpace(string(b)) 49 if s == exe { 50 return nil 51 } 52 53 paths = strings.Split(s, ":") 54 cache.Write(cache.MAN_PATHS, exe, paths, cache.Days(30)) 55 return paths 56 } 57 58 // ParseByPaths runs the parser to locate any flags with hyphen prefixes 59 func ParseByPaths(command string, paths []string) ([]string, map[string]string) { 60 var f flagsT 61 if cache.Read(cache.MAN_FLAGS, command, &f) { 62 return f.Flags, f.Descriptions 63 } 64 65 f.Descriptions = make(map[string]string) 66 67 for i := range paths { 68 if !rxMatchManSection.MatchString(paths[i]) { 69 continue 70 } 71 72 scanner, closer, err := createScanner(paths[i]) 73 switch { 74 case err != nil: 75 return []string{errPrefix + err.Error()}, map[string]string{} 76 case scanner == nil: 77 return []string{errPrefix + "scanner is undefined"}, map[string]string{} 78 default: 79 parseFlags(&f.Descriptions, scanner) 80 closer() 81 } 82 } 83 84 parseDescriptions(command, &f.Descriptions) 85 86 f.Flags = make([]string, len(f.Descriptions)) 87 var i int 88 for flag := range f.Descriptions { 89 f.Flags[i] = flag 90 i++ 91 } 92 sort.Strings(f.Flags) 93 94 cache.Write(cache.MAN_FLAGS, command, f, cache.Days(30)) 95 return f.Flags, f.Descriptions 96 } 97 98 func createScanner(filename string) (*bufio.Scanner, func() error, error) { 99 var scanner *bufio.Scanner 100 101 file, err := os.Open(filename) 102 if err != nil { 103 return nil, nil, err 104 } 105 106 closer := file.Close 107 108 if len(filename) > 3 && filename[len(filename)-3:] == ".gz" { 109 gz, err := gzip.NewReader(file) 110 if err != nil { 111 return nil, closer, err 112 } 113 114 closer = func() error { 115 gz.Close() 116 file.Close() 117 return nil 118 } 119 scanner = bufio.NewScanner(gz) 120 121 } else { 122 scanner = bufio.NewScanner(file) 123 } 124 125 return scanner, closer, err 126 } 127 128 // ParseByStdio runs the parser to locate any flags with hyphen prefixes 129 func ParseByStdio(r io.Reader) ([]string, map[string]string) { 130 fMap := make(map[string]string) 131 132 parseDescriptionsLines(r, &fMap) 133 134 flags := make([]string, len(fMap)) 135 var i int 136 for f := range fMap { 137 flags[i] = f 138 i++ 139 } 140 sort.Strings(flags) 141 142 return flags, fMap 143 } 144 145 func parseFlags(flags *map[string]string, scanner *bufio.Scanner) { 146 for scanner.Scan() { 147 s := rmbs.Remove(scanner.Text()) 148 149 match := rxMatchFlagsEscaped.FindAllStringSubmatch(s, -1) 150 for i := range match { 151 if len(match[i]) == 0 { 152 continue 153 } 154 155 s := strings.Replace(match[i][1], `\`, "", -1) 156 if strings.HasSuffix(s, "fR") || strings.HasSuffix(s, "fP") { 157 s = s[:len(s)-2] 158 } 159 (*flags)[s] = "" 160 } 161 162 match = rxMatchFlagsQuoted.FindAllStringSubmatch(s, -1) 163 for i := range match { 164 if len(match[i]) == 0 { 165 continue 166 } 167 168 flag := rxMatchGetFlag.FindAllStringSubmatch(match[i][1], -1) 169 for j := range flag { 170 if len(flag[j]) == 0 { 171 continue 172 } 173 174 (*flags)[flag[j][1]] = "" 175 } 176 } 177 178 match = rxMatchFlagsDarwin.FindAllStringSubmatch(s, -1) // eg `cat` on OSX 179 for i := range match { 180 if len(match[i]) == 0 { 181 continue 182 } 183 184 (*flags)["-"+match[i][1]] = "" 185 } 186 187 match = rxMatchFlagsOther.FindAllStringSubmatch(s, -1) 188 for i := range match { 189 if len(match[i]) == 0 { 190 continue 191 } 192 193 //// Fix \^ seen on some OSX man pages 194 //match[i][1] = strings.Replace(match[i][1], `\^`, "", -1) 195 196 flag := rxMatchGetFlag.FindAllStringSubmatch(match[i][1], -1) 197 for j := range flag { 198 if len(flag[j]) == 0 { 199 continue 200 } 201 202 (*flags)[flag[j][1]] = "" 203 } 204 } 205 206 match = rxMatchFlagsNoFmt.FindAllStringSubmatch(s, -1) 207 for i := range match { 208 if len(match[i]) != 3 { 209 continue 210 } 211 212 (*flags)[match[i][1]] = "" 213 } 214 215 match = rxMatchGetFlag.FindAllStringSubmatch(s, -1) 216 for i := range match { 217 if len(match[i]) != 2 { 218 continue 219 } 220 if strings.HasPrefix(match[i][1], "---") { 221 continue 222 } 223 224 (*flags)[match[i][1]] = "" 225 } 226 } 227 228 if scanner.Err() != nil { 229 panic(errPrefix + scanner.Err().Error()) 230 } 231 }