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  }