github.com/clusterize-io/tusk@v0.6.3-0.20211001020217-cfe8a8cd0d4a/appcli/completion.go (about) 1 package appcli 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "strings" 8 9 "github.com/clusterize-io/tusk/runner" 10 "github.com/urfave/cli" 11 ) 12 13 // completionFlag is the flag passed when performing shell completions. 14 var completionFlag = "--" + cli.BashCompletionFlag.GetName() 15 16 // IsCompleting returns whether tab-completion is currently occurring. 17 func IsCompleting(args []string) bool { 18 return args[len(args)-1] == completionFlag 19 } 20 21 // context represents the subset of *cli.Context required for flag completion. 22 type context interface { 23 // IsSet checks if a flag was already set, meaning we no longer need to 24 // complete it. 25 IsSet(string) bool 26 // NArg is the number of non-flag arguments. This is used to determine if a 27 // sub command is being called. 28 NArg() int 29 } 30 31 // createDefaultComplete prints the completion metadata for the top-level app. 32 // The metadata includes the completion type followed by a list of options. 33 // The available completion types are "normal" and "file". Normal will return 34 // tasks and flags, while file allows completion engines to use system files. 35 func createDefaultComplete(w io.Writer, app *cli.App) func(c *cli.Context) { 36 return func(c *cli.Context) { 37 defaultComplete(w, c, app) 38 } 39 } 40 41 func defaultComplete(w io.Writer, c context, app *cli.App) { 42 // If there's an arg, but we're not using command-completion, it's a user 43 // error. There's nothing to complete. 44 if c.NArg() > 0 { 45 return 46 } 47 48 trailingArg := os.Args[len(os.Args)-2] 49 if isCompletingFlagArg(app.Flags, trailingArg) { 50 fmt.Fprintln(w, "file") 51 return 52 } 53 54 fmt.Fprintln(w, "normal") 55 for i := range app.Commands { 56 printCommand(w, &app.Commands[i]) 57 } 58 for _, flag := range app.Flags { 59 printFlag(w, c, flag) 60 } 61 } 62 63 // createCommandComplete prints the completion metadata for a cli command. 64 // The metadata includes the completion type followed by a list of options. 65 // The available completion types are "normal" and "file". Normal will return 66 // task-specific flags, while file allows completion engines to use system files. 67 func createCommandComplete(w io.Writer, command *cli.Command, cfg *runner.Config) func(c *cli.Context) { 68 return func(c *cli.Context) { 69 commandComplete(w, c, command, cfg) 70 } 71 } 72 73 func commandComplete(w io.Writer, c context, command *cli.Command, cfg *runner.Config) { 74 t := cfg.Tasks[command.Name] 75 trailingArg := os.Args[len(os.Args)-2] 76 77 if isCompletingFlagArg(command.Flags, trailingArg) { 78 printCompletingFlagArg(w, t, cfg, trailingArg) 79 return 80 } 81 82 if c.NArg()+1 <= len(t.Args) { 83 fmt.Fprintln(w, "task-args") 84 arg := t.Args[c.NArg()] 85 for _, value := range arg.ValuesAllowed { 86 fmt.Fprintln(w, value) 87 } 88 } else { 89 fmt.Fprintln(w, "task-no-args") 90 } 91 for _, flag := range command.Flags { 92 printFlag(w, c, flag) 93 } 94 } 95 96 func printCompletingFlagArg(w io.Writer, t *runner.Task, cfg *runner.Config, trailingArg string) { 97 options, err := runner.FindAllOptions(t, cfg) 98 if err != nil { 99 return 100 } 101 102 opt, ok := getOptionFlag(trailingArg, options) 103 if !ok { 104 return 105 } 106 107 if len(opt.ValuesAllowed) > 0 { 108 fmt.Fprintln(w, "value") 109 for _, value := range opt.ValuesAllowed { 110 fmt.Fprintln(w, value) 111 } 112 return 113 } 114 115 // Default to file completion 116 fmt.Fprintln(w, "file") 117 } 118 119 func getOptionFlag(flag string, options []*runner.Option) (*runner.Option, bool) { 120 flagName := getFlagName(flag) 121 for _, opt := range options { 122 if flagName == opt.Name || flagName == opt.Short { 123 return opt, true 124 } 125 } 126 127 return nil, false 128 } 129 130 func printCommand(w io.Writer, command *cli.Command) { 131 if command.Hidden { 132 return 133 } 134 135 name := strings.ReplaceAll(command.Name, ":", `\:`) 136 137 if command.Usage == "" { 138 fmt.Fprintln(w, name) 139 return 140 } 141 142 fmt.Fprintf( 143 w, 144 "%s:%s\n", 145 name, 146 strings.ReplaceAll(command.Usage, "\n", ""), 147 ) 148 } 149 150 func printFlag(w io.Writer, c context, flag cli.Flag) { 151 values := strings.Split(flag.GetName(), ", ") 152 for _, value := range values { 153 if len(value) == 1 || c.IsSet(value) { 154 continue 155 } 156 157 name := strings.ReplaceAll(value, ":", `\:`) 158 159 desc := getDescription(flag) 160 if desc == "" { 161 fmt.Fprintln(w, "--"+name) 162 return 163 } 164 165 fmt.Fprintf( 166 w, 167 "--%s:%s\n", 168 name, 169 strings.ReplaceAll(desc, "\n", ""), 170 ) 171 } 172 } 173 174 func getDescription(flag cli.Flag) string { 175 return strings.SplitN(flag.String(), "\t", 2)[1] 176 } 177 178 func removeCompletionArg(args []string) []string { 179 var output []string 180 for _, arg := range args { 181 if arg != completionFlag { 182 output = append(output, arg) 183 } 184 } 185 186 return output 187 } 188 189 // isCompletingFlagArg returns if the trailing arg is an incomplete flag. 190 func isCompletingFlagArg(flags []cli.Flag, arg string) bool { 191 if !strings.HasPrefix(arg, "-") { 192 return false 193 } 194 195 name := getFlagName(arg) 196 short := !strings.HasPrefix(arg, "--") 197 198 for _, flag := range flags { 199 switch flag.(type) { 200 case cli.BoolFlag, cli.BoolTFlag: 201 continue 202 } 203 204 if flagMatchesName(flag, name, short) { 205 return true 206 } 207 } 208 209 return false 210 } 211 212 func flagMatchesName(flag cli.Flag, name string, short bool) bool { 213 for _, candidate := range strings.Split(flag.GetName(), ", ") { 214 if len(candidate) == 1 && !short { 215 continue 216 } 217 218 if name == candidate { 219 return true 220 } 221 } 222 223 return false 224 } 225 226 func getFlagName(flag string) string { 227 if strings.HasPrefix(flag, "--") { 228 return flag[2:] 229 } 230 231 return flag[len(flag)-1:] 232 } 233 234 func isFlagArgumentError(err error) bool { 235 return strings.HasPrefix(err.Error(), "flag needs an argument") 236 }