Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions sources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package main

import (
"fmt"
"os"
"os/exec"
"regexp"
"slices"
"strings"
)

var (
overStrike = regexp.MustCompile(`.\x08`)
flagLine = regexp.MustCompile(`^(\s{1,8})(-.*?)(?:\s{2,}(.*))?$`)
flagCluster = regexp.MustCompile(`,\s+|\s+\|\s+`)
subCmdLine = regexp.MustCompile(`^([ \t]{1,8})(\S+?)(?:\s{2,}(.*))?$`)
synopsisBlock = regexp.MustCompile(`(?s)SYNOPSIS\n(.*?)\n[A-Z][A-Z ]+\n`)
)

// source is a struct that represents a source of information like man page, --help output and many other
// it comes with a set of functions allowing the source to retain it's own way of capturing targets
type source interface {
name() string
fetch(args []string) (string, error)
parseFlags(text string, args []string) []entry
parseSubcmd(text string, args []string) []entry
}

// CmdManPage is a struct implementing source interface with functions for parsing and fetching man page
type CmdManPage struct{}

func (s *CmdManPage) name() string {
return "command man page"
}

func (s *CmdManPage) fetch(args []string) (string, error) {
return manPage(args[0])
}

func (s *CmdManPage) parseFlags(text string, args []string) []entry {
if m := matchedEntries(args[1:], text); len(m) > 0 {
return m
}
return []entry{}
}

func (s *CmdManPage) parseSubcmd(text string, args []string) []entry {
subEntries := parseSubCmd(text)
result := []entry{}
for _, sc := range subEntries {
for _, a := range args {
if slices.Contains(sc.flags, a) {
result = append(result, sc)
}
}
}
return result
}

// SubCmdManPage is a struct implementing source interface with functions for parsing and fetching man page
type SubCmdManPage struct{}

func (s *SubCmdManPage) name() string {
return "sub command man page"
}

func (s *SubCmdManPage) fetch(args []string) (string, error) {
if !strings.HasPrefix(args[1], "-") {
return manPage(args[0] + args[1])
} else {
return manPage(args[0])
}
}

func (s *SubCmdManPage) parseFlags(text string, args []string) []entry {
if m := matchedEntries(args[1:], text); len(m) > 0 {
return m
}
return []entry{}
}

func (s *SubCmdManPage) parseSubcmd(text string, args []string) []entry {
// TODO: chain sub commands
subEntries := parseSubCmd(text)
result := []entry{}
for _, sc := range subEntries {
for _, a := range args {
if slices.Contains(sc.flags, a) {
result = append(result, sc)
}
}
}
return result
}

// synopsisCheck checks if the line after SYNOPSIS section show a genuine sub command (cmd sub [...) or a command (cmd-sub [...)
// a good example of this can be ssh-add where a command like ssh add (host named add) can trigger a false man page
func synopsisCheck(text, name string) bool {
m := synopsisBlock.FindStringSubmatch(text)
if m == nil {
return false
}
word := regexp.MustCompile(`\b` + regexp.QuoteMeta(name) + `\b`)
return word.MatchString(m[1])
}

// manPage returns manpage with formatting removed
func manPage(page string) (string, error) {
c := exec.Command("man", page)
c.Env = append(os.Environ(), "MANWIDTH=80", "TERM=DUMB")
out, err := c.CombinedOutput()
if err != nil {
return "", err
}
return string(overStrike.ReplaceAll(out, nil)), nil
}

// helpOutput returns the output of the --help flag or just the help as a sub command
func helpOutput(cmd, flag string) (string, error) {
out, err := exec.Command(cmd, flag).CombinedOutput()
if err != nil {
return "", err
}
if len(out) == 0 {
return "", fmt.Errorf("lenght of help output is zero")
}
return string(out), nil
}
188 changes: 29 additions & 159 deletions whats.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"flag"
"fmt"
"os"
"os/exec"
"regexp"
"slices"
"strings"
)
Expand All @@ -16,15 +14,7 @@ const maxLen = 80
// flagLine matches a flag entry: indented line starting with -x or
// --long, with
// the description either after 2+ spaces on the same line or on following lines.
var (
overStrike = regexp.MustCompile(`.\x08`)
flagLine = regexp.MustCompile(`^(\s{1,8})(-.*?)(?:\s{2,}(.*))?$`)
flagCluster = regexp.MustCompile(`,\s+|\s+\|\s+`)
subCmdLine = regexp.MustCompile(`^([ \t]{1,8})(\S+?)(?:\s{2,}(.*))?$`)
synopsisBlock = regexp.MustCompile(`(?s)SYNOPSIS\n(.*?)\n[A-Z][A-Z ]+\n`)

isDump = *flag.Bool("dump", false, "dump the entire parsed slice of entries")
)
var isDump = *flag.Bool("dump", false, "dump the entire parsed slice of entries")

// entry is a flag and its description
type entry struct {
Expand Down Expand Up @@ -56,94 +46,33 @@ func main() {
if args[0] == "sudo" {
args = slices.Delete(args, 0, 1)
}
fmt.Println(args)

var result []entry

isSubCmd := false
if !strings.HasPrefix(args[1], "-") {
isSubCmd = true
sources := []source{
&CmdManPage{},
&SubCmdManPage{},
}

c := []entry{}
s := []entry{}
result := []entry{}

if text, err := manPage(args[0]); err == nil && len(text) != 0 {
var m []entry
if isSubCmd {
m = matchedEntries(args[2:], text)
} else {
m = matchedEntries(args[1:], text)
}
if len(m) > 0 {
c = append(c, m...)
for _, s := range sources {
text, err := s.fetch(args)
if err != nil {
continue
}
}

var subManText string

if isSubCmd {
if text, err := manPage(args[0] + "-" + args[1]); err == nil && len(text) != 0 {
subManText = text
matches := matchedEntries(args[1:], text)
if len(matches) > 0 {
s = append(s, matches...)
}

subEntries := parseSubCmd(text)
for _, sc := range subEntries {
if slices.Contains(sc.flags, args[1]) {
s = append(s, sc)
}
cmdMatches := s.parseFlags(text, args)
for _, m := range cmdMatches {
if isDuplicate(result, m) {
continue
}
result = append(result, m)
}
}

genuineSubCmd := isSubCmd && subManText != "" && hasSubcommand(subManText, args[0]+"-"+args[1])

if genuineSubCmd && len(c) > 0 {
result = append(result, c...)
} else if len(c) > len(s) {
result = append(result, c...)
} else {
result = append(result, s...)
}

if len(result) < len(args[1:]) {
if text, err := helpOutput(args[0], "--help"); err == nil && len(text) > 0 {
var r []entry
if isSubCmd {
r = matchedEntries(args[2:], text)
} else {
r = matchedEntries(args[1:], text)
}

if len(r) > 0 {
for _, e := range r {
if !matchesArg(e, args) {
continue
}

if containsEntry(result, e) {
continue
}

result = append(result, e)
}
}
if isSubCmd {
subEntries := parseSubCmd(text)

if len(subEntries) > 0 {
for _, sc := range subEntries {
if slices.Contains(sc.flags, args[1]) {
if !containsEntry(result, sc) {
result = append(result, sc)
}
}
}
}
subCmdMatches := s.parseSubcmd(text, args)
for _, m := range subCmdMatches {
if isDuplicate(result, m) {
continue
}
result = append(result, m)
}
}

Expand All @@ -161,52 +90,13 @@ func main() {
}
}

func hasSubcommand(text, name string) bool {
m := synopsisBlock.FindStringSubmatch(text)
if m == nil {
return false
}
word := regexp.MustCompile(`\b` + regexp.QuoteMeta(name) + `\b`)
return word.MatchString(m[1])
}

func entriesEqual(a, b entry) bool {
if a.desc != b.desc {
return false
}

if len(a.flags) != len(b.flags) {
return false
}

for i := range a.flags {
if a.flags[i] != b.flags[i] { // TODO: use reflect instead
return false
}
}

return true
}

func containsEntry(l []entry, e entry) bool {
for _, existing := range l {
if entriesEqual(existing, e) {
return true
}
}
return false
}

func matchesArg(e entry, args []string) bool {
for _, f := range e.flags {
for _, a := range args {
if f == a {
return true
}
}
}

return false
// isDuplicate checks if slice A has any element of entry B's slice of Flags
func isDuplicate(result []entry, m entry) bool {
return slices.ContainsFunc(result, func(r entry) bool {
return slices.ContainsFunc(r.flags, func(a string) bool {
return slices.Contains(m.flags, a)
})
})
}

// matchedEntries returns a slice of entry's that match flags in args
Expand Down Expand Up @@ -245,6 +135,9 @@ func parseSubCmd(text string) []entry {
var entries []entry
for i, line := range lines {
if m := subCmdLine.FindStringSubmatch(line); m != nil {
if strings.HasPrefix(m[2], "-") {
continue
}
description := descAfter(lines, i, m[3])
if description == "" {
description = descAfter(lines, i+1, m[3])
Expand Down Expand Up @@ -378,26 +271,3 @@ func getSummary(desc string) string {
}
return ""
}

// manPage returns manpage with formatting removed
func manPage(page string) (string, error) {
c := exec.Command("man", page)
c.Env = append(os.Environ(), "MANWIDTH=80")
out, err := c.CombinedOutput()
if err != nil {
return "", err
}
return string(overStrike.ReplaceAll(out, nil)), nil
}

// helpOutput returns the output of the --help flag or just the help as a sub command
func helpOutput(cmd, flag string) (string, error) {
out, err := exec.Command(cmd, flag).CombinedOutput()
if err != nil {
return "", err
}
if len(out) == 0 {
return "", fmt.Errorf("lenght of help output is zero")
}
return string(out), nil
}