fix(11.1-07): require built, run src= code; case-sensitive go doc; parse command forms
- .go src= targets must be in the default build and reached from a Test or an Example with output - go doc -c makes the identifier fallback case-sensitive - commandWord parses env prefixes, flags, go run and bin/ forms
This commit is contained in:
@@ -30,10 +30,6 @@ type Commands struct {
|
||||
// are checked as commands and highlighted with prompts.
|
||||
var shellLangs = []string{"sh", "shell", "bash", "console"}
|
||||
|
||||
// commandToken finds `summer <name>` and `./bin/<app> <name>` at the start
|
||||
// of a shell command (after an optional "$ " prompt).
|
||||
var commandToken = regexp.MustCompile(`^(?:\$\s+)?(summer|\./bin/[A-Za-z0-9._-]+)\s+(\S+)`)
|
||||
|
||||
// commandSeparators split one shell line into its commands.
|
||||
var commandSeparators = regexp.MustCompile(`&&|\|\||;|\|`)
|
||||
|
||||
@@ -64,14 +60,13 @@ func (s *site) checkCommands(docs []parsedDoc) ([]Problem, error) {
|
||||
var problems []Problem
|
||||
check := func(d parsedDoc, line int, text string) {
|
||||
for _, cmd := range commandSeparators.Split(text, -1) {
|
||||
m := commandToken.FindStringSubmatch(strings.TrimSpace(cmd))
|
||||
if m == nil || strings.HasPrefix(m[2], "-") {
|
||||
name, isTool, ok := commandWord(cmd)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name := m[2]
|
||||
known := tool[name]
|
||||
if m[1] != "summer" {
|
||||
known = app[name]
|
||||
known := app[name]
|
||||
if isTool {
|
||||
known = tool[name]
|
||||
}
|
||||
if !known {
|
||||
problems = append(problems, Problem{File: d.file, Line: line, Rule: "command",
|
||||
@@ -102,6 +97,91 @@ func (s *site) checkCommands(docs []parsedDoc) ([]Problem, error) {
|
||||
return problems, nil
|
||||
}
|
||||
|
||||
// commandWord finds the summer or application command word in one shell
|
||||
// command. tool is true for summer and `go run ./cmd/summer`, false for
|
||||
// ./bin/{app} and bin/{app}. ok is false when the line is not one of
|
||||
// those programs or when flags leave no command word. Flag handling
|
||||
// matches cobra's stripFlags for a root whose only flag is the bool
|
||||
// --help / -h: "--" ends the search, --help and -h take no value, any
|
||||
// other --name or two-character -x consumes the next token, and a token
|
||||
// that already contains "=" is skipped alone.
|
||||
func commandWord(cmd string) (name string, tool bool, ok bool) {
|
||||
fields := strings.Fields(strings.TrimSpace(cmd))
|
||||
if len(fields) > 0 && fields[0] == "$" {
|
||||
fields = fields[1:]
|
||||
}
|
||||
for len(fields) > 0 && isAssignment(fields[0]) {
|
||||
fields = fields[1:]
|
||||
}
|
||||
switch {
|
||||
case len(fields) == 0:
|
||||
return "", false, false
|
||||
case fields[0] == "summer":
|
||||
tool = true
|
||||
fields = fields[1:]
|
||||
case len(fields) >= 3 && fields[0] == "go" && fields[1] == "run" && fields[2] == "./cmd/summer":
|
||||
tool = true
|
||||
fields = fields[3:]
|
||||
case isAppBin(fields[0]):
|
||||
fields = fields[1:]
|
||||
default:
|
||||
return "", false, false
|
||||
}
|
||||
for len(fields) > 0 {
|
||||
s := fields[0]
|
||||
switch {
|
||||
case s == "--":
|
||||
return "", tool, false
|
||||
case strings.HasPrefix(s, "-") && strings.Contains(s, "="):
|
||||
fields = fields[1:]
|
||||
case s == "--help" || s == "-h":
|
||||
fields = fields[1:]
|
||||
case strings.HasPrefix(s, "--") || (strings.HasPrefix(s, "-") && len(s) == 2):
|
||||
if len(fields) < 3 {
|
||||
return "", tool, false
|
||||
}
|
||||
fields = fields[2:]
|
||||
case strings.HasPrefix(s, "-"):
|
||||
fields = fields[1:]
|
||||
default:
|
||||
return s, tool, true
|
||||
}
|
||||
}
|
||||
return "", tool, false
|
||||
}
|
||||
|
||||
// isAssignment reports a leading VAR=value token: a name of letters,
|
||||
// digits and underscores that does not start with a digit.
|
||||
func isAssignment(tok string) bool {
|
||||
name, _, ok := strings.Cut(tok, "=")
|
||||
if !ok || name == "" || (name[0] >= '0' && name[0] <= '9') {
|
||||
return false
|
||||
}
|
||||
for _, r := range name {
|
||||
if r != '_' && (r < '0' || r > '9') && (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isAppBin reports ./bin/{app} and bin/{app}, with app in [A-Za-z0-9._-].
|
||||
func isAppBin(tok string) bool {
|
||||
rest, ok := strings.CutPrefix(tok, "./bin/")
|
||||
if !ok {
|
||||
rest, ok = strings.CutPrefix(tok, "bin/")
|
||||
}
|
||||
if !ok || rest == "" || strings.Contains(rest, "/") {
|
||||
return false
|
||||
}
|
||||
for _, r := range rest {
|
||||
if r != '.' && r != '_' && r != '-' && (r < '0' || r > '9') && (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// exampleCommandNames returns the string-literal Name of every
|
||||
// bonfire.Command composite literal (including the elided elements of a
|
||||
// []bonfire.Command literal) in the non-test Go files under dir. It parses
|
||||
|
||||
Reference in New Issue
Block a user