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 }