package cisco import ( "fmt" "strings" ) // commandResult holds the output of a command and whether the session should end. type commandResult struct { output string exit bool } // commandEntry defines a single command with its name and optional sub-commands. type commandEntry struct { name string subs []commandEntry // nil for leaf commands } // userExecCommands defines the command tree for user EXEC mode. var userExecCommands = []commandEntry{ {name: "show", subs: []commandEntry{ {name: "version"}, {name: "clock"}, {name: "ip", subs: []commandEntry{ {name: "route"}, {name: "interface", subs: []commandEntry{ {name: "brief"}, }}, }}, {name: "interfaces"}, {name: "vlan", subs: []commandEntry{ {name: "brief"}, }}, }}, {name: "enable"}, {name: "exit"}, {name: "?"}, } // privilegedExecCommands extends user commands for privileged mode. var privilegedExecCommands = []commandEntry{ {name: "show", subs: []commandEntry{ {name: "version"}, {name: "clock"}, {name: "ip", subs: []commandEntry{ {name: "route"}, {name: "interface", subs: []commandEntry{ {name: "brief"}, }}, }}, {name: "interfaces"}, {name: "running-config"}, {name: "startup-config"}, {name: "vlan", subs: []commandEntry{ {name: "brief"}, }}, }}, {name: "configure", subs: []commandEntry{ {name: "terminal"}, }}, {name: "write", subs: []commandEntry{ {name: "memory"}, }}, {name: "copy"}, {name: "reload"}, {name: "disable"}, {name: "terminal", subs: []commandEntry{ {name: "length"}, }}, {name: "exit"}, {name: "?"}, } // globalConfigCommands defines the command tree for global config mode. var globalConfigCommands = []commandEntry{ {name: "hostname"}, {name: "interface"}, {name: "ip", subs: []commandEntry{ {name: "route"}, }}, {name: "no"}, {name: "end"}, {name: "exit"}, {name: "?"}, } // interfaceConfigCommands defines the command tree for interface config mode. var interfaceConfigCommands = []commandEntry{ {name: "ip", subs: []commandEntry{ {name: "address"}, }}, {name: "description"}, {name: "shutdown"}, {name: "no", subs: []commandEntry{ {name: "shutdown"}, }}, {name: "switchport", subs: []commandEntry{ {name: "mode"}, }}, {name: "end"}, {name: "exit"}, {name: "?"}, } // commandsForMode returns the command tree for the given IOS mode. func commandsForMode(mode iosMode) []commandEntry { switch mode { case modeUserExec: return userExecCommands case modePrivilegedExec: return privilegedExecCommands case modeGlobalConfig: return globalConfigCommands case modeInterfaceConfig: return interfaceConfigCommands default: return userExecCommands } } // resolveAbbreviation attempts to match an abbreviated word against a list of // command entries. It returns the matched entry name, or an error string if // ambiguous or unknown. func resolveAbbreviation(word string, entries []commandEntry) (string, error) { word = strings.ToLower(word) var matches []string for _, e := range entries { if strings.ToLower(e.name) == word { return e.name, nil // exact match } if strings.HasPrefix(strings.ToLower(e.name), word) { matches = append(matches, e.name) } } switch len(matches) { case 0: return "", fmt.Errorf("unknown") case 1: return matches[0], nil default: return "", fmt.Errorf("ambiguous") } } // resolveCommand resolves a sequence of abbreviated words into the canonical // command path (e.g., ["sh", "run"] → ["show", "running-config"]). // It returns the resolved path, any remaining arguments, and an error if // resolution fails. func resolveCommand(words []string, entries []commandEntry) ([]string, []string, error) { var resolved []string current := entries for i, w := range words { name, err := resolveAbbreviation(w, current) if err != nil { if err.Error() == "unknown" && len(resolved) > 0 { // Remaining words are arguments to the resolved command. return resolved, words[i:], nil } return resolved, words[i:], err } resolved = append(resolved, name) // Find sub-commands for the matched entry. var nextLevel []commandEntry for _, e := range current { if e.name == name { nextLevel = e.subs break } } if nextLevel == nil { // Leaf command — rest are arguments. return resolved, words[i+1:], nil } current = nextLevel } return resolved, nil, nil } // dispatch processes a command line in the context of the current IOS state. func (s *iosState) dispatch(input string) commandResult { words := strings.Fields(input) if len(words) == 0 { return commandResult{} } // Handle "?" as a help request. if words[0] == "?" { return s.cmdHelp() } cmds := commandsForMode(s.mode) resolved, args, err := resolveCommand(words, cmds) if err != nil { if err.Error() == "ambiguous" { return commandResult{output: fmt.Sprintf("%% Ambiguous command: \"%s\"", input)} } return commandResult{output: invalidInput(input)} } if len(resolved) == 0 { return commandResult{output: invalidInput(input)} } cmd := strings.Join(resolved, " ") switch s.mode { case modeUserExec: return s.dispatchUserExec(cmd, args) case modePrivilegedExec: return s.dispatchPrivilegedExec(cmd, args) case modeGlobalConfig: return s.dispatchGlobalConfig(cmd, args) case modeInterfaceConfig: return s.dispatchInterfaceConfig(cmd, args) } return commandResult{output: invalidInput(input)} } func (s *iosState) dispatchUserExec(cmd string, args []string) commandResult { switch cmd { case "show version": return commandResult{output: showVersion(s)} case "show clock": return commandResult{output: showClock()} case "show ip route": return commandResult{output: showIPRoute(s)} case "show ip interface brief": return commandResult{output: showIPInterfaceBrief(s)} case "show interfaces": return commandResult{output: showInterfaces(s)} case "show vlan brief": return commandResult{output: showVLANBrief()} case "enable": return commandResult{} // handled in Handle() loop case "exit": return commandResult{exit: true} } return commandResult{output: invalidInput(cmd)} } func (s *iosState) dispatchPrivilegedExec(cmd string, args []string) commandResult { switch cmd { case "show version": return commandResult{output: showVersion(s)} case "show clock": return commandResult{output: showClock()} case "show ip route": return commandResult{output: showIPRoute(s)} case "show ip interface brief": return commandResult{output: showIPInterfaceBrief(s)} case "show interfaces": return commandResult{output: showInterfaces(s)} case "show running-config": return commandResult{output: showRunningConfig(s)} case "show startup-config": return commandResult{output: showRunningConfig(s)} // same as running case "show vlan brief": return commandResult{output: showVLANBrief()} case "configure terminal": s.mode = modeGlobalConfig return commandResult{output: "Enter configuration commands, one per line. End with CNTL/Z."} case "write memory": return commandResult{output: "[OK]"} case "copy": return commandResult{output: "[OK]"} case "reload": return commandResult{output: "System configuration has been modified. Save? [yes/no]: ", exit: true} case "disable": s.mode = modeUserExec return commandResult{} case "terminal length": return commandResult{} // accept silently case "exit": return commandResult{exit: true} } return commandResult{output: invalidInput(cmd)} } func (s *iosState) dispatchGlobalConfig(cmd string, args []string) commandResult { switch cmd { case "hostname": if len(args) < 1 { return commandResult{output: "% Incomplete command."} } s.hostname = args[0] return commandResult{} case "interface": if len(args) < 1 { return commandResult{output: "% Incomplete command."} } ifName := strings.Join(args, "") s.currentIf = ifName s.mode = modeInterfaceConfig return commandResult{} case "ip route": return commandResult{} // accept silently case "no": return commandResult{} // accept silently case "end": s.mode = modePrivilegedExec return commandResult{} case "exit": s.mode = modePrivilegedExec return commandResult{} } return commandResult{output: invalidInput(cmd)} } func (s *iosState) dispatchInterfaceConfig(cmd string, args []string) commandResult { switch cmd { case "ip address": if len(args) < 2 { return commandResult{output: "% Incomplete command."} } if iface := s.findInterface(s.currentIf); iface != nil { iface.ip = args[0] iface.mask = args[1] } return commandResult{} case "description": if len(args) < 1 { return commandResult{output: "% Incomplete command."} } if iface := s.findInterface(s.currentIf); iface != nil { iface.desc = strings.Join(args, " ") } return commandResult{} case "shutdown": if iface := s.findInterface(s.currentIf); iface != nil { iface.shutdown = true iface.status = "administratively down" iface.protocol = "down" } return commandResult{} case "no shutdown": if iface := s.findInterface(s.currentIf); iface != nil { iface.shutdown = false iface.status = "up" iface.protocol = "up" } return commandResult{} case "switchport mode": return commandResult{} // accept silently case "end": s.mode = modePrivilegedExec s.currentIf = "" return commandResult{} case "exit": s.mode = modeGlobalConfig s.currentIf = "" return commandResult{} } return commandResult{output: invalidInput(cmd)} } func (s *iosState) cmdHelp() commandResult { cmds := commandsForMode(s.mode) var b strings.Builder for _, e := range cmds { if e.name == "?" { continue } b.WriteString(fmt.Sprintf(" %-20s %s\n", e.name, helpText(e.name))) } return commandResult{output: b.String()} } func helpText(name string) string { switch name { case "show": return "Show running system information" case "enable": return "Turn on privileged commands" case "disable": return "Turn off privileged commands" case "exit": return "Exit from the EXEC" case "configure": return "Enter configuration mode" case "write": return "Write running configuration to memory" case "copy": return "Copy from one file to another" case "reload": return "Halt and perform a cold restart" case "terminal": return "Set terminal line parameters" case "hostname": return "Set system's network name" case "interface": return "Select an interface to configure" case "ip": return "Global IP configuration subcommands" case "no": return "Negate a command or set its defaults" case "end": return "Exit from configure mode" case "description": return "Interface specific description" case "shutdown": return "Shutdown the selected interface" case "switchport": return "Set switching mode characteristics" default: return "" } } func invalidInput(input string) string { return fmt.Sprintf("%% Invalid input detected at '^' marker.\n\n%s\n^", input) }