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  }