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  }