completer.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808
  1. package cli
  2. import (
  3. "sort"
  4. "strings"
  5. "sync"
  6. "time"
  7. )
  8. // TerminalWriter is an interface satisfied by *terminal.Terminal and mock terminals.
  9. type TerminalWriter interface {
  10. Write(buf []byte) (n int, err error)
  11. }
  12. // Node represents a command or subsection in the CLI hierarchy.
  13. type Node struct {
  14. Name string
  15. Aliases []string
  16. Description string
  17. Children []*Node
  18. Dynamic func() []string
  19. }
  20. // AddChild appends a child node to this node and returns the child.
  21. func (n *Node) AddChild(child *Node) *Node {
  22. n.Children = append(n.Children, child)
  23. return child
  24. }
  25. // FindChild searches for a child matching token by Name, Aliases, or unique prefix.
  26. func (n *Node) FindChild(token string) *Node {
  27. token = strings.ToLower(token)
  28. // 1. Exact Name match
  29. for _, ch := range n.Children {
  30. if strings.ToLower(ch.Name) == token {
  31. return ch
  32. }
  33. }
  34. // 2. Exact Aliases match
  35. for _, ch := range n.Children {
  36. for _, a := range ch.Aliases {
  37. if strings.ToLower(a) == token {
  38. return ch
  39. }
  40. }
  41. }
  42. // 3. Unique prefix match among children (excluding parameters ending in '=')
  43. var match *Node
  44. for _, ch := range n.Children {
  45. if !strings.HasSuffix(ch.Name, "=") && strings.HasPrefix(strings.ToLower(ch.Name), token) {
  46. if match != nil {
  47. return nil // Ambiguous
  48. }
  49. match = ch
  50. }
  51. }
  52. return match
  53. }
  54. // Completer manages tab-completion state and double-tab candidate listing.
  55. type Completer struct {
  56. term TerminalWriter
  57. executor *Executor
  58. root *Node
  59. termWidth int
  60. mu sync.Mutex
  61. lastTabTime time.Time
  62. lastTabLine string
  63. lastTabPos int
  64. }
  65. // NewCompleter creates and initializes a Completer with the full command tree.
  66. func NewCompleter(term TerminalWriter, executor *Executor) *Completer {
  67. c := &Completer{
  68. term: term,
  69. executor: executor,
  70. termWidth: 80,
  71. }
  72. c.root = c.buildCommandTree()
  73. return c
  74. }
  75. // SetWidth sets the terminal width for candidate column formatting.
  76. func (c *Completer) SetWidth(w int) {
  77. c.mu.Lock()
  78. defer c.mu.Unlock()
  79. if w > 20 {
  80. c.termWidth = w
  81. }
  82. }
  83. // Callback is the AutoCompleteCallback for golang.org/x/term.Terminal.
  84. func (c *Completer) Callback(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
  85. if key != '\t' {
  86. return "", 0, false
  87. }
  88. c.mu.Lock()
  89. defer c.mu.Unlock()
  90. candidates, prefixBeforeWord, currentWord, isParamVal, paramPrefix := c.getCompletions(line, pos)
  91. if len(candidates) == 0 {
  92. // Nothing to complete
  93. c.lastTabTime = time.Time{}
  94. c.lastTabLine = ""
  95. c.lastTabPos = 0
  96. return "", 0, false
  97. }
  98. suffix := ""
  99. if pos < len(line) {
  100. suffix = line[pos:]
  101. }
  102. // Single match: complete it immediately!
  103. if len(candidates) == 1 {
  104. c.lastTabTime = time.Time{}
  105. c.lastTabLine = ""
  106. c.lastTabPos = 0
  107. match := candidates[0]
  108. completedToken := match
  109. if isParamVal {
  110. completedToken = paramPrefix + match + " "
  111. } else if strings.HasSuffix(match, "=") {
  112. // Parameter key, e.g. "address=" -> no trailing space
  113. completedToken = match
  114. } else {
  115. completedToken = match + " "
  116. }
  117. newLine = prefixBeforeWord + completedToken + suffix
  118. newPos = len(prefixBeforeWord) + len(completedToken)
  119. return newLine, newPos, true
  120. }
  121. // Multiple candidates: find Longest Common Prefix (LCP)
  122. lcp := longestCommonPrefix(candidates)
  123. targetWord := currentWord
  124. if isParamVal {
  125. targetWord = currentWord[len(paramPrefix):]
  126. }
  127. // If LCP can extend the current input, expand it
  128. if len(lcp) > len(targetWord) {
  129. extendedToken := lcp
  130. if isParamVal {
  131. extendedToken = paramPrefix + lcp
  132. }
  133. newLine = prefixBeforeWord + extendedToken + suffix
  134. newPos = len(prefixBeforeWord) + len(extendedToken)
  135. // Record tab state so an immediate second tab shows remaining options
  136. c.lastTabTime = time.Now()
  137. c.lastTabLine = newLine
  138. c.lastTabPos = newPos
  139. return newLine, newPos, true
  140. }
  141. // LCP cannot extend input further: check for 2nd Tab (within 1000ms)
  142. isSecondTab := time.Since(c.lastTabTime) <= 1000*time.Millisecond &&
  143. c.lastTabLine == line &&
  144. c.lastTabPos == pos
  145. if !isSecondTab {
  146. // 1st Tab: Record state and wait for potential 2nd Tab
  147. c.lastTabTime = time.Now()
  148. c.lastTabLine = line
  149. c.lastTabPos = pos
  150. return line, pos, true
  151. }
  152. // 2nd Tab: DISPLAY ALL CANDIDATES!
  153. c.lastTabTime = time.Time{}
  154. c.lastTabLine = ""
  155. c.lastTabPos = 0
  156. if c.term != nil {
  157. formatted := c.formatCandidates(candidates)
  158. _, _ = c.term.Write([]byte(formatted))
  159. }
  160. return line, pos, true
  161. }
  162. // getCompletions computes the candidate completions for the current cursor context.
  163. func (c *Completer) getCompletions(line string, pos int) (candidates []string, prefixBeforeWord string, currentWord string, isParamVal bool, paramPrefix string) {
  164. if pos > len(line) {
  165. pos = len(line)
  166. }
  167. input := line[:pos]
  168. lastSpace := strings.LastIndexAny(input, " \t")
  169. if lastSpace == -1 {
  170. // Single word so far (or empty)
  171. if strings.HasPrefix(input, "/") {
  172. prefixBeforeWord = "/"
  173. currentWord = input[1:]
  174. } else {
  175. prefixBeforeWord = ""
  176. currentWord = input
  177. }
  178. } else {
  179. prefixBeforeWord = input[:lastSpace+1]
  180. currentWord = input[lastSpace+1:]
  181. }
  182. // Parse preceding words to navigate tree
  183. rawPrefix := strings.TrimSpace(prefixBeforeWord)
  184. hasSlash := strings.HasPrefix(rawPrefix, "/")
  185. if hasSlash {
  186. rawPrefix = strings.TrimPrefix(rawPrefix, "/")
  187. }
  188. var precedingTokens []string
  189. if rawPrefix != "" {
  190. precedingTokens = strings.Fields(rawPrefix)
  191. }
  192. // Traverse tree to find current node and track used parameter keys
  193. currNode := c.root
  194. usedParams := make(map[string]bool)
  195. for _, token := range precedingTokens {
  196. if idx := strings.Index(token, "="); idx != -1 {
  197. // Parameter key=value, stay at current node
  198. key := strings.ToLower(token[:idx+1])
  199. usedParams[key] = true
  200. continue
  201. }
  202. next := currNode.FindChild(token)
  203. if next != nil {
  204. currNode = next
  205. }
  206. }
  207. // Check if user is typing a parameter value (e.g. "interface=eth" or "role=")
  208. if idx := strings.Index(currentWord, "="); idx != -1 {
  209. paramKey := strings.ToLower(currentWord[:idx+1])
  210. paramVal := strings.ToLower(currentWord[idx+1:])
  211. paramChild := currNode.FindChild(paramKey)
  212. if paramChild != nil && paramChild.Dynamic != nil {
  213. isParamVal = true
  214. paramPrefix = currentWord[:idx+1]
  215. for _, val := range paramChild.Dynamic() {
  216. if strings.HasPrefix(strings.ToLower(val), paramVal) {
  217. candidates = append(candidates, val)
  218. }
  219. }
  220. sort.Strings(candidates)
  221. return
  222. }
  223. }
  224. // Gather child commands and parameters
  225. search := strings.ToLower(currentWord)
  226. for _, ch := range currNode.Children {
  227. nameLower := strings.ToLower(ch.Name)
  228. if strings.HasSuffix(nameLower, "=") {
  229. if usedParams[nameLower] {
  230. continue // parameter already provided
  231. }
  232. }
  233. if strings.HasPrefix(nameLower, search) {
  234. candidates = append(candidates, ch.Name)
  235. }
  236. }
  237. // Check if current node has dynamic positional choices (e.g. interface names or service names)
  238. if currNode.Dynamic != nil {
  239. for _, dyn := range currNode.Dynamic() {
  240. if strings.HasPrefix(strings.ToLower(dyn), search) {
  241. candidates = append(candidates, dyn)
  242. }
  243. }
  244. }
  245. // Deduplicate and sort
  246. candidates = uniqueStrings(candidates)
  247. sort.Strings(candidates)
  248. return
  249. }
  250. func (c *Completer) formatCandidates(candidates []string) string {
  251. if len(candidates) == 0 {
  252. return ""
  253. }
  254. termWidth := c.termWidth
  255. if termWidth < 40 {
  256. termWidth = 80
  257. }
  258. maxLen := 0
  259. for _, cand := range candidates {
  260. if len(cand) > maxLen {
  261. maxLen = len(cand)
  262. }
  263. }
  264. colWidth := maxLen + 3
  265. if colWidth < 10 {
  266. colWidth = 10
  267. }
  268. numCols := (termWidth - 4) / colWidth
  269. if numCols < 1 {
  270. numCols = 1
  271. }
  272. if numCols > len(candidates) {
  273. numCols = len(candidates)
  274. }
  275. numRows := (len(candidates) + numCols - 1) / numCols
  276. var sb strings.Builder
  277. sb.WriteString("\n")
  278. for r := 0; r < numRows; r++ {
  279. sb.WriteString(" ")
  280. for col := 0; col < numCols; col++ {
  281. idx := col*numRows + r
  282. if idx < len(candidates) {
  283. cand := candidates[idx]
  284. colored := colorizeCandidate(cand)
  285. sb.WriteString(colored)
  286. if col < numCols-1 && (col+1)*numRows+r < len(candidates) {
  287. pad := colWidth - len(cand)
  288. if pad > 0 {
  289. sb.WriteString(strings.Repeat(" ", pad))
  290. }
  291. }
  292. }
  293. }
  294. sb.WriteString("\n")
  295. }
  296. return sb.String()
  297. }
  298. func colorizeCandidate(s string) string {
  299. if strings.HasSuffix(s, "=") {
  300. return ColorYellow + s + ColorReset
  301. }
  302. switch s {
  303. case "print", "add", "remove", "enable", "disable", "set", "reboot", "flush", "renew", "release", "connect", "disconnect":
  304. return ColorGreen + s + ColorReset
  305. case "interface", "ip", "routing", "tool", "log", "user", "system",
  306. "vlan", "bridge", "wireguard", "eoip", "vrrp", "pppoe-client",
  307. "address", "route", "dhcp-client", "dhcp-server", "dns", "firewall", "service",
  308. "filter", "nat", "mangle", "address-list", "bgp", "ospf", "port", "peers", "lease", "network", "cache", "static", "rule", "table", "instance", "area", "neighbor", "resource", "identity":
  309. return ColorCyan + s + ColorReset
  310. case "www", "ssh", "telnet", "web":
  311. return ColorMagenta + s + ColorReset
  312. default:
  313. return ColorWhite + s + ColorReset
  314. }
  315. }
  316. func longestCommonPrefix(strs []string) string {
  317. if len(strs) == 0 {
  318. return ""
  319. }
  320. prefix := strs[0]
  321. for _, s := range strs[1:] {
  322. for !strings.HasPrefix(strings.ToLower(s), strings.ToLower(prefix)) {
  323. if len(prefix) == 0 {
  324. return ""
  325. }
  326. prefix = prefix[:len(prefix)-1]
  327. }
  328. }
  329. return prefix
  330. }
  331. func uniqueStrings(slice []string) []string {
  332. seen := make(map[string]bool)
  333. var result []string
  334. for _, s := range slice {
  335. if !seen[s] {
  336. seen[s] = true
  337. result = append(result, s)
  338. }
  339. }
  340. return result
  341. }
  342. // buildCommandTree creates the full tree of commands, subsections, actions, parameters, and dynamic providers.
  343. func (c *Completer) buildCommandTree() *Node {
  344. root := &Node{Name: "root"}
  345. // Helper for dynamic interfaces
  346. dynInterfaces := func() []string {
  347. if c.executor != nil && c.executor.netMgr != nil {
  348. ifaces, err := c.executor.netMgr.ListInterfaces()
  349. if err == nil {
  350. var names []string
  351. for _, iface := range ifaces {
  352. names = append(names, iface.Name)
  353. }
  354. return names
  355. }
  356. }
  357. return []string{"eth0", "eth1", "lo"}
  358. }
  359. // Helper for services
  360. dynServices := func() []string {
  361. if c.executor != nil && c.executor.servicesMgr != nil {
  362. var names []string
  363. for _, s := range c.executor.servicesMgr.ListServices() {
  364. names = append(names, s.Name)
  365. }
  366. return names
  367. }
  368. return []string{"www", "telnet", "ssh"}
  369. }
  370. // Common standard actions
  371. addPrint := func(n *Node) {
  372. n.AddChild(&Node{Name: "print", Aliases: []string{"pr", "show", "list"}})
  373. }
  374. addRemove := func(n *Node) {
  375. n.AddChild(&Node{Name: "remove", Aliases: []string{"rem", "delete"}})
  376. }
  377. addEnableDisable := func(n *Node) {
  378. n.AddChild(&Node{Name: "enable"})
  379. n.AddChild(&Node{Name: "disable"})
  380. }
  381. // 1. /interface
  382. ifaceNode := root.AddChild(&Node{Name: "interface", Aliases: []string{"interfaces"}})
  383. addPrint(ifaceNode)
  384. ifaceNode.AddChild(&Node{Name: "enable", Dynamic: dynInterfaces})
  385. ifaceNode.AddChild(&Node{Name: "disable", Dynamic: dynInterfaces})
  386. ifaceSet := ifaceNode.AddChild(&Node{Name: "set", Dynamic: dynInterfaces})
  387. ifaceSet.AddChild(&Node{Name: "mtu="})
  388. // /interface vlan
  389. vlanNode := ifaceNode.AddChild(&Node{Name: "vlan", Aliases: []string{"vlans"}})
  390. addPrint(vlanNode)
  391. addRemove(vlanNode)
  392. vlanAdd := vlanNode.AddChild(&Node{Name: "add"})
  393. vlanAdd.AddChild(&Node{Name: "name="})
  394. vlanAdd.AddChild(&Node{Name: "vlan-id="})
  395. vlanAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
  396. // /interface bridge
  397. bridgeNode := ifaceNode.AddChild(&Node{Name: "bridge", Aliases: []string{"bridges"}})
  398. addPrint(bridgeNode)
  399. addRemove(bridgeNode)
  400. addEnableDisable(bridgeNode)
  401. bridgeAdd := bridgeNode.AddChild(&Node{Name: "add"})
  402. bridgeAdd.AddChild(&Node{Name: "name="})
  403. // /interface bridge port
  404. bridgePortNode := bridgeNode.AddChild(&Node{Name: "port", Aliases: []string{"ports"}})
  405. addPrint(bridgePortNode)
  406. addRemove(bridgePortNode)
  407. addEnableDisable(bridgePortNode)
  408. bridgePortAdd := bridgePortNode.AddChild(&Node{Name: "add"})
  409. bridgePortAdd.AddChild(&Node{Name: "bridge="})
  410. bridgePortAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
  411. bridgePortAdd.AddChild(&Node{Name: "pvid="})
  412. // /interface wireguard
  413. wgNode := ifaceNode.AddChild(&Node{Name: "wireguard", Aliases: []string{"wg"}})
  414. addPrint(wgNode)
  415. addRemove(wgNode)
  416. addEnableDisable(wgNode)
  417. wgAdd := wgNode.AddChild(&Node{Name: "add"})
  418. wgAdd.AddChild(&Node{Name: "name="})
  419. wgAdd.AddChild(&Node{Name: "listen-port="})
  420. wgAdd.AddChild(&Node{Name: "private-key="})
  421. // /interface wireguard peers
  422. wgPeers := wgNode.AddChild(&Node{Name: "peers", Aliases: []string{"peer"}})
  423. addPrint(wgPeers)
  424. addRemove(wgPeers)
  425. wgPeersAdd := wgPeers.AddChild(&Node{Name: "add"})
  426. wgPeersAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
  427. wgPeersAdd.AddChild(&Node{Name: "public-key="})
  428. wgPeersAdd.AddChild(&Node{Name: "allowed-address="})
  429. wgPeersAdd.AddChild(&Node{Name: "endpoint-address="})
  430. wgPeersAdd.AddChild(&Node{Name: "endpoint-port="})
  431. // /interface eoip
  432. eoipNode := ifaceNode.AddChild(&Node{Name: "eoip"})
  433. addPrint(eoipNode)
  434. addRemove(eoipNode)
  435. addEnableDisable(eoipNode)
  436. eoipAdd := eoipNode.AddChild(&Node{Name: "add"})
  437. eoipAdd.AddChild(&Node{Name: "name="})
  438. eoipAdd.AddChild(&Node{Name: "remote-ip="})
  439. eoipAdd.AddChild(&Node{Name: "tunnel-id="})
  440. // /interface vrrp
  441. vrrpNode := ifaceNode.AddChild(&Node{Name: "vrrp"})
  442. addPrint(vrrpNode)
  443. addRemove(vrrpNode)
  444. addEnableDisable(vrrpNode)
  445. vrrpAdd := vrrpNode.AddChild(&Node{Name: "add"})
  446. vrrpAdd.AddChild(&Node{Name: "name="})
  447. vrrpAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
  448. vrrpAdd.AddChild(&Node{Name: "vrid="})
  449. vrrpAdd.AddChild(&Node{Name: "priority="})
  450. // /interface pppoe-client
  451. pppoeNode := ifaceNode.AddChild(&Node{Name: "pppoe-client"})
  452. addPrint(pppoeNode)
  453. addRemove(pppoeNode)
  454. pppoeNode.AddChild(&Node{Name: "connect"})
  455. pppoeNode.AddChild(&Node{Name: "disconnect"})
  456. pppoeAdd := pppoeNode.AddChild(&Node{Name: "add"})
  457. pppoeAdd.AddChild(&Node{Name: "name="})
  458. pppoeAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
  459. pppoeAdd.AddChild(&Node{Name: "user="})
  460. pppoeAdd.AddChild(&Node{Name: "password="})
  461. // 2. /ip
  462. ipNode := root.AddChild(&Node{Name: "ip"})
  463. // /ip address
  464. ipAddrNode := ipNode.AddChild(&Node{Name: "address", Aliases: []string{"addr"}})
  465. addPrint(ipAddrNode)
  466. addRemove(ipAddrNode)
  467. ipAddrAdd := ipAddrNode.AddChild(&Node{Name: "add"})
  468. ipAddrAdd.AddChild(&Node{Name: "address="})
  469. ipAddrAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
  470. ipAddrAdd.AddChild(&Node{Name: "network="})
  471. // /ip route
  472. ipRouteNode := ipNode.AddChild(&Node{Name: "route", Aliases: []string{"routes"}})
  473. addPrint(ipRouteNode)
  474. addRemove(ipRouteNode)
  475. addEnableDisable(ipRouteNode)
  476. ipRouteAdd := ipRouteNode.AddChild(&Node{Name: "add"})
  477. ipRouteAdd.AddChild(&Node{Name: "dst-address="})
  478. ipRouteAdd.AddChild(&Node{Name: "gateway="})
  479. ipRouteAdd.AddChild(&Node{Name: "distance="})
  480. // /ip route rule
  481. ipRouteRule := ipRouteNode.AddChild(&Node{Name: "rule", Aliases: []string{"rules"}})
  482. addPrint(ipRouteRule)
  483. addRemove(ipRouteRule)
  484. ipRouteRuleAdd := ipRouteRule.AddChild(&Node{Name: "add"})
  485. ipRouteRuleAdd.AddChild(&Node{Name: "src-address="})
  486. ipRouteRuleAdd.AddChild(&Node{Name: "dst-address="})
  487. ipRouteRuleAdd.AddChild(&Node{Name: "table="})
  488. ipRouteRuleAdd.AddChild(&Node{Name: "action=", Dynamic: func() []string { return []string{"lookup", "unreachable", "drop"} }})
  489. // /ip route table
  490. ipRouteTable := ipRouteNode.AddChild(&Node{Name: "table", Aliases: []string{"tables"}})
  491. addPrint(ipRouteTable)
  492. addRemove(ipRouteTable)
  493. ipRouteTableAdd := ipRouteTable.AddChild(&Node{Name: "add"})
  494. ipRouteTableAdd.AddChild(&Node{Name: "name="})
  495. ipRouteTableAdd.AddChild(&Node{Name: "fib="})
  496. // /ip dhcp-client
  497. dhcpCliNode := ipNode.AddChild(&Node{Name: "dhcp-client"})
  498. addPrint(dhcpCliNode)
  499. addRemove(dhcpCliNode)
  500. dhcpCliNode.AddChild(&Node{Name: "renew"})
  501. dhcpCliNode.AddChild(&Node{Name: "release"})
  502. dhcpCliAdd := dhcpCliNode.AddChild(&Node{Name: "add"})
  503. dhcpCliAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
  504. dhcpCliAdd.AddChild(&Node{Name: "disabled=", Dynamic: func() []string { return []string{"yes", "no"} }})
  505. // /ip dhcp-server
  506. dhcpSrvNode := ipNode.AddChild(&Node{Name: "dhcp-server"})
  507. addPrint(dhcpSrvNode)
  508. addRemove(dhcpSrvNode)
  509. addEnableDisable(dhcpSrvNode)
  510. dhcpSrvAdd := dhcpSrvNode.AddChild(&Node{Name: "add"})
  511. dhcpSrvAdd.AddChild(&Node{Name: "name="})
  512. dhcpSrvAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
  513. dhcpSrvAdd.AddChild(&Node{Name: "address-pool="})
  514. dhcpSrvAdd.AddChild(&Node{Name: "lease-time="})
  515. // /ip dhcp-server lease
  516. dhcpLeaseNode := dhcpSrvNode.AddChild(&Node{Name: "lease", Aliases: []string{"leases"}})
  517. addPrint(dhcpLeaseNode)
  518. addRemove(dhcpLeaseNode)
  519. // /ip dhcp-server network
  520. dhcpNetNode := dhcpSrvNode.AddChild(&Node{Name: "network", Aliases: []string{"networks"}})
  521. addPrint(dhcpNetNode)
  522. addRemove(dhcpNetNode)
  523. dhcpNetAdd := dhcpNetNode.AddChild(&Node{Name: "add"})
  524. dhcpNetAdd.AddChild(&Node{Name: "address="})
  525. dhcpNetAdd.AddChild(&Node{Name: "gateway="})
  526. dhcpNetAdd.AddChild(&Node{Name: "dns-server="})
  527. // /ip dns
  528. dnsNode := ipNode.AddChild(&Node{Name: "dns"})
  529. addPrint(dnsNode)
  530. dnsSet := dnsNode.AddChild(&Node{Name: "set"})
  531. dnsSet.AddChild(&Node{Name: "servers="})
  532. dnsSet.AddChild(&Node{Name: "allow-remote-requests=", Dynamic: func() []string { return []string{"yes", "no"} }})
  533. dnsCache := dnsNode.AddChild(&Node{Name: "cache"})
  534. addPrint(dnsCache)
  535. dnsCache.AddChild(&Node{Name: "flush"})
  536. dnsStatic := dnsNode.AddChild(&Node{Name: "static"})
  537. addPrint(dnsStatic)
  538. addRemove(dnsStatic)
  539. dnsStaticAdd := dnsStatic.AddChild(&Node{Name: "add"})
  540. dnsStaticAdd.AddChild(&Node{Name: "name="})
  541. dnsStaticAdd.AddChild(&Node{Name: "address="})
  542. // /ip firewall
  543. fwNode := ipNode.AddChild(&Node{Name: "firewall"})
  544. // /ip firewall filter
  545. fwFilter := fwNode.AddChild(&Node{Name: "filter"})
  546. addPrint(fwFilter)
  547. addRemove(fwFilter)
  548. addEnableDisable(fwFilter)
  549. fwFilterAdd := fwFilter.AddChild(&Node{Name: "add"})
  550. fwFilterAdd.AddChild(&Node{Name: "chain=", Dynamic: func() []string { return []string{"forward", "input", "output"} }})
  551. fwFilterAdd.AddChild(&Node{Name: "action=", Dynamic: func() []string { return []string{"accept", "drop", "reject"} }})
  552. fwFilterAdd.AddChild(&Node{Name: "protocol=", Dynamic: func() []string { return []string{"icmp", "tcp", "udp"} }})
  553. fwFilterAdd.AddChild(&Node{Name: "src-address="})
  554. fwFilterAdd.AddChild(&Node{Name: "dst-address="})
  555. fwFilterAdd.AddChild(&Node{Name: "dst-port="})
  556. fwFilterAdd.AddChild(&Node{Name: "in-interface=", Dynamic: dynInterfaces})
  557. fwFilterAdd.AddChild(&Node{Name: "out-interface=", Dynamic: dynInterfaces})
  558. fwFilterAdd.AddChild(&Node{Name: "comment="})
  559. // /ip firewall nat
  560. fwNat := fwNode.AddChild(&Node{Name: "nat"})
  561. addPrint(fwNat)
  562. addRemove(fwNat)
  563. addEnableDisable(fwNat)
  564. fwNatAdd := fwNat.AddChild(&Node{Name: "add"})
  565. fwNatAdd.AddChild(&Node{Name: "chain=", Dynamic: func() []string { return []string{"dstnat", "srcnat"} }})
  566. fwNatAdd.AddChild(&Node{Name: "action=", Dynamic: func() []string { return []string{"accept", "dst-nat", "masquerade", "redirect", "src-nat"} }})
  567. fwNatAdd.AddChild(&Node{Name: "protocol=", Dynamic: func() []string { return []string{"icmp", "tcp", "udp"} }})
  568. fwNatAdd.AddChild(&Node{Name: "src-address="})
  569. fwNatAdd.AddChild(&Node{Name: "dst-address="})
  570. fwNatAdd.AddChild(&Node{Name: "to-addresses="})
  571. fwNatAdd.AddChild(&Node{Name: "to-ports="})
  572. fwNatAdd.AddChild(&Node{Name: "out-interface=", Dynamic: dynInterfaces})
  573. fwNatAdd.AddChild(&Node{Name: "comment="})
  574. // /ip firewall mangle
  575. fwMangle := fwNode.AddChild(&Node{Name: "mangle"})
  576. addPrint(fwMangle)
  577. addRemove(fwMangle)
  578. addEnableDisable(fwMangle)
  579. fwMangleAdd := fwMangle.AddChild(&Node{Name: "add"})
  580. fwMangleAdd.AddChild(&Node{Name: "chain=", Dynamic: func() []string { return []string{"forward", "input", "output", "postrouting", "prerouting"} }})
  581. fwMangleAdd.AddChild(&Node{Name: "action=", Dynamic: func() []string { return []string{"accept", "mark-packet", "mark-routing"} }})
  582. fwMangleAdd.AddChild(&Node{Name: "protocol=", Dynamic: func() []string { return []string{"icmp", "tcp", "udp"} }})
  583. fwMangleAdd.AddChild(&Node{Name: "src-address="})
  584. fwMangleAdd.AddChild(&Node{Name: "dst-address="})
  585. // /ip firewall address-list
  586. fwAddrList := fwNode.AddChild(&Node{Name: "address-list"})
  587. addPrint(fwAddrList)
  588. addRemove(fwAddrList)
  589. addEnableDisable(fwAddrList)
  590. fwAddrListAdd := fwAddrList.AddChild(&Node{Name: "add"})
  591. fwAddrListAdd.AddChild(&Node{Name: "list="})
  592. fwAddrListAdd.AddChild(&Node{Name: "address="})
  593. fwAddrListAdd.AddChild(&Node{Name: "comment="})
  594. // /ip service
  595. serviceNode := ipNode.AddChild(&Node{Name: "service", Aliases: []string{"services"}})
  596. addPrint(serviceNode)
  597. serviceNode.AddChild(&Node{Name: "enable", Dynamic: dynServices})
  598. serviceNode.AddChild(&Node{Name: "disable", Dynamic: dynServices})
  599. serviceSet := serviceNode.AddChild(&Node{Name: "set", Dynamic: dynServices})
  600. serviceSet.AddChild(&Node{Name: "port="})
  601. serviceSet.AddChild(&Node{Name: "disabled=", Dynamic: func() []string { return []string{"yes", "no"} }})
  602. // 3. /routing
  603. routingNode := root.AddChild(&Node{Name: "routing"})
  604. // /routing bgp
  605. bgpNode := routingNode.AddChild(&Node{Name: "bgp"})
  606. bgpInst := bgpNode.AddChild(&Node{Name: "instance"})
  607. addPrint(bgpInst)
  608. bgpInstSet := bgpInst.AddChild(&Node{Name: "set"})
  609. bgpInstSet.AddChild(&Node{Name: "as="})
  610. bgpInstSet.AddChild(&Node{Name: "router-id="})
  611. bgpInstSet.AddChild(&Node{Name: "enabled=", Dynamic: func() []string { return []string{"yes", "no"} }})
  612. bgpPeer := bgpNode.AddChild(&Node{Name: "peer", Aliases: []string{"peers", "connection"}})
  613. addPrint(bgpPeer)
  614. addRemove(bgpPeer)
  615. addEnableDisable(bgpPeer)
  616. bgpPeerAdd := bgpPeer.AddChild(&Node{Name: "add"})
  617. bgpPeerAdd.AddChild(&Node{Name: "name="})
  618. bgpPeerAdd.AddChild(&Node{Name: "remote-address="})
  619. bgpPeerAdd.AddChild(&Node{Name: "remote-as="})
  620. bgpPeerAdd.AddChild(&Node{Name: "description="})
  621. bgpNet := bgpNode.AddChild(&Node{Name: "network"})
  622. addPrint(bgpNet)
  623. addRemove(bgpNet)
  624. bgpNetAdd := bgpNet.AddChild(&Node{Name: "add"})
  625. bgpNetAdd.AddChild(&Node{Name: "network="})
  626. bgpRoutes := bgpNode.AddChild(&Node{Name: "routes"})
  627. addPrint(bgpRoutes)
  628. // /routing ospf
  629. ospfNode := routingNode.AddChild(&Node{Name: "ospf"})
  630. ospfInst := ospfNode.AddChild(&Node{Name: "instance"})
  631. addPrint(ospfInst)
  632. ospfInstSet := ospfInst.AddChild(&Node{Name: "set"})
  633. ospfInstSet.AddChild(&Node{Name: "router-id="})
  634. ospfInstSet.AddChild(&Node{Name: "enabled=", Dynamic: func() []string { return []string{"yes", "no"} }})
  635. ospfArea := ospfNode.AddChild(&Node{Name: "area"})
  636. addPrint(ospfArea)
  637. addRemove(ospfArea)
  638. ospfAreaAdd := ospfArea.AddChild(&Node{Name: "add"})
  639. ospfAreaAdd.AddChild(&Node{Name: "name="})
  640. ospfAreaAdd.AddChild(&Node{Name: "area-id="})
  641. ospfIface := ospfNode.AddChild(&Node{Name: "interface"})
  642. addPrint(ospfIface)
  643. addRemove(ospfIface)
  644. ospfIfaceAdd := ospfIface.AddChild(&Node{Name: "add"})
  645. ospfIfaceAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
  646. ospfIfaceAdd.AddChild(&Node{Name: "cost="})
  647. ospfIfaceAdd.AddChild(&Node{Name: "priority="})
  648. ospfIfaceAdd.AddChild(&Node{Name: "network-type=", Dynamic: func() []string { return []string{"broadcast", "point-to-point"} }})
  649. ospfNeighbor := ospfNode.AddChild(&Node{Name: "neighbor", Aliases: []string{"neighbors"}})
  650. addPrint(ospfNeighbor)
  651. // /routing filter
  652. routingFilter := routingNode.AddChild(&Node{Name: "filter"})
  653. addPrint(routingFilter)
  654. rfRule := routingFilter.AddChild(&Node{Name: "rule", Aliases: []string{"rules"}})
  655. addPrint(rfRule)
  656. addRemove(rfRule)
  657. rfRuleAdd := rfRule.AddChild(&Node{Name: "add"})
  658. rfRuleAdd.AddChild(&Node{Name: "chain="})
  659. rfRuleAdd.AddChild(&Node{Name: "action=", Dynamic: func() []string { return []string{"accept", "discard", "reject"} }})
  660. rfRuleAdd.AddChild(&Node{Name: "prefix="})
  661. rfRuleAdd.AddChild(&Node{Name: "prefix-len="})
  662. // 4. /tool
  663. toolNode := root.AddChild(&Node{Name: "tool"})
  664. toolPing := toolNode.AddChild(&Node{Name: "ping"})
  665. toolPing.AddChild(&Node{Name: "count="})
  666. toolPing.AddChild(&Node{Name: "size="})
  667. toolPing.AddChild(&Node{Name: "interval="})
  668. toolTrace := toolNode.AddChild(&Node{Name: "traceroute"})
  669. toolTrace.AddChild(&Node{Name: "max-hops="})
  670. toolBtest := toolNode.AddChild(&Node{Name: "bandwidth-test", Aliases: []string{"btest"}})
  671. toolBtest.AddChild(&Node{Name: "target="})
  672. toolBtest.AddChild(&Node{Name: "duration="})
  673. toolBtest.AddChild(&Node{Name: "direction=", Dynamic: func() []string { return []string{"receive", "send", "both"} }})
  674. // 5. /log
  675. logNode := root.AddChild(&Node{Name: "log"})
  676. logPrint := logNode.AddChild(&Node{Name: "print", Aliases: []string{"pr", "show"}})
  677. logPrint.AddChild(&Node{Name: "limit="})
  678. logPrint.AddChild(&Node{Name: "filter="})
  679. // 6. /user
  680. userNode := root.AddChild(&Node{Name: "user", Aliases: []string{"users"}})
  681. addPrint(userNode)
  682. addRemove(userNode)
  683. userAdd := userNode.AddChild(&Node{Name: "add"})
  684. userAdd.AddChild(&Node{Name: "name="})
  685. userAdd.AddChild(&Node{Name: "password="})
  686. userAdd.AddChild(&Node{Name: "role=", Dynamic: func() []string { return []string{"admin", "full", "read", "viewer"} }})
  687. userAdd.AddChild(&Node{Name: "comment="})
  688. userSet := userNode.AddChild(&Node{Name: "set"})
  689. userSet.AddChild(&Node{Name: "name="})
  690. userSet.AddChild(&Node{Name: "password="})
  691. userSet.AddChild(&Node{Name: "role=", Dynamic: func() []string { return []string{"admin", "full", "read", "viewer"} }})
  692. userSet.AddChild(&Node{Name: "comment="})
  693. userSet.AddChild(&Node{Name: "disabled=", Dynamic: func() []string { return []string{"yes", "no"} }})
  694. // 7. /system
  695. sysNode := root.AddChild(&Node{Name: "system"})
  696. sysRes := sysNode.AddChild(&Node{Name: "resource"})
  697. addPrint(sysRes)
  698. sysIdent := sysNode.AddChild(&Node{Name: "identity"})
  699. addPrint(sysIdent)
  700. sysIdentSet := sysIdent.AddChild(&Node{Name: "set"})
  701. sysIdentSet.AddChild(&Node{Name: "name="})
  702. sysNode.AddChild(&Node{Name: "reboot"})
  703. // 8. Direct root utility commands
  704. root.AddChild(&Node{Name: "clear"})
  705. root.AddChild(&Node{Name: "help", Aliases: []string{"?"}})
  706. root.AddChild(&Node{Name: "quit"})
  707. root.AddChild(&Node{Name: "exit"})
  708. root.AddChild(&Node{Name: "ping"})
  709. root.AddChild(&Node{Name: "traceroute"})
  710. return root
  711. }