github.com/sercand/please@v13.4.0+incompatible/src/help/help.go (about)

     1  // +build !bootstrap
     2  
     3  // Package help prints help messages about parts of plz.
     4  package help
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"os"
    10  	"regexp"
    11  	"sort"
    12  	"strings"
    13  	"text/template"
    14  
    15  	"gopkg.in/op/go-logging.v1"
    16  
    17  	"github.com/thought-machine/please/src/cli"
    18  	"github.com/thought-machine/please/src/core"
    19  	"github.com/thought-machine/please/src/parse"
    20  	"github.com/thought-machine/please/src/utils"
    21  )
    22  
    23  var log = logging.MustGetLogger("help")
    24  
    25  const topicsHelpMessage = `
    26  The following help topics are available:
    27  
    28  %s`
    29  
    30  // maxSuggestionDistance is the maximum Levenshtein edit distance we'll suggest help topics at.
    31  const maxSuggestionDistance = 4
    32  
    33  // Help prints help on a particular topic.
    34  // It returns true if the topic is known or false if it isn't.
    35  func Help(topic string) bool {
    36  	if message := help(topic); message != "" {
    37  		printMessage(message)
    38  		return true
    39  	}
    40  	fmt.Printf("Sorry OP, can't halp you with %s\n", topic)
    41  	if message := suggest(topic); message != "" {
    42  		printMessage(message)
    43  		fmt.Printf(" Or have a look on the website: https://please.build\n")
    44  	} else {
    45  		fmt.Printf("\nMaybe have a look on the website? https://please.build\n")
    46  	}
    47  	return false
    48  }
    49  
    50  // Topics prints the list of help topics beginning with the given prefix.
    51  func Topics(prefix string) {
    52  	for _, topic := range allTopics() {
    53  		if strings.HasPrefix(topic, prefix) {
    54  			fmt.Println(topic)
    55  		}
    56  	}
    57  }
    58  
    59  func help(topic string) string {
    60  	topic = strings.ToLower(topic)
    61  	if topic == "topics" {
    62  		return fmt.Sprintf(topicsHelpMessage, strings.Join(allTopics(), "\n"))
    63  	}
    64  	for _, filename := range AssetNames() {
    65  		if message, found := findHelpFromFile(topic, filename); found {
    66  			return message
    67  		}
    68  	}
    69  	// Check built-in build rules.
    70  	m := parse.AllBuiltinFunctions(core.NewDefaultBuildState(), nil)
    71  	if f, present := m[topic]; present {
    72  		var b strings.Builder
    73  		if err := template.Must(template.New("").Parse(docstringTemplate)).Execute(&b, f); err != nil {
    74  			log.Fatalf("%s", err)
    75  		}
    76  		s := strings.Replace(b.String(), "    Args:\n", "    ${BOLD_YELLOW}Args:${RESET}\n", 1)
    77  		for _, a := range f.Arguments {
    78  			r := regexp.MustCompile("( +)(" + a.Name + `)( \([a-z |]+\))?:`)
    79  			s = r.ReplaceAllString(s, "$1$${YELLOW}$2$${RESET}$${GREEN}$3$${RESET}:")
    80  		}
    81  		return s
    82  	}
    83  	return ""
    84  }
    85  
    86  func findHelpFromFile(topic, filename string) (string, bool) {
    87  	preamble, topics := loadData(filename)
    88  	message, found := topics[topic]
    89  	if !found {
    90  		return "", false
    91  	}
    92  	if preamble == "" {
    93  		return message, true
    94  	}
    95  	return fmt.Sprintf(preamble+"\n\n", topic) + message, true
    96  }
    97  
    98  func loadData(filename string) (string, map[string]string) {
    99  	data := MustAsset(filename)
   100  	f := helpFile{}
   101  	if err := json.Unmarshal(data, &f); err != nil {
   102  		log.Fatalf("Failed to load help data: %s\n", err)
   103  	}
   104  	return f.Preamble, f.Topics
   105  }
   106  
   107  // suggest looks through all known help topics and tries to make a suggestion about what the user might have meant.
   108  func suggest(topic string) string {
   109  	return utils.PrettyPrintSuggestion(topic, allTopics(), maxSuggestionDistance)
   110  }
   111  
   112  // allTopics returns all the possible topics to get help on.
   113  func allTopics() []string {
   114  	topics := []string{}
   115  	for _, filename := range AssetNames() {
   116  		_, data := loadData(filename)
   117  		for t := range data {
   118  			topics = append(topics, t)
   119  		}
   120  	}
   121  	for t := range parse.AllBuiltinFunctions(core.NewDefaultBuildState(), nil) {
   122  		topics = append(topics, t)
   123  	}
   124  	sort.Strings(topics)
   125  	return topics
   126  }
   127  
   128  // helpFile is a struct we use for unmarshalling.
   129  type helpFile struct {
   130  	Preamble string            `json:"preamble"`
   131  	Topics   map[string]string `json:"topics"`
   132  }
   133  
   134  // printMessage prints a message, with some string replacements for ANSI codes.
   135  func printMessage(msg string) {
   136  	if cli.StdErrIsATerminal && cli.StdOutIsATerminal {
   137  		backtickRegex := regexp.MustCompile("\\`[^\\`\n]+\\`")
   138  		msg = backtickRegex.ReplaceAllStringFunc(msg, func(s string) string {
   139  			return "${BOLD_CYAN}" + strings.Replace(s, "`", "", -1) + "${RESET}"
   140  		})
   141  	}
   142  	// Replace % to %% when not followed by anything so it doesn't become a replacement.
   143  	cli.Fprintf(os.Stdout, strings.Replace(msg, "% ", "%% ", -1)+"\n")
   144  }
   145  
   146  const docstringTemplate = `${BLUE}{{ .Name }}${RESET} is a built-in build rule in Please. Instructions for use & its arguments:
   147  
   148  ${BOLD_YELLOW}{{ .Name }}${RESET}(
   149  {{- range $i, $a := .Arguments }}{{ if gt $i 0 }}, {{ end }}${GREEN}{{ $a.Name }}${RESET}{{ end -}}
   150  ):
   151  
   152  {{ .Docstring }}
   153  
   154  Online help is available at https://please.build/lexicon.html#{{ .Name }}.
   155  `