1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package cobra
16
17 import (
18 "fmt"
19 "os"
20 "strings"
21 "sync"
22
23 "github.com/spf13/pflag"
24 )
25
26 const (
27
28
29 ShellCompRequestCmd = "__complete"
30
31
32 ShellCompNoDescRequestCmd = "__completeNoDesc"
33 )
34
35
36 var flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective){}
37
38
39 var flagCompletionMutex = &sync.RWMutex{}
40
41
42
43 type ShellCompDirective int
44
45 type flagCompError struct {
46 subCommand string
47 flagName string
48 }
49
50 func (e *flagCompError) Error() string {
51 return "Subcommand '" + e.subCommand + "' does not support flag '" + e.flagName + "'"
52 }
53
54 const (
55
56 ShellCompDirectiveError ShellCompDirective = 1 << iota
57
58
59
60 ShellCompDirectiveNoSpace
61
62
63
64 ShellCompDirectiveNoFileComp
65
66
67
68
69
70
71 ShellCompDirectiveFilterFileExt
72
73
74
75
76
77
78 ShellCompDirectiveFilterDirs
79
80
81
82 ShellCompDirectiveKeepOrder
83
84
85
86
87
88 shellCompDirectiveMaxValue
89
90
91
92
93 ShellCompDirectiveDefault ShellCompDirective = 0
94 )
95
96 const (
97
98 compCmdName = "completion"
99 compCmdNoDescFlagName = "no-descriptions"
100 compCmdNoDescFlagDesc = "disable completion descriptions"
101 compCmdNoDescFlagDefault = false
102 )
103
104
105 type CompletionOptions struct {
106
107 DisableDefaultCmd bool
108
109
110 DisableNoDescFlag bool
111
112
113 DisableDescriptions bool
114
115 HiddenDefaultCmd bool
116 }
117
118
119
120 func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
121 return nil, ShellCompDirectiveNoFileComp
122 }
123
124
125
126 func FixedCompletions(choices []string, directive ShellCompDirective) func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
127 return func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
128 return choices, directive
129 }
130 }
131
132
133 func (c *Command) RegisterFlagCompletionFunc(flagName string, f func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)) error {
134 flag := c.Flag(flagName)
135 if flag == nil {
136 return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' does not exist", flagName)
137 }
138 flagCompletionMutex.Lock()
139 defer flagCompletionMutex.Unlock()
140
141 if _, exists := flagCompletionFunctions[flag]; exists {
142 return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' already registered", flagName)
143 }
144 flagCompletionFunctions[flag] = f
145 return nil
146 }
147
148
149 func (c *Command) GetFlagCompletionFunc(flagName string) (func(*Command, []string, string) ([]string, ShellCompDirective), bool) {
150 flag := c.Flag(flagName)
151 if flag == nil {
152 return nil, false
153 }
154
155 flagCompletionMutex.RLock()
156 defer flagCompletionMutex.RUnlock()
157
158 completionFunc, exists := flagCompletionFunctions[flag]
159 return completionFunc, exists
160 }
161
162
163 func (d ShellCompDirective) string() string {
164 var directives []string
165 if d&ShellCompDirectiveError != 0 {
166 directives = append(directives, "ShellCompDirectiveError")
167 }
168 if d&ShellCompDirectiveNoSpace != 0 {
169 directives = append(directives, "ShellCompDirectiveNoSpace")
170 }
171 if d&ShellCompDirectiveNoFileComp != 0 {
172 directives = append(directives, "ShellCompDirectiveNoFileComp")
173 }
174 if d&ShellCompDirectiveFilterFileExt != 0 {
175 directives = append(directives, "ShellCompDirectiveFilterFileExt")
176 }
177 if d&ShellCompDirectiveFilterDirs != 0 {
178 directives = append(directives, "ShellCompDirectiveFilterDirs")
179 }
180 if d&ShellCompDirectiveKeepOrder != 0 {
181 directives = append(directives, "ShellCompDirectiveKeepOrder")
182 }
183 if len(directives) == 0 {
184 directives = append(directives, "ShellCompDirectiveDefault")
185 }
186
187 if d >= shellCompDirectiveMaxValue {
188 return fmt.Sprintf("ERROR: unexpected ShellCompDirective value: %d", d)
189 }
190 return strings.Join(directives, ", ")
191 }
192
193
194 func (c *Command) initCompleteCmd(args []string) {
195 completeCmd := &Command{
196 Use: fmt.Sprintf("%s [command-line]", ShellCompRequestCmd),
197 Aliases: []string{ShellCompNoDescRequestCmd},
198 DisableFlagsInUseLine: true,
199 Hidden: true,
200 DisableFlagParsing: true,
201 Args: MinimumNArgs(1),
202 Short: "Request shell completion choices for the specified command-line",
203 Long: fmt.Sprintf("%[2]s is a special command that is used by the shell completion logic\n%[1]s",
204 "to request completion choices for the specified command-line.", ShellCompRequestCmd),
205 Run: func(cmd *Command, args []string) {
206 finalCmd, completions, directive, err := cmd.getCompletions(args)
207 if err != nil {
208 CompErrorln(err.Error())
209
210
211
212 }
213
214 noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd)
215 for _, comp := range completions {
216 if GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable {
217
218 if strings.HasPrefix(comp, activeHelpMarker) {
219 continue
220 }
221 }
222 if noDescriptions {
223
224 comp = strings.Split(comp, "\t")[0]
225 }
226
227
228
229
230
231 comp = strings.Split(comp, "\n")[0]
232
233
234
235
236
237
238 comp = strings.TrimSpace(comp)
239
240
241 fmt.Fprintln(finalCmd.OutOrStdout(), comp)
242 }
243
244
245
246
247 fmt.Fprintf(finalCmd.OutOrStdout(), ":%d\n", directive)
248
249
250
251 fmt.Fprintf(finalCmd.ErrOrStderr(), "Completion ended with directive: %s\n", directive.string())
252 },
253 }
254 c.AddCommand(completeCmd)
255 subCmd, _, err := c.Find(args)
256 if err != nil || subCmd.Name() != ShellCompRequestCmd {
257
258
259
260
261
262 c.RemoveCommand(completeCmd)
263 }
264 }
265
266 func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) {
267
268
269 toComplete := args[len(args)-1]
270 trimmedArgs := args[:len(args)-1]
271
272 var finalCmd *Command
273 var finalArgs []string
274 var err error
275
276
277 if c.Root().TraverseChildren {
278 finalCmd, finalArgs, err = c.Root().Traverse(trimmedArgs)
279 } else {
280
281
282
283
284
285 rootCmd := c.Root()
286 if len(rootCmd.Commands()) == 1 {
287 rootCmd.RemoveCommand(c)
288 }
289
290 finalCmd, finalArgs, err = rootCmd.Find(trimmedArgs)
291 }
292 if err != nil {
293
294 return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs)
295 }
296 finalCmd.ctx = c.ctx
297
298
299
300
301
302
303 if !finalCmd.DisableFlagParsing {
304 finalCmd.InitDefaultHelpFlag()
305 finalCmd.InitDefaultVersionFlag()
306 }
307
308
309
310
311
312 flag, finalArgs, toComplete, flagErr := checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
313
314
315
316
317
318 flagCompletion := true
319 _ = finalCmd.ParseFlags(append(finalArgs, "--"))
320 newArgCount := finalCmd.Flags().NArg()
321
322
323 if err = finalCmd.ParseFlags(finalArgs); err != nil {
324 return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
325 }
326
327 realArgCount := finalCmd.Flags().NArg()
328 if newArgCount > realArgCount {
329
330 flagCompletion = false
331 }
332
333 if flagErr != nil {
334
335 if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
336 return finalCmd, []string{}, ShellCompDirectiveDefault, flagErr
337 }
338 }
339
340
341
342 if helpOrVersionFlagPresent(finalCmd) {
343 return finalCmd, []string{}, ShellCompDirectiveNoFileComp, nil
344 }
345
346
347
348 if !finalCmd.DisableFlagParsing {
349 finalArgs = finalCmd.Flags().Args()
350 }
351
352 if flag != nil && flagCompletion {
353
354 if validExts, present := flag.Annotations[BashCompFilenameExt]; present {
355 if len(validExts) != 0 {
356
357 return finalCmd, validExts, ShellCompDirectiveFilterFileExt, nil
358 }
359
360
361
362
363
364 }
365
366 if subDir, present := flag.Annotations[BashCompSubdirsInDir]; present {
367 if len(subDir) == 1 {
368
369 return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil
370 }
371
372 return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil
373 }
374 }
375
376 var completions []string
377 var directive ShellCompDirective
378
379
380 finalCmd.enforceFlagGroupsForCompletion()
381
382
383
384
385
386
387
388 if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") && flagCompletion {
389
390 completions = completeRequireFlags(finalCmd, toComplete)
391
392
393 if len(completions) == 0 {
394 doCompleteFlags := func(flag *pflag.Flag) {
395 if !flag.Changed ||
396 strings.Contains(flag.Value.Type(), "Slice") ||
397 strings.Contains(flag.Value.Type(), "Array") {
398
399
400 completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
401 }
402 }
403
404
405
406
407 finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
408 doCompleteFlags(flag)
409 })
410
411
412
413
414
415 finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
416 doCompleteFlags(flag)
417 })
418 }
419
420 directive = ShellCompDirectiveNoFileComp
421 if len(completions) == 1 && strings.HasSuffix(completions[0], "=") {
422
423
424 directive = ShellCompDirectiveNoSpace
425 }
426
427 if !finalCmd.DisableFlagParsing {
428
429
430
431
432 return finalCmd, completions, directive, nil
433 }
434 } else {
435 directive = ShellCompDirectiveDefault
436 if flag == nil {
437 foundLocalNonPersistentFlag := false
438
439
440 if !finalCmd.Root().TraverseChildren {
441
442 localNonPersistentFlags := finalCmd.LocalNonPersistentFlags()
443 finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
444 if localNonPersistentFlags.Lookup(flag.Name) != nil && flag.Changed {
445 foundLocalNonPersistentFlag = true
446 }
447 })
448 }
449
450
451 if len(finalArgs) == 0 && !foundLocalNonPersistentFlag {
452
453
454
455 for _, subCmd := range finalCmd.Commands() {
456 if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand {
457 if strings.HasPrefix(subCmd.Name(), toComplete) {
458 completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
459 }
460 directive = ShellCompDirectiveNoFileComp
461 }
462 }
463 }
464
465
466 completions = append(completions, completeRequireFlags(finalCmd, toComplete)...)
467
468
469
470 if len(finalCmd.ValidArgs) > 0 {
471 if len(finalArgs) == 0 {
472
473 for _, validArg := range finalCmd.ValidArgs {
474 if strings.HasPrefix(validArg, toComplete) {
475 completions = append(completions, validArg)
476 }
477 }
478 directive = ShellCompDirectiveNoFileComp
479
480
481
482 if len(completions) == 0 {
483 for _, argAlias := range finalCmd.ArgAliases {
484 if strings.HasPrefix(argAlias, toComplete) {
485 completions = append(completions, argAlias)
486 }
487 }
488 }
489 }
490
491
492
493 return finalCmd, completions, directive, nil
494 }
495
496
497
498
499 }
500 }
501
502
503 var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
504 if flag != nil && flagCompletion {
505 flagCompletionMutex.RLock()
506 completionFn = flagCompletionFunctions[flag]
507 flagCompletionMutex.RUnlock()
508 } else {
509 completionFn = finalCmd.ValidArgsFunction
510 }
511 if completionFn != nil {
512
513
514 var comps []string
515 comps, directive = completionFn(finalCmd, finalArgs, toComplete)
516 completions = append(completions, comps...)
517 }
518
519 return finalCmd, completions, directive, nil
520 }
521
522 func helpOrVersionFlagPresent(cmd *Command) bool {
523 if versionFlag := cmd.Flags().Lookup("version"); versionFlag != nil &&
524 len(versionFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && versionFlag.Changed {
525 return true
526 }
527 if helpFlag := cmd.Flags().Lookup("help"); helpFlag != nil &&
528 len(helpFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && helpFlag.Changed {
529 return true
530 }
531 return false
532 }
533
534 func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string {
535 if nonCompletableFlag(flag) {
536 return []string{}
537 }
538
539 var completions []string
540 flagName := "--" + flag.Name
541 if strings.HasPrefix(flagName, toComplete) {
542
543 completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
544
545
546
547
548
549
550
551
552
553
554
555
556
557 }
558
559 flagName = "-" + flag.Shorthand
560 if len(flag.Shorthand) > 0 && strings.HasPrefix(flagName, toComplete) {
561 completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
562 }
563
564 return completions
565 }
566
567 func completeRequireFlags(finalCmd *Command, toComplete string) []string {
568 var completions []string
569
570 doCompleteRequiredFlags := func(flag *pflag.Flag) {
571 if _, present := flag.Annotations[BashCompOneRequiredFlag]; present {
572 if !flag.Changed {
573
574 completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
575 }
576 }
577 }
578
579
580
581
582 finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
583 doCompleteRequiredFlags(flag)
584 })
585 finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
586 doCompleteRequiredFlags(flag)
587 })
588
589 return completions
590 }
591
592 func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
593 if finalCmd.DisableFlagParsing {
594
595
596 return nil, args, lastArg, nil
597 }
598
599 var flagName string
600 trimmedArgs := args
601 flagWithEqual := false
602 orgLastArg := lastArg
603
604
605
606
607 if len(lastArg) > 0 && lastArg[0] == '-' {
608 if index := strings.Index(lastArg, "="); index >= 0 {
609
610 if strings.HasPrefix(lastArg[:index], "--") {
611
612 flagName = lastArg[2:index]
613 } else {
614
615
616
617
618 flagName = lastArg[index-1 : index]
619 }
620 lastArg = lastArg[index+1:]
621 flagWithEqual = true
622 } else {
623
624 return nil, args, lastArg, nil
625 }
626 }
627
628 if len(flagName) == 0 {
629 if len(args) > 0 {
630 prevArg := args[len(args)-1]
631 if isFlagArg(prevArg) {
632
633
634
635 if index := strings.Index(prevArg, "="); index < 0 {
636 if strings.HasPrefix(prevArg, "--") {
637
638 flagName = prevArg[2:]
639 } else {
640
641
642
643
644 flagName = prevArg[len(prevArg)-1:]
645 }
646
647
648 trimmedArgs = args[:len(args)-1]
649 }
650 }
651 }
652 }
653
654 if len(flagName) == 0 {
655
656 return nil, trimmedArgs, lastArg, nil
657 }
658
659 flag := findFlag(finalCmd, flagName)
660 if flag == nil {
661
662 return nil, args, orgLastArg, &flagCompError{subCommand: finalCmd.Name(), flagName: flagName}
663 }
664
665 if !flagWithEqual {
666 if len(flag.NoOptDefVal) != 0 {
667
668
669
670 trimmedArgs = args
671 flag = nil
672 }
673 }
674
675 return flag, trimmedArgs, lastArg, nil
676 }
677
678
679
680
681
682
683 func (c *Command) InitDefaultCompletionCmd() {
684 if c.CompletionOptions.DisableDefaultCmd || !c.HasSubCommands() {
685 return
686 }
687
688 for _, cmd := range c.commands {
689 if cmd.Name() == compCmdName || cmd.HasAlias(compCmdName) {
690
691 return
692 }
693 }
694
695 haveNoDescFlag := !c.CompletionOptions.DisableNoDescFlag && !c.CompletionOptions.DisableDescriptions
696
697 completionCmd := &Command{
698 Use: compCmdName,
699 Short: "Generate the autocompletion script for the specified shell",
700 Long: fmt.Sprintf(`Generate the autocompletion script for %[1]s for the specified shell.
701 See each sub-command's help for details on how to use the generated script.
702 `, c.Root().Name()),
703 Args: NoArgs,
704 ValidArgsFunction: NoFileCompletions,
705 Hidden: c.CompletionOptions.HiddenDefaultCmd,
706 GroupID: c.completionCommandGroupID,
707 }
708 c.AddCommand(completionCmd)
709
710 out := c.OutOrStdout()
711 noDesc := c.CompletionOptions.DisableDescriptions
712 shortDesc := "Generate the autocompletion script for %s"
713 bash := &Command{
714 Use: "bash",
715 Short: fmt.Sprintf(shortDesc, "bash"),
716 Long: fmt.Sprintf(`Generate the autocompletion script for the bash shell.
717
718 This script depends on the 'bash-completion' package.
719 If it is not installed already, you can install it via your OS's package manager.
720
721 To load completions in your current shell session:
722
723 source <(%[1]s completion bash)
724
725 To load completions for every new session, execute once:
726
727 #### Linux:
728
729 %[1]s completion bash > /etc/bash_completion.d/%[1]s
730
731 #### macOS:
732
733 %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
734
735 You will need to start a new shell for this setup to take effect.
736 `, c.Root().Name()),
737 Args: NoArgs,
738 DisableFlagsInUseLine: true,
739 ValidArgsFunction: NoFileCompletions,
740 RunE: func(cmd *Command, args []string) error {
741 return cmd.Root().GenBashCompletionV2(out, !noDesc)
742 },
743 }
744 if haveNoDescFlag {
745 bash.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
746 }
747
748 zsh := &Command{
749 Use: "zsh",
750 Short: fmt.Sprintf(shortDesc, "zsh"),
751 Long: fmt.Sprintf(`Generate the autocompletion script for the zsh shell.
752
753 If shell completion is not already enabled in your environment you will need
754 to enable it. You can execute the following once:
755
756 echo "autoload -U compinit; compinit" >> ~/.zshrc
757
758 To load completions in your current shell session:
759
760 source <(%[1]s completion zsh)
761
762 To load completions for every new session, execute once:
763
764 #### Linux:
765
766 %[1]s completion zsh > "${fpath[1]}/_%[1]s"
767
768 #### macOS:
769
770 %[1]s completion zsh > $(brew --prefix)/share/zsh/site-functions/_%[1]s
771
772 You will need to start a new shell for this setup to take effect.
773 `, c.Root().Name()),
774 Args: NoArgs,
775 ValidArgsFunction: NoFileCompletions,
776 RunE: func(cmd *Command, args []string) error {
777 if noDesc {
778 return cmd.Root().GenZshCompletionNoDesc(out)
779 }
780 return cmd.Root().GenZshCompletion(out)
781 },
782 }
783 if haveNoDescFlag {
784 zsh.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
785 }
786
787 fish := &Command{
788 Use: "fish",
789 Short: fmt.Sprintf(shortDesc, "fish"),
790 Long: fmt.Sprintf(`Generate the autocompletion script for the fish shell.
791
792 To load completions in your current shell session:
793
794 %[1]s completion fish | source
795
796 To load completions for every new session, execute once:
797
798 %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
799
800 You will need to start a new shell for this setup to take effect.
801 `, c.Root().Name()),
802 Args: NoArgs,
803 ValidArgsFunction: NoFileCompletions,
804 RunE: func(cmd *Command, args []string) error {
805 return cmd.Root().GenFishCompletion(out, !noDesc)
806 },
807 }
808 if haveNoDescFlag {
809 fish.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
810 }
811
812 powershell := &Command{
813 Use: "powershell",
814 Short: fmt.Sprintf(shortDesc, "powershell"),
815 Long: fmt.Sprintf(`Generate the autocompletion script for powershell.
816
817 To load completions in your current shell session:
818
819 %[1]s completion powershell | Out-String | Invoke-Expression
820
821 To load completions for every new session, add the output of the above command
822 to your powershell profile.
823 `, c.Root().Name()),
824 Args: NoArgs,
825 ValidArgsFunction: NoFileCompletions,
826 RunE: func(cmd *Command, args []string) error {
827 if noDesc {
828 return cmd.Root().GenPowerShellCompletion(out)
829 }
830 return cmd.Root().GenPowerShellCompletionWithDesc(out)
831
832 },
833 }
834 if haveNoDescFlag {
835 powershell.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
836 }
837
838 completionCmd.AddCommand(bash, zsh, fish, powershell)
839 }
840
841 func findFlag(cmd *Command, name string) *pflag.Flag {
842 flagSet := cmd.Flags()
843 if len(name) == 1 {
844
845
846 if short := flagSet.ShorthandLookup(name); short != nil {
847 name = short.Name
848 } else {
849 set := cmd.InheritedFlags()
850 if short = set.ShorthandLookup(name); short != nil {
851 name = short.Name
852 } else {
853 return nil
854 }
855 }
856 }
857 return cmd.Flag(name)
858 }
859
860
861
862
863
864 func CompDebug(msg string, printToStdErr bool) {
865 msg = fmt.Sprintf("[Debug] %s", msg)
866
867
868
869 if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" {
870 f, err := os.OpenFile(path,
871 os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
872 if err == nil {
873 defer f.Close()
874 WriteStringAndCheck(f, msg)
875 }
876 }
877
878 if printToStdErr {
879
880 fmt.Fprint(os.Stderr, msg)
881 }
882 }
883
884
885
886
887
888 func CompDebugln(msg string, printToStdErr bool) {
889 CompDebug(fmt.Sprintf("%s\n", msg), printToStdErr)
890 }
891
892
893 func CompError(msg string) {
894 msg = fmt.Sprintf("[Error] %s", msg)
895 CompDebug(msg, true)
896 }
897
898
899 func CompErrorln(msg string) {
900 CompError(fmt.Sprintf("%s\n", msg))
901 }
902
View as plain text