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  }