github.com/posener/complete/v2@v2.1.0/complete.go (about) 1 package complete 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "strconv" 8 "strings" 9 10 "github.com/posener/complete/v2/install" 11 "github.com/posener/complete/v2/internal/arg" 12 "github.com/posener/complete/v2/internal/tokener" 13 ) 14 15 // Completer is an interface that a command line should implement in order to get bash completion. 16 type Completer interface { 17 // SubCmdList should return the list of all sub commands of the current command. 18 SubCmdList() []string 19 // SubCmdGet should return a sub command of the current command for the given sub command name. 20 SubCmdGet(cmd string) Completer 21 // FlagList should return a list of all the flag names of the current command. The flag names 22 // should not have the dash prefix. 23 FlagList() []string 24 // FlagGet should return completion options for a given flag. It is invoked with the flag name 25 // without the dash prefix. The flag is not promised to be in the command flags. In that case, 26 // this method should return a nil predictor. 27 FlagGet(flag string) Predictor 28 // ArgsGet should return predictor for positional arguments of the command line. 29 ArgsGet() Predictor 30 } 31 32 // Predictor can predict completion options. 33 type Predictor interface { 34 // Predict returns prediction options for a given prefix. The prefix is what currently is typed 35 // as a hint for what to return, but the returned values can have any prefix. The returned 36 // values will be filtered by the prefix when needed regardless. The prefix may be empty which 37 // means that no value was typed. 38 Predict(prefix string) []string 39 } 40 41 // PredictFunc is a function that implements the Predictor interface. 42 type PredictFunc func(prefix string) []string 43 44 func (p PredictFunc) Predict(prefix string) []string { 45 if p == nil { 46 return nil 47 } 48 return p(prefix) 49 } 50 51 var ( 52 getEnv = os.Getenv 53 exit = os.Exit 54 ) 55 56 // Complete the command line arguments for the given command in the case that the program 57 // was invoked with COMP_LINE and COMP_POINT environment variables. In that case it will also 58 // `os.Exit()`. The program name should be provided for installation purposes. 59 func Complete(name string, cmd Completer) { 60 var ( 61 line = getEnv("COMP_LINE") 62 point = getEnv("COMP_POINT") 63 doInstall = getEnv("COMP_INSTALL") == "1" 64 doUninstall = getEnv("COMP_UNINSTALL") == "1" 65 yes = getEnv("COMP_YES") == "1" 66 ) 67 var ( 68 out io.Writer = os.Stdout 69 in io.Reader = os.Stdin 70 ) 71 if doInstall || doUninstall { 72 install.Run(name, doUninstall, yes, out, in) 73 exit(0) 74 return 75 } 76 if line == "" { 77 return 78 } 79 i, err := strconv.Atoi(point) 80 if err != nil { 81 panic("COMP_POINT env should be integer, got: " + point) 82 } 83 if i > len(line) { 84 i = len(line) 85 } 86 87 // Parse the command line up to the completion point. 88 args := arg.Parse(line[:i]) 89 90 // The first word is the current command name. 91 args = args[1:] 92 93 // Run the completion algorithm. 94 options, err := completer{Completer: cmd, args: args}.complete() 95 if err != nil { 96 fmt.Fprintln(out, "\n"+err.Error()) 97 } else { 98 for _, option := range options { 99 fmt.Fprintln(out, option) 100 } 101 } 102 exit(0) 103 } 104 105 type completer struct { 106 Completer 107 args []arg.Arg 108 stack []Completer 109 } 110 111 // compete command with given before and after text. 112 // if the command has sub commands: try to complete only sub commands or help flags. Otherwise 113 // complete flags and positional arguments. 114 func (c completer) complete() ([]string, error) { 115 reset: 116 arg := arg.Arg{} 117 if len(c.args) > 0 { 118 arg = c.args[0] 119 } 120 switch { 121 case len(c.SubCmdList()) == 0: 122 // No sub commands, parse flags and positional arguments. 123 return c.suggestLeafCommandOptions(), nil 124 125 // case !arg.Completed && arg.IsFlag(): 126 // Suggest help flags for command 127 // return []string{helpFlag(arg.Text)}, nil 128 129 case !arg.Completed: 130 // Currently typing a sub command. 131 return c.suggestSubCommands(arg.Text), nil 132 133 case c.SubCmdGet(arg.Text) != nil: 134 // Sub command completed, look into that sub command completion. 135 // Set the complete command to the requested sub command, and the before text to all the text 136 // after the command name and rerun the complete algorithm with the new sub command. 137 c.stack = append([]Completer{c.Completer}, c.stack...) 138 c.Completer = c.SubCmdGet(arg.Text) 139 c.args = c.args[1:] 140 goto reset 141 142 default: 143 144 // Sub command is unknown... 145 return nil, fmt.Errorf("unknown subcommand: %s", arg.Text) 146 } 147 } 148 149 func (c completer) suggestSubCommands(prefix string) []string { 150 if len(prefix) > 0 && prefix[0] == '-' { 151 help, _ := helpFlag(prefix) 152 return []string{help} 153 } 154 subs := c.SubCmdList() 155 return suggest("", prefix, func(prefix string) []string { 156 var options []string 157 for _, sub := range subs { 158 if strings.HasPrefix(sub, prefix) { 159 options = append(options, sub) 160 } 161 } 162 return options 163 }) 164 } 165 166 func (c completer) suggestLeafCommandOptions() (options []string) { 167 arg, before := arg.Arg{}, arg.Arg{} 168 if len(c.args) > 0 { 169 arg = c.args[len(c.args)-1] 170 } 171 if len(c.args) > 1 { 172 before = c.args[len(c.args)-2] 173 } 174 175 if !arg.Completed { 176 // Complete value being typed. 177 if arg.HasValue { 178 // Complete value of current flag. 179 if arg.HasFlag { 180 return c.suggestFlagValue(arg.Flag, arg.Value) 181 } 182 // Complete value of flag in a previous argument. 183 if before.HasFlag && !before.HasValue { 184 return c.suggestFlagValue(before.Flag, arg.Value) 185 } 186 } 187 188 // A value with no flag. Suggest positional argument. 189 if !arg.HasValue { 190 options = c.suggestFlag(arg.Dashes, arg.Flag) 191 } 192 if !arg.HasFlag { 193 options = append(options, c.suggestArgsValue(arg.Value)...) 194 } 195 // Suggest flag according to prefix. 196 return options 197 } 198 199 // Has a value that was already completed. Suggest all flags and positional arguments. 200 if arg.HasValue { 201 options = c.suggestFlag(arg.Dashes, "") 202 if !arg.HasFlag { 203 options = append(options, c.suggestArgsValue("")...) 204 } 205 return options 206 } 207 // A flag without a value. Suggest a value or suggest any flag. 208 options = c.suggestFlagValue(arg.Flag, "") 209 if len(options) > 0 { 210 return options 211 } 212 return c.suggestFlag("", "") 213 } 214 215 func (c completer) suggestFlag(dashes, prefix string) []string { 216 if dashes == "" { 217 dashes = "-" 218 } 219 return suggest(dashes, prefix, func(prefix string) []string { 220 var options []string 221 c.iterateStack(func(cmd Completer) { 222 // Suggest all flags with the given prefix. 223 for _, name := range cmd.FlagList() { 224 if strings.HasPrefix(name, prefix) { 225 options = append(options, dashes+name) 226 } 227 } 228 }) 229 return options 230 }) 231 } 232 233 func (c completer) suggestFlagValue(flagName, prefix string) []string { 234 var options []string 235 c.iterateStack(func(cmd Completer) { 236 if len(options) == 0 { 237 if p := cmd.FlagGet(flagName); p != nil { 238 options = p.Predict(prefix) 239 } 240 } 241 }) 242 return filterByPrefix(prefix, options...) 243 } 244 245 func (c completer) suggestArgsValue(prefix string) []string { 246 var options []string 247 c.iterateStack(func(cmd Completer) { 248 if len(options) == 0 { 249 if p := cmd.ArgsGet(); p != nil { 250 options = p.Predict(prefix) 251 } 252 } 253 }) 254 return filterByPrefix(prefix, options...) 255 } 256 257 func (c completer) iterateStack(f func(Completer)) { 258 for _, cmd := range append([]Completer{c.Completer}, c.stack...) { 259 f(cmd) 260 } 261 } 262 263 func suggest(dashes, prefix string, collect func(prefix string) []string) []string { 264 options := collect(prefix) 265 help, helpMatched := helpFlag(dashes + prefix) 266 // In case that something matched: 267 if len(options) > 0 { 268 if strings.HasPrefix(help, dashes+prefix) { 269 options = append(options, help) 270 } 271 return options 272 } 273 274 if helpMatched { 275 return []string{help} 276 } 277 278 // Nothing matched. 279 options = collect("") 280 help, _ = helpFlag(dashes) 281 return append(options, help) 282 } 283 284 func filterByPrefix(prefix string, options ...string) []string { 285 var filtered []string 286 for _, option := range options { 287 if fixed, ok := hasPrefix(option, prefix); ok { 288 filtered = append(filtered, fixed) 289 } 290 } 291 if len(filtered) > 0 { 292 return filtered 293 } 294 return options 295 } 296 297 // hasPrefix checks if s has the give prefix. It disregards quotes and escaped spaces, and return 298 // s in the form of the given prefix. 299 func hasPrefix(s, prefix string) (string, bool) { 300 var ( 301 token tokener.Tokener 302 si, pi int 303 ) 304 for ; pi < len(prefix); pi++ { 305 token.Visit(prefix[pi]) 306 lastQuote := !token.Escaped() && (prefix[pi] == '"' || prefix[pi] == '\'') 307 if lastQuote { 308 continue 309 } 310 if si == len(s) { 311 break 312 } 313 if s[si] == ' ' && !token.Quoted() && token.Escaped() { 314 s = s[:si] + "\\" + s[si:] 315 } 316 if s[si] != prefix[pi] { 317 return "", false 318 } 319 si++ 320 } 321 322 if pi < len(prefix) { 323 return "", false 324 } 325 326 for ; si < len(s); si++ { 327 token.Visit(s[si]) 328 } 329 330 return token.Closed(), true 331 } 332 333 // helpFlag returns either "-h", "-help" or "--help". 334 func helpFlag(prefix string) (string, bool) { 335 if prefix == "" || prefix == "-" || prefix == "-h" { 336 return "-h", true 337 } 338 if strings.HasPrefix("--help", prefix) { 339 return "--help", true 340 } 341 if strings.HasPrefix(prefix, "--") { 342 return "--help", false 343 } 344 return "-help", false 345 }