tractor.dev/toolkit-go@v0.0.0-20241010005851-214d91207d07/engine/cli/framework.go (about) 1 package cli 2 3 import ( 4 "context" 5 "flag" 6 "fmt" 7 "io" 8 "os" 9 "reflect" 10 "strconv" 11 ) 12 13 // Initializer is a hook to allow units to customize the root Command. 14 type Initializer interface { 15 InitializeCLI(root *Command) 16 } 17 18 type Preprocessor interface { 19 PreprocessCLI(args []string) []string 20 } 21 22 // Runner is a unit that takes over the program entrypoint. 23 type Runner interface { 24 Run(ctx context.Context) error 25 } 26 27 // Framework manages a root command, allowing Initializers 28 // to modify it, which by default runs a DefaultRunner. 29 type Framework struct { 30 DefaultRunner Runner 31 Initializers []Initializer 32 Preprocessors []Preprocessor 33 Root *Command 34 } 35 36 // Initialize sets up a Root command that simply runs the 37 // DefaultRunner, and also runs any Initializers. 38 func (f *Framework) Initialize() { 39 f.Root = &Command{} 40 for _, i := range f.Initializers { 41 i.InitializeCLI(f.Root) 42 } 43 } 44 45 // Run executes the root command with os.Args and STDIO. 46 func (f *Framework) Run(ctx context.Context) error { 47 args := os.Args[1:] 48 for _, p := range f.Preprocessors { 49 args = p.PreprocessCLI(args) 50 } 51 return Execute(ContextWithIO(ctx, os.Stdin, os.Stdout, os.Stderr), f.Root, args) 52 } 53 54 // Execute takes a root Command plus arguments, finds the Command to run, 55 // parses flags, checks for expected arguments, and runs the Command. 56 // It also adds a version flag if the root Command has Version set. 57 func Execute(ctx context.Context, root *Command, args []string) error { 58 var ( 59 stdout io.Writer = os.Stdout 60 stderr io.Writer = os.Stderr 61 ioctx *Context 62 ) 63 if c, ok := ctx.(*Context); ok { 64 stdout = c 65 stderr = c 66 ioctx = c 67 } else { 68 ioctx = ContextWithIO(ctx, os.Stdin, stdout, stderr) 69 } 70 71 var showVersion bool 72 if root.Version != "" { 73 root.Flags().BoolVar(&showVersion, "v", false, "show version") 74 } 75 76 cmd, n := root.Find(args) 77 f := cmd.Flags() 78 if f != nil { 79 if err := f.Parse(args[n:]); err != nil { 80 if err == flag.ErrHelp { 81 return (&CommandHelp{cmd}).WriteHelp(stderr) 82 } 83 return err 84 } 85 } 86 87 if showVersion { 88 fmt.Fprintln(stdout, root.Version) 89 return nil 90 } 91 92 if cmd.Args != nil { 93 if err := cmd.Args(cmd, f.Args()); err != nil { 94 return err 95 } 96 } 97 98 if cmd.Run == nil { 99 (&CommandHelp{cmd}).WriteHelp(stderr) 100 return nil 101 } 102 103 cmd.Run(ioctx, f.Args()) 104 return nil 105 } 106 107 // Export wraps a function as a command. 108 func Export(fn interface{}, use string) *Command { 109 rv := reflect.ValueOf(fn) 110 t := rv.Type() 111 if t.Kind() != reflect.Func { 112 panic("can only export funcs") 113 } 114 return &Command{ 115 Usage: use, 116 Args: ExactArgs(t.NumIn()), 117 Run: func(ctx *Context, args []string) { 118 var in []reflect.Value 119 for n := 0; n < t.NumIn(); n++ { 120 switch t.In(n).Kind() { 121 case reflect.String: 122 in = append(in, reflect.ValueOf(args[n])) 123 case reflect.Int: 124 arg, err := strconv.Atoi(args[n]) 125 if err != nil { 126 panic(err) 127 } 128 in = append(in, reflect.ValueOf(arg)) 129 default: 130 panic("argument kind not supported: " + t.In(n).Kind().String()) 131 } 132 } 133 rv.Call(in) 134 }, 135 } 136 }