github.com/jfrog/jfrog-cli-core/v2@v2.51.0/plugins/components/conversionlayer.go (about)

     1  package components
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/jfrog/gofrog/datastructures"
     9  	"github.com/jfrog/jfrog-cli-core/v2/docs/common"
    10  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    11  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    12  	"github.com/urfave/cli"
    13  )
    14  
    15  func ConvertApp(jfrogApp App) (*cli.App, error) {
    16  	var err error
    17  	app := cli.NewApp()
    18  	app.Name = jfrogApp.Name
    19  	app.Description = jfrogApp.Description
    20  	app.Version = jfrogApp.Version
    21  	app.Commands, err = ConvertAppCommands(jfrogApp)
    22  	if err != nil {
    23  		return nil, err
    24  	}
    25  	// Defaults:
    26  	app.EnableBashCompletion = true
    27  	return app, nil
    28  }
    29  
    30  func ConvertAppCommands(jfrogApp App, commandPrefix ...string) (cmds []cli.Command, err error) {
    31  	cmds, err = convertCommands(jfrogApp.Commands, commandPrefix...)
    32  	if err != nil || len(jfrogApp.Subcommands) == 0 {
    33  		return
    34  	}
    35  	subcommands, err := convertSubcommands(jfrogApp.Subcommands, commandPrefix...)
    36  	if err != nil {
    37  		return
    38  	}
    39  	cmds = append(cmds, subcommands...)
    40  	return
    41  }
    42  
    43  func convertSubcommands(subcommands []Namespace, nameSpaces ...string) ([]cli.Command, error) {
    44  	var converted []cli.Command
    45  	for _, ns := range subcommands {
    46  		nameSpaceCommand := cli.Command{
    47  			Name:     ns.Name,
    48  			Usage:    ns.Description,
    49  			Category: ns.Category,
    50  		}
    51  		nsCommands, err := convertCommands(ns.Commands, append(nameSpaces, ns.Name)...)
    52  		if err != nil {
    53  			return converted, err
    54  		}
    55  		nameSpaceCommand.Subcommands = nsCommands
    56  		converted = append(converted, nameSpaceCommand)
    57  	}
    58  	return converted, nil
    59  }
    60  
    61  func convertCommands(commands []Command, nameSpaces ...string) ([]cli.Command, error) {
    62  	var converted []cli.Command
    63  	for _, cmd := range commands {
    64  		cur, err := convertCommand(cmd, nameSpaces...)
    65  		if err != nil {
    66  			return converted, err
    67  		}
    68  		converted = append(converted, cur)
    69  	}
    70  	return converted, nil
    71  }
    72  
    73  func convertCommand(cmd Command, namespaces ...string) (cli.Command, error) {
    74  	convertedFlags, convertedStringFlags, err := convertFlags(cmd)
    75  	if err != nil {
    76  		return cli.Command{}, err
    77  	}
    78  	cmdUsages, err := createCommandUsages(cmd, convertedStringFlags, namespaces...)
    79  	if err != nil {
    80  		return cli.Command{}, err
    81  	}
    82  	return cli.Command{
    83  		Name:            cmd.Name,
    84  		Flags:           convertedFlags,
    85  		Aliases:         cmd.Aliases,
    86  		Category:        cmd.Category,
    87  		Description:     cmd.Description,
    88  		HelpName:        common.CreateUsage(getCmdUsageString(cmd, namespaces...), cmd.Description, cmdUsages),
    89  		UsageText:       createArgumentsSummary(cmd),
    90  		ArgsUsage:       createEnvVarsSummary(cmd),
    91  		BashComplete:    common.CreateBashCompletionFunc(),
    92  		SkipFlagParsing: cmd.SkipFlagParsing,
    93  		Hidden:          cmd.Hidden,
    94  		// Passing any other interface than 'cli.ActionFunc' will fail the command.
    95  		Action: getActionFunc(cmd),
    96  	}, nil
    97  }
    98  
    99  func removeEmptyValues(slice []string) []string {
   100  	var result []string
   101  	for _, s := range slice {
   102  		if s != "" {
   103  			result = append(result, s)
   104  		}
   105  	}
   106  	return result
   107  }
   108  
   109  // Create the command usage strings that will be shown in the help.
   110  func createCommandUsages(cmd Command, convertedStringFlags map[string]StringFlag, namespaces ...string) (usages []string, err error) {
   111  	// Handle manual usages provided.
   112  	if cmd.UsageOptions != nil {
   113  		for _, manualUsage := range cmd.UsageOptions.Usage {
   114  			usages = append(usages, fmt.Sprintf("%s %s", coreutils.GetCliExecutableName(), manualUsage))
   115  		}
   116  		if cmd.UsageOptions.ReplaceAutoGeneratedUsage {
   117  			return
   118  		}
   119  	}
   120  	// Handle auto generated usages for the command.
   121  	generated, err := generateCommandUsages(getCmdUsageString(cmd, namespaces...), cmd, convertedStringFlags)
   122  	if err != nil {
   123  		return
   124  	}
   125  	usages = append(usages, generated...)
   126  	return
   127  }
   128  
   129  func getCmdUsageString(cmd Command, namespaces ...string) string {
   130  	return strings.Join(append(removeEmptyValues(namespaces), cmd.Name), " ")
   131  }
   132  
   133  // Generated usages are based on the command's flags and arguments:
   134  // <cli-name> <command-name> [command options] --mandatory-opt1=<opt1-value-alias> --mandatory-opt2=<value>... <arg1> [optional-arg2] <arg3>...
   135  func generateCommandUsages(usagePrefix string, cmd Command, convertedStringFlags map[string]StringFlag) (usages []string, err error) {
   136  	argumentsUsageParts, flagReplacements, err := getArgumentsUsageParts(cmd, convertedStringFlags)
   137  	if err != nil {
   138  		return
   139  	}
   140  	if len(argumentsUsageParts) == 0 {
   141  		// No arguments provided.
   142  		usages = append(usages, fmt.Sprintf("%s%s", usagePrefix, getFlagUsagePart(cmd, convertedStringFlags, nil)))
   143  		return
   144  	}
   145  	usages = append(usages, fmt.Sprintf("%s%s%s", usagePrefix, getFlagUsagePart(cmd, convertedStringFlags, nil), argumentsUsageParts[0]))
   146  	if len(argumentsUsageParts) == 1 {
   147  		// No flag replacements, return single usage.
   148  		return
   149  	}
   150  	// Add the usage with the flag replacements.
   151  	usages = append(usages, fmt.Sprintf("%s%s%s", usagePrefix, getFlagUsagePart(cmd, convertedStringFlags, flagReplacements), argumentsUsageParts[1]))
   152  	return
   153  }
   154  
   155  // Get the command usage parts that are related to arguments, if any.
   156  // Mandatory arguments represented as <Arg-Name> and optional arguments represented as [Arg-Name].
   157  // If some arguments have flag replacements, creates two parts: with and without all replacements:
   158  // 1) <arg1> [optional-arg2] <arg3>
   159  // 2) --<optional-flag-replacement-2-3>=<value> <arg1>
   160  func getArgumentsUsageParts(cmd Command, convertedStringFlags map[string]StringFlag) (usageParts []string, flagReplacements *datastructures.Set[string], err error) {
   161  	var usage string
   162  	if usage = getArgsUsagePart(cmd); usage != "" {
   163  		// No replacements arguments usage part. (1)
   164  		usageParts = append(usageParts, usage)
   165  	}
   166  	if usage, flagReplacements, err = getArgsUsagePartWithReplacements(cmd, convertedStringFlags); err != nil {
   167  		return
   168  	}
   169  	if usage != "" {
   170  		// With replacements arguments usage part. (2)
   171  		usageParts = append(usageParts, usage)
   172  	}
   173  	return
   174  }
   175  
   176  func getArgsUsagePart(cmd Command) (usage string) {
   177  	for _, argument := range cmd.Arguments {
   178  		usage += getArgumentUsage(argument)
   179  	}
   180  	return
   181  }
   182  
   183  func getArgumentUsage(argument Argument) string {
   184  	if argument.Optional {
   185  		return fmt.Sprintf(" [%s]", argument.Name)
   186  	}
   187  	return fmt.Sprintf(" <%s>", argument.Name)
   188  }
   189  
   190  func getArgsUsagePartWithReplacements(cmd Command, convertedStringFlags map[string]StringFlag) (usage string, flagReplacements *datastructures.Set[string], err error) {
   191  	flagReplacements = datastructures.MakeSet[string]()
   192  	for _, argument := range cmd.Arguments {
   193  		if argument.ReplaceWithFlag == "" {
   194  			usage += getArgumentUsage(argument)
   195  			continue
   196  		}
   197  		if flagReplacements.Exists(argument.ReplaceWithFlag) {
   198  			// Flag already exists in the replacements, skip. (Multiple arguments can have the same replacement flag)
   199  			continue
   200  		}
   201  		if _, exists := convertedStringFlags[argument.ReplaceWithFlag]; !exists {
   202  			err = fmt.Errorf("command '%s': argument '%s' has a defined replacement flag '%s' that does not exist", cmd.Name, argument.Name, argument.ReplaceWithFlag)
   203  			return
   204  		}
   205  		flagReplacements.Add(argument.ReplaceWithFlag)
   206  	}
   207  	if flagReplacements.Size() == 0 {
   208  		// No replacements, return empty string.
   209  		return "", nil, nil
   210  	}
   211  	for _, flagName := range flagReplacements.ToSlice() {
   212  		usage = getMandatoryFlagUsage(convertedStringFlags[flagName]) + usage
   213  	}
   214  	return
   215  }
   216  
   217  // Get the command usage part that is related to flags, if any.
   218  // If some flags are optional, returns with general prefix `[command options]` followed by the mandatory flags.
   219  // Mandatory flags are returned with their value alias, if provided. --<Name>=<ValueAlias> or --<Name>=<value> if no alias.
   220  func getFlagUsagePart(cmd Command, convertedStringFlags map[string]StringFlag, flagReplacements *datastructures.Set[string]) (usage string) {
   221  	// Calculate flag counts.
   222  	totalFlagCount := len(cmd.Flags)
   223  	if totalFlagCount == 0 {
   224  		return
   225  	}
   226  	mandatoryFlagCount := getMandatoryFlagCount(cmd)
   227  	optionalFlagCount := totalFlagCount - mandatoryFlagCount
   228  	optionalFlagCountUsedAsArgReplacements := 0
   229  	if flagReplacements != nil {
   230  		optionalFlagCountUsedAsArgReplacements = flagReplacements.Size()
   231  	}
   232  	// Add general prefix.
   233  	if optionalFlagCount-optionalFlagCountUsedAsArgReplacements > 0 {
   234  		usage += " [command options]"
   235  	}
   236  	if mandatoryFlagCount == 0 {
   237  		return
   238  	}
   239  	// Add mandatory flags.
   240  	for flagName, flag := range convertedStringFlags {
   241  		if flag.Mandatory {
   242  			valueAlias := "value"
   243  			if flag.HelpValue != "" {
   244  				valueAlias = flag.HelpValue
   245  			}
   246  			usage += fmt.Sprintf(" --%s=<%s>", flagName, valueAlias)
   247  		}
   248  	}
   249  	return
   250  }
   251  
   252  func getMandatoryFlagCount(cmd Command) int {
   253  	count := 0
   254  	for _, flag := range cmd.Flags {
   255  		if flag.IsMandatory() {
   256  			count++
   257  		}
   258  	}
   259  	return count
   260  }
   261  
   262  func getMandatoryFlagUsage(flag StringFlag) string {
   263  	valueAlias := "value"
   264  	if flag.HelpValue != "" {
   265  		valueAlias = flag.HelpValue
   266  	}
   267  	return fmt.Sprintf(" --%s=<%s>", flag.Name, valueAlias)
   268  }
   269  
   270  func createArgumentsSummary(cmd Command) string {
   271  	summary := ""
   272  	for i, argument := range cmd.Arguments {
   273  		if i > 0 {
   274  			summary += "\n"
   275  		}
   276  		optional := ""
   277  		if argument.Optional {
   278  			optional = " [Optional]"
   279  		}
   280  		summary += "\t" + argument.Name + optional + "\n\t\t" + argument.Description + "\n"
   281  	}
   282  	return summary
   283  }
   284  
   285  func createEnvVarsSummary(cmd Command) string {
   286  	var envVarsSummary []string
   287  	for i, env := range cmd.EnvVars {
   288  		summary := ""
   289  		if i > 0 {
   290  			summary += "\n"
   291  		}
   292  		summary += "\t" + env.Name + "\n"
   293  		if env.Default != "" {
   294  			summary += "\t\t[Default: " + env.Default + "]\n"
   295  		}
   296  		summary += "\t\t" + env.Description
   297  		envVarsSummary = append(envVarsSummary, summary)
   298  	}
   299  	return strings.Join(envVarsSummary, "\n")
   300  }
   301  
   302  func convertFlags(cmd Command) ([]cli.Flag, map[string]StringFlag, error) {
   303  	var convertedFlags []cli.Flag
   304  	convertedStringFlags := map[string]StringFlag{}
   305  	for _, flag := range cmd.Flags {
   306  		converted, convertedString, err := convertByType(flag)
   307  		if err != nil {
   308  			return convertedFlags, convertedStringFlags, fmt.Errorf("command '%s': %w", cmd.Name, err)
   309  		}
   310  		if converted != nil {
   311  			convertedFlags = append(convertedFlags, converted)
   312  		}
   313  		if convertedString != nil {
   314  			convertedStringFlags[flag.GetName()] = *convertedString
   315  		}
   316  	}
   317  	return convertedFlags, convertedStringFlags, nil
   318  }
   319  
   320  func convertByType(flag Flag) (cli.Flag, *StringFlag, error) {
   321  	switch actualType := flag.(type) {
   322  	case StringFlag:
   323  		return convertStringFlag(actualType), &actualType, nil
   324  	case BoolFlag:
   325  		return convertBoolFlag(actualType), nil, nil
   326  	}
   327  	return nil, nil, errorutils.CheckErrorf("flag '%s' does not match any known flag type", flag.GetName())
   328  }
   329  
   330  func convertStringFlag(f StringFlag) cli.Flag {
   331  	stringFlag := cli.StringFlag{
   332  		Name:   f.Name,
   333  		Hidden: f.Hidden,
   334  		Usage:  f.Description + "` `",
   335  	}
   336  	// If default is set, add its value and return.
   337  	if f.DefaultValue != "" {
   338  		stringFlag.Usage = fmt.Sprintf("[Default: %s] %s", f.DefaultValue, stringFlag.Usage)
   339  		return stringFlag
   340  	}
   341  	// Otherwise, mark as mandatory/optional accordingly.
   342  	if f.Mandatory {
   343  		stringFlag.Usage = "[Mandatory] " + stringFlag.Usage
   344  	} else {
   345  		stringFlag.Usage = "[Optional] " + stringFlag.Usage
   346  	}
   347  	return stringFlag
   348  }
   349  
   350  func convertBoolFlag(f BoolFlag) cli.Flag {
   351  	if f.DefaultValue {
   352  		return cli.BoolTFlag{
   353  			Name:   f.Name,
   354  			Hidden: f.Hidden,
   355  			Usage:  "[Default: true] " + f.Description + "` `",
   356  		}
   357  	}
   358  	return cli.BoolFlag{
   359  		Name:   f.Name,
   360  		Hidden: f.Hidden,
   361  		Usage:  "[Default: false] " + f.Description + "` `",
   362  	}
   363  }
   364  
   365  // Wrap the base's ActionFunc with our own, while retrieving needed information from the Context.
   366  func getActionFunc(cmd Command) cli.ActionFunc {
   367  	return func(baseContext *cli.Context) error {
   368  		pluginContext, err := ConvertContext(baseContext, cmd.Flags...)
   369  		if err != nil {
   370  			return err
   371  		}
   372  		return cmd.Action(pluginContext)
   373  	}
   374  }
   375  
   376  func ConvertContext(baseContext *cli.Context, flagsToConvert ...Flag) (*Context, error) {
   377  	pluginContext := &Context{
   378  		CommandName:      baseContext.Command.Name,
   379  		Arguments:        baseContext.Args(),
   380  		PrintCommandHelp: getPrintCommandHelpFunc(baseContext),
   381  	}
   382  	return pluginContext, fillFlagMaps(pluginContext, baseContext, flagsToConvert)
   383  }
   384  
   385  func getPrintCommandHelpFunc(c *cli.Context) func(commandName string) error {
   386  	return func(commandName string) error {
   387  		return cli.ShowCommandHelp(c, c.Command.Name)
   388  	}
   389  }
   390  
   391  func fillFlagMaps(c *Context, baseContext *cli.Context, originalFlags []Flag) error {
   392  	c.stringFlags = make(map[string]string)
   393  	c.boolFlags = make(map[string]bool)
   394  
   395  	// Loop over all plugin's known flags.
   396  	for _, flag := range originalFlags {
   397  		if stringFlag, ok := flag.(StringFlag); ok {
   398  			finalValue, err := getValueForStringFlag(stringFlag, baseContext.String(stringFlag.Name))
   399  			if err != nil {
   400  				return err
   401  			}
   402  			c.stringFlags[stringFlag.Name] = finalValue
   403  			continue
   404  		}
   405  
   406  		if boolFlag, ok := flag.(BoolFlag); ok {
   407  			c.boolFlags[boolFlag.Name] = getValueForBoolFlag(boolFlag, baseContext)
   408  		}
   409  	}
   410  	return nil
   411  }
   412  
   413  func getValueForStringFlag(f StringFlag, receivedValue string) (finalValue string, err error) {
   414  	if receivedValue != "" {
   415  		return receivedValue, nil
   416  	}
   417  	// Empty but has a default value defined.
   418  	if f.DefaultValue != "" {
   419  		return f.DefaultValue, nil
   420  	}
   421  	// Empty but mandatory.
   422  	if f.Mandatory {
   423  		return "", errors.New("Mandatory flag '" + f.Name + "' is missing")
   424  	}
   425  	return "", nil
   426  }
   427  
   428  func getValueForBoolFlag(f BoolFlag, baseContext *cli.Context) bool {
   429  	if f.DefaultValue {
   430  		return baseContext.BoolT(f.Name)
   431  	}
   432  	return baseContext.Bool(f.Name)
   433  }