run.go 3.52 KB
Newer Older
1 2 3 4 5 6 7 8
package cli

import (
	"context"
	"fmt"
	"io"
	"os"
	"strings"
9
	"time"
10

tavit ohanian's avatar
tavit ohanian committed
11
	cmds "gitlab.dms3.io/dms3/public/go-dms3-cmds"
12 13
)

dignifiedquire's avatar
dignifiedquire committed
14 15 16 17 18 19 20
// ExitError is the error used when a specific exit code needs to be returned.
type ExitError int

func (e ExitError) Error() string {
	return fmt.Sprintf("exit code %d", int(e))
}

21
// Closer is a helper interface to check if the env supports closing
22 23 24 25 26 27
type Closer interface {
	Close()
}

func Run(ctx context.Context, root *cmds.Command,
	cmdline []string, stdin, stdout, stderr *os.File,
dignifiedquire's avatar
dignifiedquire committed
28
	buildEnv cmds.MakeEnvironment, makeExecutor cmds.MakeExecutor) error {
29 30

	printErr := func(err error) {
Steven Allen's avatar
Steven Allen committed
31
		fmt.Fprintf(stderr, "Error: %s\n", err)
32 33 34 35
	}

	req, errParse := Parse(ctx, cmdline[1:], stdin, root)

36 37 38 39 40
	// Handle the timeout up front.
	var cancel func()
	if timeoutStr, ok := req.Options[cmds.TimeoutOpt]; ok {
		timeout, err := time.ParseDuration(timeoutStr.(string))
		if err != nil {
41
			printErr(err)
dignifiedquire's avatar
dignifiedquire committed
42
			return err
43 44 45 46 47 48 49
		}
		req.Context, cancel = context.WithTimeout(req.Context, timeout)
	} else {
		req.Context, cancel = context.WithCancel(req.Context)
	}
	defer cancel()

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
	// this is a message to tell the user how to get the help text
	printMetaHelp := func(w io.Writer) {
		cmdPath := strings.Join(req.Path, " ")
		fmt.Fprintf(w, "Use '%s %s --help' for information about this command\n", cmdline[0], cmdPath)
	}

	printHelp := func(long bool, w io.Writer) {
		helpFunc := ShortHelp
		if long {
			helpFunc = LongHelp
		}

		var path []string
		if req != nil {
			path = req.Path
		}

67 68 69 70
		if err := helpFunc(cmdline[0], root, path, w); err != nil {
			// This should not happen
			panic(err)
		}
71 72 73 74 75 76
	}

	// BEFORE handling the parse error, if we have enough information
	// AND the user requested help, print it out and exit
	err := HandleHelp(cmdline[0], req, stdout)
	if err == nil {
dignifiedquire's avatar
dignifiedquire committed
77
		return nil
78
	} else if err != ErrNoHelpRequested {
dignifiedquire's avatar
dignifiedquire committed
79
		return err
80 81 82 83 84 85 86 87 88 89 90 91 92 93
	}
	// no help requested, continue.

	// ok now handle parse error (which means cli input was wrong,
	// e.g. incorrect number of args, or nonexistent subcommand)
	if errParse != nil {
		printErr(errParse)

		// this was a user error, print help
		if req != nil && req.Command != nil {
			fmt.Fprintln(stderr) // i need some space
			printHelp(false, stderr)
		}

94
		return errParse
95 96 97 98 99 100 101
	}

	// here we handle the cases where
	// - commands with no Run func are invoked directly.
	// - the main command is invoked.
	if req == nil || req.Command == nil || req.Command.Run == nil {
		printHelp(false, stdout)
dignifiedquire's avatar
dignifiedquire committed
102
		return nil
103 104 105 106
	}

	cmd := req.Command

107
	env, err := buildEnv(req.Context, req)
108 109
	if err != nil {
		printErr(err)
dignifiedquire's avatar
dignifiedquire committed
110
		return err
111 112 113 114 115 116 117 118
	}
	if c, ok := env.(Closer); ok {
		defer c.Close()
	}

	exctr, err := makeExecutor(req, env)
	if err != nil {
		printErr(err)
dignifiedquire's avatar
dignifiedquire committed
119
		return err
120 121 122 123 124 125 126 127 128 129
	}

	encTypeStr, _ := req.Options[cmds.EncLong].(string)
	encType := cmds.EncodingType(encTypeStr)

	// use JSON if text was requested but the command doesn't have a text-encoder
	if _, ok := cmd.Encoders[encType]; encType == cmds.Text && !ok {
		req.Options[cmds.EncLong] = cmds.JSON
	}

Steven Allen's avatar
Steven Allen committed
130
	re, err := NewResponseEmitter(stdout, stderr, req)
Steven Allen's avatar
Steven Allen committed
131 132 133
	if err != nil {
		printErr(err)
		return err
134 135
	}

Steven Allen's avatar
Steven Allen committed
136 137 138 139 140
	// Execute the command.
	err = exctr.Execute(req, re, env)
	// If we get an error here, don't bother reading the status from the
	// response emitter. It may not even be closed.
	if err != nil {
141 142
		printErr(err)

Steven Allen's avatar
Steven Allen committed
143
		if kiterr, ok := err.(*cmds.Error); ok {
144 145
			err = *kiterr
		}
Steven Allen's avatar
Steven Allen committed
146
		if kiterr, ok := err.(cmds.Error); ok && kiterr.Code == cmds.ErrClient {
147 148 149
			printMetaHelp(stderr)
		}

dignifiedquire's avatar
dignifiedquire committed
150
		return err
151 152
	}

Steven Allen's avatar
Steven Allen committed
153 154 155
	if code := re.Status(); code != 0 {
		return ExitError(code)
	}
dignifiedquire's avatar
dignifiedquire committed
156
	return nil
157
}