github.com/clusterize-io/tusk@v0.6.3-0.20211001020217-cfe8a8cd0d4a/appcli/app.go (about)

     1  package appcli
     2  
     3  import (
     4  	"errors"
     5  	"io/ioutil"
     6  	"os"
     7  	"sort"
     8  
     9  	"github.com/urfave/cli"
    10  
    11  	"github.com/clusterize-io/tusk/runner"
    12  )
    13  
    14  // newBaseApp creates a basic cli.App with top-level flags.
    15  func newBaseApp() *cli.App {
    16  	app := cli.NewApp()
    17  	app.Usage = "the modern task runner"
    18  	app.HideVersion = true
    19  	app.HideHelp = true
    20  	app.EnableBashCompletion = true
    21  	app.UseShortOptionHandling = true
    22  	app.ExitErrHandler = func(*cli.Context, error) {}
    23  
    24  	app.Flags = append(app.Flags,
    25  		cli.BoolFlag{
    26  			Name:  "h, help",
    27  			Usage: "Show help and exit",
    28  		},
    29  		cli.StringFlag{
    30  			Name:  "f, file",
    31  			Usage: "Set `file` to use as the config file",
    32  		},
    33  		cli.StringFlag{
    34  			Name:  "install-completion",
    35  			Usage: "Install tab completion for a `shell`",
    36  		},
    37  		cli.StringFlag{
    38  			Name:  "uninstall-completion",
    39  			Usage: "Uninstall tab completion for a `shell`",
    40  		},
    41  		cli.BoolFlag{
    42  			Name:  "q, quiet",
    43  			Usage: "Only print command output and application errors",
    44  		},
    45  		cli.BoolFlag{
    46  			Name:  "s, silent",
    47  			Usage: "Print no output",
    48  		},
    49  		cli.BoolFlag{
    50  			Name:  "v, verbose",
    51  			Usage: "Print verbose output",
    52  		},
    53  		cli.BoolFlag{
    54  			Name:  "V, version",
    55  			Usage: "Print version and exit",
    56  		},
    57  	)
    58  
    59  	sort.Sort(cli.FlagsByName(app.Flags))
    60  	return app
    61  }
    62  
    63  // newSilentApp creates a cli.App that will never print to stderr / stdout.
    64  func newSilentApp() *cli.App {
    65  	app := newBaseApp()
    66  	app.Writer = ioutil.Discard
    67  	app.ErrWriter = ioutil.Discard
    68  	app.CommandNotFound = func(c *cli.Context, command string) {}
    69  	return app
    70  }
    71  
    72  // newMetaApp creates a cli.App containing metadata, which can parse flags.
    73  func newMetaApp(cfgText []byte) (*cli.App, error) {
    74  	cfg, err := runner.Parse(cfgText)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	app := newSilentApp()
    80  	app.Metadata = make(map[string]interface{})
    81  	app.Metadata["tasks"] = make(map[string]*runner.Task)
    82  	app.Metadata["argsPassed"] = []string{}
    83  	app.Metadata["flagsPassed"] = make(map[string]string)
    84  
    85  	if err := addTasks(app, nil, cfg, createMetadataBuildCommand); err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	return app, nil
    90  }
    91  
    92  // NewApp creates a cli.App that executes tasks.
    93  func NewApp(args []string, meta *runner.Metadata) (*cli.App, error) {
    94  	metaApp, err := newMetaApp(meta.CfgText)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	if rerr := metaApp.Run(args); rerr != nil {
   100  		return nil, rerr
   101  	}
   102  
   103  	var taskName string
   104  	command, ok := metaApp.Metadata["command"].(*cli.Command)
   105  	if ok {
   106  		taskName = command.Name
   107  	}
   108  
   109  	argsPassed, flagsPassed, err := getPassedValues(metaApp)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	cfg, err := runner.ParseComplete(meta, taskName, argsPassed, flagsPassed)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	app := newBaseApp()
   120  	if cfg.Name != "" {
   121  		app.Name = cfg.Name
   122  		app.HelpName = cfg.Name
   123  	}
   124  	if cfg.Usage != "" {
   125  		app.Usage = cfg.Usage
   126  	}
   127  
   128  	if err := addTasks(app, meta, cfg, createExecuteCommand); err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	copyFlags(app, metaApp)
   133  
   134  	app.BashComplete = createDefaultComplete(os.Stdout, app)
   135  	for i := range app.Commands {
   136  		cmd := &app.Commands[i]
   137  		cmd.BashComplete = createCommandComplete(os.Stdout, cmd, cfg)
   138  	}
   139  
   140  	return app, nil
   141  }
   142  
   143  // getPassedValues returns the args and flags passed by command line.
   144  func getPassedValues(app *cli.App) (args []string, flags map[string]string, err error) {
   145  	argsPassed, ok := app.Metadata["argsPassed"].([]string)
   146  	if !ok {
   147  		return nil, nil, errors.New("could not read args from metadata")
   148  	}
   149  	flagsPassed, ok := app.Metadata["flagsPassed"].(map[string]string)
   150  	if !ok {
   151  		return nil, nil, errors.New("could not read flags from metadata")
   152  	}
   153  
   154  	return argsPassed, flagsPassed, nil
   155  }
   156  
   157  // GetConfigMetadata returns a metadata object based on global options passed.
   158  func GetConfigMetadata(args []string) (*runner.Metadata, error) {
   159  	var err error
   160  	app := newSilentApp()
   161  	metadata := runner.NewMetadata()
   162  
   163  	app.Action = func(c *cli.Context) error {
   164  		// To prevent app from exiting, app.Action must return nil on error.
   165  		// The enclosing function will still return the error.
   166  		err = metadata.Set(c)
   167  		return nil
   168  	}
   169  
   170  	if runErr := populateMetadata(app, args); runErr != nil {
   171  		return nil, runErr
   172  	}
   173  
   174  	return metadata, err
   175  }
   176  
   177  // populateMetadata runs the app to populate the metadata struct.
   178  func populateMetadata(app *cli.App, args []string) error {
   179  	args = removeCompletionArg(args)
   180  
   181  	if err := app.Run(args); err != nil {
   182  		// Ignore flags without arguments during metadata creation
   183  		if isFlagArgumentError(err) {
   184  			return app.Run(args[:len(args)-1])
   185  		}
   186  
   187  		return err
   188  	}
   189  
   190  	return nil
   191  }