Add shared command path primitives

This commit is contained in:
2026-10-19 15:08:00 +00:00
parent d7cc12a347
commit b3fb795224
5 changed files with 207 additions and 30 deletions

View File

@@ -0,0 +1,106 @@
// Package commandpath provides the shared representation and operations for
// command paths used by CommandKit's core and backend adapter.
package commandpath
import (
"fmt"
"strconv"
"strings"
)
// Path is a sequence of command-name tokens. The empty path identifies the
// application root.
type Path []string
// ParseDeclaration parses a whitespace-separated command path for capture.
// Declaration paths must contain at least one component.
func ParseDeclaration(value string) (Path, error) {
path := Path(strings.Fields(value))
if len(path) == 0 {
return nil, fmt.Errorf("command path %q must contain at least one name", value)
}
for _, component := range path {
if err := ValidateComponent(component); err != nil {
return nil, err
}
}
return path.Clone(), nil
}
// ParseLookup parses a whitespace-separated lookup path. An empty or
// whitespace-only value selects the application root.
func ParseLookup(value string) (Path, error) {
path := Path(strings.Fields(value))
for _, component := range path {
if err := ValidateComponent(component); err != nil {
return nil, err
}
}
return path.Clone(), nil
}
// FromTokens makes an owned path copy without splitting or trimming tokens.
func FromTokens(tokens []string) Path {
return Path(tokens).Clone()
}
// Clone returns an owned copy of the path.
func (path Path) Clone() Path {
return append(Path(nil), path...)
}
// CloneTokens returns an owned copy of token data.
func CloneTokens(tokens []string) []string {
return append([]string(nil), tokens...)
}
// ValidateComponent validates one canonical command-name token.
func ValidateComponent(name string) error {
fields := strings.Fields(name)
if len(fields) != 1 || fields[0] != name || strings.HasPrefix(name, "-") || name == "--" {
return fmt.Errorf("command name %q must be one non-option token", name)
}
return nil
}
// Equal reports whether both paths contain the same components.
func (path Path) Equal(other Path) bool {
if len(path) != len(other) {
return false
}
for index := range path {
if path[index] != other[index] {
return false
}
}
return true
}
// HasPrefix reports whether prefix is a component-wise prefix of path.
func (path Path) HasPrefix(prefix Path) bool {
if len(prefix) > len(path) {
return false
}
for index := range prefix {
if path[index] != prefix[index] {
return false
}
}
return true
}
// Key returns a normalized, unambiguous key for this path.
func (path Path) Key() string {
var key strings.Builder
for _, component := range path {
key.WriteString(strconv.Itoa(len(component)))
key.WriteByte(':')
key.WriteString(component)
}
return key.String()
}
// String formats the path for diagnostics and help text.
func (path Path) String() string {
return strings.Join(path, " ")
}

View File

@@ -0,0 +1,80 @@
package commandpath
import "testing"
func TestParseDeclarationAndLookup(t *testing.T) {
declaration, err := ParseDeclaration(" config\tshow ")
if err != nil {
t.Fatal(err)
}
if got, want := declaration.String(), "config show"; got != want {
t.Fatalf("declaration string = %q, want %q", got, want)
}
if _, err := ParseDeclaration(" \t "); err == nil {
t.Fatal("ParseDeclaration accepted an empty path")
}
root, err := ParseLookup(" \t ")
if err != nil {
t.Fatal(err)
}
if len(root) != 0 || root.String() != "" {
t.Fatalf("empty lookup path = %#v, string %q", root, root.String())
}
lookup, err := ParseLookup("config show")
if err != nil || !lookup.Equal(declaration) {
t.Fatalf("lookup = %#v, err %v; want %#v", lookup, err, declaration)
}
quoted := `say"hello C:\tmp`
parsed, err := ParseDeclaration(quoted)
if err != nil {
t.Fatal(err)
}
if got := parsed.String(); got != quoted {
t.Fatalf("quoted path = %q, want literal %q", got, quoted)
}
}
func TestValidateComponent(t *testing.T) {
for _, value := range []string{"--help", "-x", "--", "name word", "name\tpart"} {
t.Run(value, func(t *testing.T) {
if err := ValidateComponent(value); err == nil {
t.Fatalf("ValidateComponent(%q) succeeded", value)
}
})
}
if err := ValidateComponent("under_score"); err != nil {
t.Fatalf("existing valid component rejected: %v", err)
}
}
func TestPathRelationsAndKey(t *testing.T) {
path := Path{"config", "show"}
if !path.Equal(Path{"config", "show"}) {
t.Fatal("equal paths did not compare equal")
}
if !path.HasPrefix(Path{"config"}) || !path.HasPrefix(nil) {
t.Fatal("path did not recognize component prefix")
}
if path.HasPrefix(Path{"show"}) || (Path{"config"}).HasPrefix(path) {
t.Fatal("path recognized a non-prefix")
}
if (Path{"a\x00b"}).Key() == (Path{"a", "b"}).Key() {
t.Fatal("normalized keys collided for distinct component sequences")
}
}
func TestTokenCopiesPreserveTokenContents(t *testing.T) {
input := []string{" show ", "", "--", "-x=value"}
path := FromTokens(input)
input[0] = "changed"
if path[0] != " show " {
t.Fatalf("FromTokens did not copy input: %#v", path)
}
copy := CloneTokens(path)
copy[1] = "changed"
if path[1] != "" {
t.Fatalf("CloneTokens did not copy input: %#v", path)
}
}