]> git.lizzy.rs Git - micro.git/blob - cmd/micro/command.go
Merge
[micro.git] / cmd / micro / command.go
1 package main
2
3 import (
4         "bytes"
5         "fmt"
6         "os"
7         "os/exec"
8         "os/signal"
9         "path/filepath"
10         "regexp"
11         "runtime"
12         "strconv"
13         "strings"
14
15         humanize "github.com/dustin/go-humanize"
16         "github.com/zyedidia/micro/cmd/micro/shellwords"
17 )
18
19 // A Command contains a action (a function to call) as well as information about how to autocomplete the command
20 type Command struct {
21         action      func([]string)
22         completions []Completion
23 }
24
25 // A StrCommand is similar to a command but keeps the name of the action
26 type StrCommand struct {
27         action      string
28         completions []Completion
29 }
30
31 var commands map[string]Command
32
33 var commandActions map[string]func([]string)
34
35 func init() {
36         commandActions = map[string]func([]string){
37                 "Set":        Set,
38                 "SetLocal":   SetLocal,
39                 "Show":       Show,
40                 "ShowKey":    ShowKey,
41                 "Run":        Run,
42                 "Bind":       Bind,
43                 "Quit":       Quit,
44                 "Save":       Save,
45                 "Replace":    Replace,
46                 "ReplaceAll": ReplaceAll,
47                 "VSplit":     VSplit,
48                 "HSplit":     HSplit,
49                 "Tab":        NewTab,
50                 "Help":       Help,
51                 "Eval":       Eval,
52                 "ToggleLog":  ToggleLog,
53                 "Plugin":     PluginCmd,
54                 "Reload":     Reload,
55                 "Cd":         Cd,
56                 "Pwd":        Pwd,
57                 "Open":       Open,
58                 "TabSwitch":  TabSwitch,
59                 "MemUsage":   MemUsage,
60                 "Retab":      Retab,
61                 "Raw":        Raw,
62         }
63 }
64
65 // InitCommands initializes the default commands
66 func InitCommands() {
67         commands = make(map[string]Command)
68
69         defaults := DefaultCommands()
70         parseCommands(defaults)
71 }
72
73 func parseCommands(userCommands map[string]StrCommand) {
74         for k, v := range userCommands {
75                 MakeCommand(k, v.action, v.completions...)
76         }
77 }
78
79 // MakeCommand is a function to easily create new commands
80 // This can be called by plugins in Lua so that plugins can define their own commands
81 func MakeCommand(name, function string, completions ...Completion) {
82         action := commandActions[function]
83         if _, ok := commandActions[function]; !ok {
84                 // If the user seems to be binding a function that doesn't exist
85                 // We hope that it's a lua function that exists and bind it to that
86                 action = LuaFunctionCommand(function)
87         }
88
89         commands[name] = Command{action, completions}
90 }
91
92 // DefaultCommands returns a map containing micro's default commands
93 func DefaultCommands() map[string]StrCommand {
94         return map[string]StrCommand{
95                 "set":        {"Set", []Completion{OptionCompletion, OptionValueCompletion}},
96                 "setlocal":   {"SetLocal", []Completion{OptionCompletion, OptionValueCompletion}},
97                 "show":       {"Show", []Completion{OptionCompletion, NoCompletion}},
98                 "showkey":    {"ShowKey", []Completion{NoCompletion}},
99                 "bind":       {"Bind", []Completion{NoCompletion}},
100                 "run":        {"Run", []Completion{NoCompletion}},
101                 "quit":       {"Quit", []Completion{NoCompletion}},
102                 "save":       {"Save", []Completion{NoCompletion}},
103                 "replace":    {"Replace", []Completion{NoCompletion}},
104                 "replaceall": {"ReplaceAll", []Completion{NoCompletion}},
105                 "vsplit":     {"VSplit", []Completion{FileCompletion, NoCompletion}},
106                 "hsplit":     {"HSplit", []Completion{FileCompletion, NoCompletion}},
107                 "tab":        {"Tab", []Completion{FileCompletion, NoCompletion}},
108                 "help":       {"Help", []Completion{HelpCompletion, NoCompletion}},
109                 "eval":       {"Eval", []Completion{NoCompletion}},
110                 "log":        {"ToggleLog", []Completion{NoCompletion}},
111                 "plugin":     {"Plugin", []Completion{PluginCmdCompletion, PluginNameCompletion}},
112                 "reload":     {"Reload", []Completion{NoCompletion}},
113                 "cd":         {"Cd", []Completion{FileCompletion}},
114                 "pwd":        {"Pwd", []Completion{NoCompletion}},
115                 "open":       {"Open", []Completion{FileCompletion}},
116                 "tabswitch":  {"TabSwitch", []Completion{NoCompletion}},
117                 "memusage":   {"MemUsage", []Completion{NoCompletion}},
118                 "retab":      {"Retab", []Completion{NoCompletion}},
119                 "raw":        {"Raw", []Completion{NoCompletion}},
120         }
121 }
122
123 func CommandAction(cmd string) func(*View, bool) bool {
124         return func(v *View, usePlugin bool) bool {
125                 HandleCommand(cmd)
126                 return false
127         }
128 }
129
130 // PluginCmd installs, removes, updates, lists, or searches for given plugins
131 func PluginCmd(args []string) {
132         if len(args) >= 1 {
133                 switch args[0] {
134                 case "install":
135                         installedVersions := GetInstalledVersions(false)
136                         for _, plugin := range args[1:] {
137                                 pp := GetAllPluginPackages().Get(plugin)
138                                 if pp == nil {
139                                         messenger.Error("Unknown plugin \"" + plugin + "\"")
140                                 } else if err := pp.IsInstallable(); err != nil {
141                                         messenger.Error("Error installing ", plugin, ": ", err)
142                                 } else {
143                                         for _, installed := range installedVersions {
144                                                 if pp.Name == installed.pack.Name {
145                                                         if pp.Versions[0].Version.Compare(installed.Version) == 1 {
146                                                                 messenger.Error(pp.Name, " is already installed but out-of-date: use 'plugin update ", pp.Name, "' to update")
147                                                         } else {
148                                                                 messenger.Error(pp.Name, " is already installed")
149                                                         }
150                                                 }
151                                         }
152                                         pp.Install()
153                                 }
154                         }
155                 case "remove":
156                         removed := ""
157                         for _, plugin := range args[1:] {
158                                 // check if the plugin exists.
159                                 if _, ok := loadedPlugins[plugin]; ok {
160                                         UninstallPlugin(plugin)
161                                         removed += plugin + " "
162                                         continue
163                                 }
164                         }
165                         if !IsSpaces(removed) {
166                                 messenger.Message("Removed ", removed)
167                         } else {
168                                 messenger.Error("The requested plugins do not exist")
169                         }
170                 case "update":
171                         UpdatePlugins(args[1:])
172                 case "list":
173                         plugins := GetInstalledVersions(false)
174                         messenger.AddLog("----------------")
175                         messenger.AddLog("The following plugins are currently installed:\n")
176                         for _, p := range plugins {
177                                 messenger.AddLog(fmt.Sprintf("%s (%s)", p.pack.Name, p.Version))
178                         }
179                         messenger.AddLog("----------------")
180                         if len(plugins) > 0 {
181                                 if CurView().Type != vtLog {
182                                         ToggleLog([]string{})
183                                 }
184                         }
185                 case "search":
186                         plugins := SearchPlugin(args[1:])
187                         messenger.Message(len(plugins), " plugins found")
188                         for _, p := range plugins {
189                                 messenger.AddLog("----------------")
190                                 messenger.AddLog(p.String())
191                         }
192                         messenger.AddLog("----------------")
193                         if len(plugins) > 0 {
194                                 if CurView().Type != vtLog {
195                                         ToggleLog([]string{})
196                                 }
197                         }
198                 case "available":
199                         packages := GetAllPluginPackages()
200                         messenger.AddLog("Available Plugins:")
201                         for _, pkg := range packages {
202                                 messenger.AddLog(pkg.Name)
203                         }
204                         if CurView().Type != vtLog {
205                                 ToggleLog([]string{})
206                         }
207                 }
208         } else {
209                 messenger.Error("Not enough arguments")
210         }
211 }
212
213 func Retab(args []string) {
214         CurView().Retab(true)
215 }
216
217 func Raw(args []string) {
218         buf := NewBufferFromString("", "Raw events")
219
220         view := NewView(buf)
221         view.Buf.Insert(view.Cursor.Loc, "Warning: Showing raw event escape codes\n")
222         view.Buf.Insert(view.Cursor.Loc, "Use CtrlQ to exit\n")
223         view.Type = vtRaw
224         tab := NewTabFromView(view)
225         tab.SetNum(len(tabs))
226         tabs = append(tabs, tab)
227         curTab = len(tabs) - 1
228         if len(tabs) == 2 {
229                 for _, t := range tabs {
230                         for _, v := range t.views {
231                                 v.ToggleTabbar()
232                         }
233                 }
234         }
235 }
236
237 // TabSwitch switches to a given tab either by name or by number
238 func TabSwitch(args []string) {
239         if len(args) > 0 {
240                 num, err := strconv.Atoi(args[0])
241                 if err != nil {
242                         // Check for tab with this name
243
244                         found := false
245                         for _, t := range tabs {
246                                 v := t.views[t.CurView]
247                                 if v.Buf.GetName() == args[0] {
248                                         curTab = v.TabNum
249                                         found = true
250                                 }
251                         }
252                         if !found {
253                                 messenger.Error("Could not find tab: ", err)
254                         }
255                 } else {
256                         num--
257                         if num >= 0 && num < len(tabs) {
258                                 curTab = num
259                         } else {
260                                 messenger.Error("Invalid tab index")
261                         }
262                 }
263         }
264 }
265
266 // Cd changes the current working directory
267 func Cd(args []string) {
268         if len(args) > 0 {
269                 path := ReplaceHome(args[0])
270                 err := os.Chdir(path)
271                 if err != nil {
272                         messenger.Error("Error with cd: ", err)
273                         return
274                 }
275                 wd, _ := os.Getwd()
276                 for _, tab := range tabs {
277                         for _, view := range tab.views {
278                                 if len(view.Buf.name) == 0 {
279                                         continue
280                                 }
281
282                                 view.Buf.Path, _ = MakeRelative(view.Buf.AbsPath, wd)
283                                 if p, _ := filepath.Abs(view.Buf.Path); !strings.Contains(p, wd) {
284                                         view.Buf.Path = view.Buf.AbsPath
285                                 }
286                         }
287                 }
288         }
289 }
290
291 // MemUsage prints micro's memory usage
292 // Alloc shows how many bytes are currently in use
293 // Sys shows how many bytes have been requested from the operating system
294 // NumGC shows how many times the GC has been run
295 // Note that Go commonly reserves more memory from the OS than is currently in-use/required
296 // Additionally, even if Go returns memory to the OS, the OS does not always claim it because
297 // there may be plenty of memory to spare
298 func MemUsage(args []string) {
299         var mem runtime.MemStats
300         runtime.ReadMemStats(&mem)
301
302         messenger.Message(fmt.Sprintf("Alloc: %v, Sys: %v, NumGC: %v", humanize.Bytes(mem.Alloc), humanize.Bytes(mem.Sys), mem.NumGC))
303 }
304
305 // Pwd prints the current working directory
306 func Pwd(args []string) {
307         wd, err := os.Getwd()
308         if err != nil {
309                 messenger.Message(err.Error())
310         } else {
311                 messenger.Message(wd)
312         }
313 }
314
315 // Open opens a new buffer with a given filename
316 func Open(args []string) {
317         if len(args) > 0 {
318                 filename := args[0]
319                 // the filename might or might not be quoted, so unquote first then join the strings.
320                 args, err := shellwords.Split(filename)
321                 if err != nil {
322                         messenger.Error("Error parsing args ", err)
323                         return
324                 }
325                 filename = strings.Join(args, " ")
326
327                 CurView().Open(filename)
328         } else {
329                 messenger.Error("No filename")
330         }
331 }
332
333 // ToggleLog toggles the log view
334 func ToggleLog(args []string) {
335         buffer := messenger.getBuffer()
336         if CurView().Type != vtLog {
337                 CurView().HSplit(buffer)
338                 CurView().Type = vtLog
339                 RedrawAll()
340                 buffer.Cursor.Loc = buffer.Start()
341                 CurView().Relocate()
342                 buffer.Cursor.Loc = buffer.End()
343                 CurView().Relocate()
344         } else {
345                 CurView().Quit(true)
346         }
347 }
348
349 // Reload reloads all files (syntax files, colorschemes...)
350 func Reload(args []string) {
351         LoadAll()
352 }
353
354 // Help tries to open the given help page in a horizontal split
355 func Help(args []string) {
356         if len(args) < 1 {
357                 // Open the default help if the user just typed "> help"
358                 CurView().openHelp("help")
359         } else {
360                 helpPage := args[0]
361                 if FindRuntimeFile(RTHelp, helpPage) != nil {
362                         CurView().openHelp(helpPage)
363                 } else {
364                         messenger.Error("Sorry, no help for ", helpPage)
365                 }
366         }
367 }
368
369 // VSplit opens a vertical split with file given in the first argument
370 // If no file is given, it opens an empty buffer in a new split
371 func VSplit(args []string) {
372         if len(args) == 0 {
373                 CurView().VSplit(NewBufferFromString("", ""))
374         } else {
375                 filename := args[0]
376                 filename = ReplaceHome(filename)
377                 file, err := os.Open(filename)
378                 fileInfo, _ := os.Stat(filename)
379
380                 if err == nil && fileInfo.IsDir() {
381                         messenger.Error(filename, " is a directory")
382                         return
383                 }
384
385                 defer file.Close()
386
387                 var buf *Buffer
388                 if err != nil {
389                         // File does not exist -- create an empty buffer with that name
390                         buf = NewBufferFromString("", filename)
391                 } else {
392                         buf = NewBuffer(file, FSize(file), filename)
393                 }
394                 CurView().VSplit(buf)
395         }
396 }
397
398 // HSplit opens a horizontal split with file given in the first argument
399 // If no file is given, it opens an empty buffer in a new split
400 func HSplit(args []string) {
401         if len(args) == 0 {
402                 CurView().HSplit(NewBufferFromString("", ""))
403         } else {
404                 filename := args[0]
405                 filename = ReplaceHome(filename)
406                 file, err := os.Open(filename)
407                 fileInfo, _ := os.Stat(filename)
408
409                 if err == nil && fileInfo.IsDir() {
410                         messenger.Error(filename, " is a directory")
411                         return
412                 }
413
414                 defer file.Close()
415
416                 var buf *Buffer
417                 if err != nil {
418                         // File does not exist -- create an empty buffer with that name
419                         buf = NewBufferFromString("", filename)
420                 } else {
421                         buf = NewBuffer(file, FSize(file), filename)
422                 }
423                 CurView().HSplit(buf)
424         }
425 }
426
427 // Eval evaluates a lua expression
428 func Eval(args []string) {
429         if len(args) >= 1 {
430                 err := L.DoString(args[0])
431                 if err != nil {
432                         messenger.Error(err)
433                 }
434         } else {
435                 messenger.Error("Not enough arguments")
436         }
437 }
438
439 // NewTab opens the given file in a new tab
440 func NewTab(args []string) {
441         if len(args) == 0 {
442                 CurView().AddTab(true)
443         } else {
444                 filename := args[0]
445                 filename = ReplaceHome(filename)
446                 file, err := os.Open(filename)
447                 fileInfo, _ := os.Stat(filename)
448
449                 if err == nil && fileInfo.IsDir() {
450                         messenger.Error(filename, " is a directory")
451                         return
452                 }
453
454                 defer file.Close()
455
456                 var buf *Buffer
457                 if err != nil {
458                         buf = NewBufferFromString("", filename)
459                 } else {
460                         buf = NewBuffer(file, FSize(file), filename)
461                 }
462
463                 tab := NewTabFromView(NewView(buf))
464                 tab.SetNum(len(tabs))
465                 tabs = append(tabs, tab)
466                 curTab = len(tabs) - 1
467                 if len(tabs) == 2 {
468                         for _, t := range tabs {
469                                 for _, v := range t.views {
470                                         v.ToggleTabbar()
471                                 }
472                         }
473                 }
474         }
475 }
476
477 // Set sets an option
478 func Set(args []string) {
479         if len(args) < 2 {
480                 messenger.Error("Not enough arguments")
481                 return
482         }
483
484         option := args[0]
485         value := args[1]
486
487         SetOptionAndSettings(option, value)
488 }
489
490 // SetLocal sets an option local to the buffer
491 func SetLocal(args []string) {
492         if len(args) < 2 {
493                 messenger.Error("Not enough arguments")
494                 return
495         }
496
497         option := args[0]
498         value := args[1]
499
500         err := SetLocalOption(option, value, CurView())
501         if err != nil {
502                 messenger.Error(err.Error())
503         }
504 }
505
506 // Show shows the value of the given option
507 func Show(args []string) {
508         if len(args) < 1 {
509                 messenger.Error("Please provide an option to show")
510                 return
511         }
512
513         option := GetOption(args[0])
514
515         if option == nil {
516                 messenger.Error(args[0], " is not a valid option")
517                 return
518         }
519
520         messenger.Message(option)
521 }
522
523 // ShowKey displays the action that a key is bound to
524 func ShowKey(args []string) {
525         if len(args) < 1 {
526                 messenger.Error("Please provide a key to show")
527                 return
528         }
529
530         if action, ok := bindingsStr[args[0]]; ok {
531                 messenger.Message(action)
532         } else {
533                 messenger.Message(args[0], " has no binding")
534         }
535 }
536
537 // Bind creates a new keybinding
538 func Bind(args []string) {
539         if len(args) < 2 {
540                 messenger.Error("Not enough arguments")
541                 return
542         }
543         BindKey(args[0], args[1])
544 }
545
546 // Run runs a shell command in the background
547 func Run(args []string) {
548         // Run a shell command in the background (openTerm is false)
549         HandleShellCommand(shellwords.Join(args...), false, true)
550 }
551
552 // Quit closes the main view
553 func Quit(args []string) {
554         // Close the main view
555         CurView().Quit(true)
556 }
557
558 // Save saves the buffer in the main view
559 func Save(args []string) {
560         if len(args) == 0 {
561                 // Save the main view
562                 CurView().Save(true)
563         } else {
564                 CurView().Buf.SaveAs(args[0])
565         }
566 }
567
568 // Replace runs search and replace
569 func Replace(args []string) {
570         if len(args) < 2 || len(args) > 4 {
571                 // We need to find both a search and replace expression
572                 messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
573                 return
574         }
575
576         all := false
577         noRegex := false
578
579         if len(args) > 2 {
580                 for _, arg := range args[2:] {
581                         switch arg {
582                         case "-a":
583                                 all = true
584                         case "-l":
585                                 noRegex = true
586                         default:
587                                 messenger.Error("Invalid flag: " + arg)
588                                 return
589                         }
590                 }
591         }
592
593         search := string(args[0])
594
595         if noRegex {
596                 search = regexp.QuoteMeta(search)
597         }
598
599         replace := string(args[1])
600
601         regex, err := regexp.Compile("(?m)" + search)
602         if err != nil {
603                 // There was an error with the user's regex
604                 messenger.Error(err.Error())
605                 return
606         }
607
608         view := CurView()
609
610         found := 0
611         replaceAll := func() {
612                 var deltas []Delta
613                 deltaXOffset := Count(replace) - Count(search)
614                 for i := 0; i < view.Buf.LinesNum(); i++ {
615                         matches := regex.FindAllIndex(view.Buf.lines[i].data, -1)
616                         str := string(view.Buf.lines[i].data)
617
618                         if matches != nil {
619                                 xOffset := 0
620                                 for _, m := range matches {
621                                         from := Loc{runePos(m[0], str) + xOffset, i}
622                                         to := Loc{runePos(m[1], str) + xOffset, i}
623
624                                         xOffset += deltaXOffset
625
626                                         deltas = append(deltas, Delta{replace, from, to})
627                                         found++
628                                 }
629                         }
630                 }
631                 view.Buf.MultipleReplace(deltas)
632         }
633
634         if all {
635                 replaceAll()
636         } else {
637                 for {
638                         // The 'check' flag was used
639                         Search(search, view, true)
640                         if !view.Cursor.HasSelection() {
641                                 break
642                         }
643                         view.Relocate()
644                         RedrawAll()
645                         choice, canceled := messenger.LetterPrompt("Perform replacement? (y,n,a)", 'y', 'n', 'a')
646                         if canceled {
647                                 if view.Cursor.HasSelection() {
648                                         view.Cursor.Loc = view.Cursor.CurSelection[0]
649                                         view.Cursor.ResetSelection()
650                                 }
651                                 messenger.Reset()
652                                 break
653                         } else if choice == 'a' {
654                                 if view.Cursor.HasSelection() {
655                                         view.Cursor.Loc = view.Cursor.CurSelection[0]
656                                         view.Cursor.ResetSelection()
657                                 }
658                                 messenger.Reset()
659                                 replaceAll()
660                                 break
661                         } else if choice == 'y' {
662                                 view.Cursor.DeleteSelection()
663                                 view.Buf.Insert(view.Cursor.Loc, replace)
664                                 view.Cursor.ResetSelection()
665                                 messenger.Reset()
666                                 found++
667                         }
668                         if view.Cursor.HasSelection() {
669                                 searchStart = view.Cursor.CurSelection[1]
670                         } else {
671                                 searchStart = view.Cursor.Loc
672                         }
673                 }
674         }
675         view.Cursor.Relocate()
676
677         if found > 1 {
678                 messenger.Message("Replaced ", found, " occurrences of ", search)
679         } else if found == 1 {
680                 messenger.Message("Replaced ", found, " occurrence of ", search)
681         } else {
682                 messenger.Message("Nothing matched ", search)
683         }
684 }
685
686 // ReplaceAll replaces search term all at once
687 func ReplaceAll(args []string) {
688         // aliased to Replace command
689         Replace(append(args, "-a"))
690 }
691
692 // RunShellCommand executes a shell command and returns the output/error
693 func RunShellCommand(input string) (string, error) {
694         args, err := shellwords.Split(input)
695         if err != nil {
696                 return "", err
697         }
698         inputCmd := args[0]
699
700         cmd := exec.Command(inputCmd, args[1:]...)
701         outputBytes := &bytes.Buffer{}
702         cmd.Stdout = outputBytes
703         cmd.Stderr = outputBytes
704         cmd.Start()
705         err = cmd.Wait() // wait for command to finish
706         outstring := outputBytes.String()
707         return outstring, err
708 }
709
710 // HandleShellCommand runs the shell command
711 // The openTerm argument specifies whether a terminal should be opened (for viewing output
712 // or interacting with stdin)
713 func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
714         args, err := shellwords.Split(input)
715         if err != nil {
716                 return ""
717         }
718         inputCmd := args[0]
719         if !openTerm {
720                 // Simply run the command in the background and notify the user when it's done
721                 messenger.Message("Running...")
722                 go func() {
723                         output, err := RunShellCommand(input)
724                         totalLines := strings.Split(output, "\n")
725
726                         if len(totalLines) < 3 {
727                                 if err == nil {
728                                         messenger.Message(inputCmd, " exited without error")
729                                 } else {
730                                         messenger.Message(inputCmd, " exited with error: ", err, ": ", output)
731                                 }
732                         } else {
733                                 messenger.Message(output)
734                         }
735                         // We have to make sure to redraw
736                         RedrawAll()
737                 }()
738         } else {
739                 // Shut down the screen because we're going to interact directly with the shell
740                 screen.Fini()
741                 screen = nil
742
743                 args := args[1:]
744
745                 // Set up everything for the command
746                 var output string
747                 cmd := exec.Command(inputCmd, args...)
748                 cmd.Stdin = os.Stdin
749                 cmd.Stdout = os.Stdout
750                 cmd.Stderr = os.Stderr
751
752                 // This is a trap for Ctrl-C so that it doesn't kill micro
753                 // Instead we trap Ctrl-C to kill the program we're running
754                 c := make(chan os.Signal, 1)
755                 signal.Notify(c, os.Interrupt)
756                 go func() {
757                         for range c {
758                                 cmd.Process.Kill()
759                         }
760                 }()
761
762                 cmd.Start()
763                 err := cmd.Wait()
764
765                 if err != nil {
766                         output = err.Error()
767                 }
768
769                 if waitToFinish {
770                         // This is just so we don't return right away and let the user press enter to return
771                         TermMessage("")
772                 }
773
774                 // Start the screen back up
775                 InitScreen()
776
777                 return output
778         }
779         return ""
780 }
781
782 // HandleCommand handles input from the user
783 func HandleCommand(input string) {
784         args, err := shellwords.Split(input)
785         if err != nil {
786                 messenger.Error("Error parsing args ", err)
787                 return
788         }
789
790         inputCmd := args[0]
791
792         if _, ok := commands[inputCmd]; !ok {
793                 messenger.Error("Unknown command ", inputCmd)
794         } else {
795                 commands[inputCmd].action(args[1:])
796         }
797 }