Skip to content

Commit daeffd0

Browse files
authored
feat: Match interface abbreviations in sequence steps without static table (#136)
Add matchSequenceStep() for sequence step matching that handles interface-style tokens (e.g. 'g0/0/2' matching 'GigabitEthernet0/0/2'). For tokens with a letter/digit boundary (alpha prefix + numeric suffix): - Step's alpha prefix must start with input's alpha prefix (case-insensitive) - Numeric suffix must match exactly This means 'int g0/0/2' matches 'interface GigabitEthernet0/0/2' but 'int gub0/0/2' and 'int g0/0/3' do not. No hardcoded abbreviation table — the sequence step itself provides the ground truth. Only applies to sequence matching, not context switches or supported commands. Closes #135
1 parent 40a27bf commit daeffd0

File tree

2 files changed

+79
-2
lines changed

2 files changed

+79
-2
lines changed

ssh_server/handlers/ciscohandlers.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,7 @@ func handleSequenceStep(t *term.Terminal, userInput string, fd *fakedevices.Fake
133133
return false, false
134134
}
135135
step := sequence[*seqIdx]
136-
match, _, multipleMatches := cmdmatch.Match(userInput, map[string]string{step.Command: ""})
137-
if !match || multipleMatches {
136+
if !matchSequenceStep(userInput, step.Command) {
138137
return false, false
139138
}
140139
output, err := fakedevices.TranscriptReader(step.Transcript, fd)
@@ -218,6 +217,51 @@ func dispatchCommand(t *term.Terminal, userInput string, fd *fakedevices.FakeDev
218217
return false
219218
}
220219

220+
// matchSequenceStep returns true if userInput matches the sequence step command.
221+
// Uses standard prefix matching for regular words, and for tokens containing a
222+
// letter/digit boundary (e.g. "g0/0/2"), matches the alpha prefix against the
223+
// step's alpha prefix and requires an exact suffix match.
224+
// This allows "int g0/0/2" to match "interface GigabitEthernet0/0/2" without
225+
// a hardcoded abbreviation table.
226+
func matchSequenceStep(userInput, stepCmd string) bool {
227+
userFields := strings.Fields(strings.ToLower(userInput))
228+
stepFields := strings.Fields(strings.ToLower(stepCmd))
229+
if len(userFields) != len(stepFields) {
230+
return false
231+
}
232+
for i, uf := range userFields {
233+
sf := stepFields[i]
234+
uAlpha, uSuffix := splitIfaceToken(uf)
235+
sAlpha, sSuffix := splitIfaceToken(sf)
236+
if uSuffix != "" || sSuffix != "" {
237+
// Interface-style token: alpha prefix must match, suffix must be equal
238+
if !strings.HasPrefix(sAlpha, uAlpha) || uSuffix != sSuffix {
239+
return false
240+
}
241+
} else {
242+
// Regular word: step word must start with user word (abbreviation)
243+
if !strings.HasPrefix(sf, uf) {
244+
return false
245+
}
246+
}
247+
}
248+
return true
249+
}
250+
251+
// splitIfaceToken splits a token like "gigabitethernet0/0/2" into ("gigabitethernet", "0/0/2").
252+
// Only splits when the suffix starts with a digit (interface-style tokens).
253+
// Returns (token, "") if there is no letter/digit boundary.
254+
func splitIfaceToken(token string) (alpha, suffix string) {
255+
i := 0
256+
for i < len(token) && token[i] >= 'a' && token[i] <= 'z' {
257+
i++
258+
}
259+
if i == 0 || i == len(token) || token[i] < '0' || token[i] > '9' {
260+
return token, ""
261+
}
262+
return token[:i], token[i:]
263+
}
264+
221265
// buildPrompt constructs the terminal prompt string.
222266
// If format is empty, falls back to hostname+context (default Cisco style).
223267
// If prefixLine is non-empty, it is prepended above the prompt on its own line.

ssh_server/handlers/ciscohandlers_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,3 +624,36 @@ func TestScenarioStrictContextSwitch(t *testing.T) {
624624
t.Errorf("expected show running-config to return output, got:\n%s", out)
625625
}
626626
}
627+
628+
func TestMatchSequenceStep(t *testing.T) {
629+
cases := []struct {
630+
input string
631+
step string
632+
want bool
633+
}{
634+
// Standard prefix matching
635+
{"show running-config", "show running-config", true},
636+
{"sho run", "show running-config", true},
637+
{"enable", "enable", true},
638+
{"en", "enable", true},
639+
// Interface token matching
640+
{"interface GigabitEthernet0/0/2", "interface GigabitEthernet0/0/2", true},
641+
{"int GigabitEthernet0/0/2", "interface GigabitEthernet0/0/2", true},
642+
{"int g0/0/2", "interface GigabitEthernet0/0/2", true},
643+
{"int gi0/0/2", "interface GigabitEthernet0/0/2", true},
644+
// Wrong suffix — must not match
645+
{"int g0/0/3", "interface GigabitEthernet0/0/2", false},
646+
// Wrong alpha prefix — must not match
647+
{"int gub0/0/2", "interface GigabitEthernet0/0/2", false},
648+
{"int fa0/0/2", "interface GigabitEthernet0/0/2", false},
649+
// Wrong word count
650+
{"interface GigabitEthernet0/0/2 extra", "interface GigabitEthernet0/0/2", false},
651+
}
652+
653+
for _, c := range cases {
654+
got := matchSequenceStep(c.input, c.step)
655+
if got != c.want {
656+
t.Errorf("matchSequenceStep(%q, %q) = %v; want %v", c.input, c.step, got, c.want)
657+
}
658+
}
659+
}

0 commit comments

Comments
 (0)