157 lines
3.9 KiB
Go
157 lines
3.9 KiB
Go
package command
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// CandidateStatus is the diagnostic disposition of one command file.
|
|
type CandidateStatus string
|
|
|
|
const (
|
|
CandidateWinner CandidateStatus = "winner"
|
|
CandidateShadowed CandidateStatus = "shadowed"
|
|
CandidateError CandidateStatus = "error"
|
|
)
|
|
|
|
// Candidate is one command file considered during Load, including shadowed
|
|
// sources that later directories override.
|
|
type Candidate struct {
|
|
Name string
|
|
Description string
|
|
Path string
|
|
Root string
|
|
Status CandidateStatus
|
|
WinnerPath string
|
|
Error string
|
|
}
|
|
|
|
// RootInfo is one scanned commands directory.
|
|
type RootInfo struct {
|
|
Dir string
|
|
Status string // ok | missing | not-directory | unreadable
|
|
}
|
|
|
|
// Inspection is a read-only snapshot matching command.Load override semantics
|
|
// (later dir wins) without changing Load itself.
|
|
type Inspection struct {
|
|
Roots []RootInfo
|
|
Candidates []Candidate
|
|
Winners []Command
|
|
}
|
|
|
|
// Inspect walks dirs in order (same as Load) and records every candidate.
|
|
// Missing dirs are listed as missing and produce no warnings.
|
|
func Inspect(dirs ...string) Inspection {
|
|
var roots []RootInfo
|
|
// Per-name list of candidates in scan order; last becomes winner.
|
|
byName := map[string][]Candidate{}
|
|
winners := map[string]Command{}
|
|
|
|
for _, dir := range dirs {
|
|
root, err := filepath.Abs(dir)
|
|
if err != nil {
|
|
roots = append(roots, RootInfo{Dir: dir, Status: "unreadable"})
|
|
continue
|
|
}
|
|
st := rootStatus(root)
|
|
roots = append(roots, RootInfo{Dir: root, Status: st})
|
|
if st != "ok" {
|
|
continue
|
|
}
|
|
visited := map[string]bool{}
|
|
if real, err := filepath.EvalSymlinks(root); err == nil {
|
|
visited[real] = true
|
|
} else {
|
|
visited[root] = true
|
|
}
|
|
walkCommands(root, root, visited, func(path string) {
|
|
c, perr := parseFile(root, path)
|
|
if perr != nil {
|
|
name := guessName(root, path)
|
|
byName[name] = append(byName[name], Candidate{
|
|
Name: name, Path: path, Root: root,
|
|
Status: CandidateError, Error: perr.Error(),
|
|
})
|
|
return
|
|
}
|
|
byName[c.Name] = append(byName[c.Name], Candidate{
|
|
Name: c.Name, Description: c.Description, Path: path, Root: root,
|
|
Status: CandidateWinner, // provisional; finalized below
|
|
})
|
|
winners[c.Name] = c
|
|
})
|
|
}
|
|
|
|
var candidates []Candidate
|
|
for name, list := range byName {
|
|
// Find last non-error candidate as winner; errors stay as errors.
|
|
winIdx := -1
|
|
for i := len(list) - 1; i >= 0; i-- {
|
|
if list[i].Status != CandidateError {
|
|
winIdx = i
|
|
break
|
|
}
|
|
}
|
|
for i, c := range list {
|
|
if c.Status == CandidateError {
|
|
candidates = append(candidates, c)
|
|
continue
|
|
}
|
|
if i == winIdx {
|
|
c.Status = CandidateWinner
|
|
c.WinnerPath = ""
|
|
} else if winIdx >= 0 {
|
|
c.Status = CandidateShadowed
|
|
c.WinnerPath = list[winIdx].Path
|
|
}
|
|
candidates = append(candidates, c)
|
|
}
|
|
_ = name
|
|
}
|
|
|
|
cmds := make([]Command, 0, len(winners))
|
|
for _, c := range winners {
|
|
cmds = append(cmds, c)
|
|
}
|
|
sort.Slice(cmds, func(i, j int) bool { return cmds[i].Name < cmds[j].Name })
|
|
sort.SliceStable(candidates, func(i, j int) bool {
|
|
if candidates[i].Name != candidates[j].Name {
|
|
return candidates[i].Name < candidates[j].Name
|
|
}
|
|
order := map[CandidateStatus]int{CandidateWinner: 0, CandidateShadowed: 1, CandidateError: 2}
|
|
return order[candidates[i].Status] < order[candidates[j].Status]
|
|
})
|
|
|
|
return Inspection{Roots: roots, Candidates: candidates, Winners: cmds}
|
|
}
|
|
|
|
func rootStatus(dir string) string {
|
|
info, err := os.Stat(dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "missing"
|
|
}
|
|
return "unreadable"
|
|
}
|
|
if !info.IsDir() {
|
|
return "not-directory"
|
|
}
|
|
f, err := os.Open(dir)
|
|
if err != nil {
|
|
return "unreadable"
|
|
}
|
|
_ = f.Close()
|
|
return "ok"
|
|
}
|
|
|
|
func guessName(root, path string) string {
|
|
rel, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
rel = filepath.Base(path)
|
|
}
|
|
return strings.ReplaceAll(strings.TrimSuffix(filepath.ToSlash(rel), ".md"), "/", ":")
|
|
}
|