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 `