github.com/DaAlbrecht/cf-cli@v0.0.0-20231128151943-1fe19bb400b9/util/command_parser/command_parser.go (about)

     1  package command_parser
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"reflect"
     8  	"strings"
     9  
    10  	"code.cloudfoundry.org/cli/cf/cmd"
    11  	"code.cloudfoundry.org/cli/command"
    12  	"code.cloudfoundry.org/cli/command/common"
    13  	"code.cloudfoundry.org/cli/command/flag"
    14  	"code.cloudfoundry.org/cli/command/translatableerror"
    15  	"code.cloudfoundry.org/cli/util/configv3"
    16  	"code.cloudfoundry.org/cli/util/ui"
    17  	"github.com/jessevdk/go-flags"
    18  	log "github.com/sirupsen/logrus"
    19  	"golang.org/x/crypto/ssh"
    20  )
    21  
    22  var ErrFailed = errors.New("command failed")
    23  var ParseErr = errors.New("incorrect type for arg")
    24  
    25  type UI interface {
    26  	DisplayError(err error)
    27  	DisplayWarning(template string, templateValues ...map[string]interface{})
    28  	DisplayText(template string, templateValues ...map[string]interface{})
    29  	FlushDeferred()
    30  }
    31  
    32  type DisplayUsage interface {
    33  	DisplayUsage()
    34  }
    35  
    36  type TriggerLegacyMain interface {
    37  	LegacyMain()
    38  	error
    39  }
    40  
    41  type CommandParser struct {
    42  	Config *configv3.Config
    43  	UI     *ui.UI
    44  }
    45  
    46  func NewCommandParser(config *configv3.Config) (CommandParser, error) {
    47  	return CommandParser{Config: config}, nil
    48  }
    49  
    50  func (p *CommandParser) ParseCommandFromArgs(ui *ui.UI, args []string) (int, error) {
    51  	p.UI = ui
    52  	return p.parse(args, &common.Commands)
    53  }
    54  
    55  func (p *CommandParser) executionWrapper(cmd flags.Commander, args []string) error {
    56  	cfConfig := p.Config
    57  	cfConfig.Flags = configv3.FlagOverride{
    58  		Verbose: common.Commands.VerboseOrVersion,
    59  	}
    60  	defer p.UI.FlushDeferred()
    61  
    62  	err := preventExtraArgs(args)
    63  	if err != nil {
    64  		return p.handleError(err)
    65  	}
    66  
    67  	err = cfConfig.CreatePluginHome()
    68  	if err != nil {
    69  		return p.handleError(err)
    70  	}
    71  
    72  	defer func() {
    73  		configWriteErr := cfConfig.WriteConfig()
    74  		if configWriteErr != nil {
    75  			fmt.Fprintf(os.Stderr, "Error writing config: %s", configWriteErr.Error())
    76  		}
    77  	}()
    78  
    79  	if extendedCmd, ok := cmd.(command.ExtendedCommander); ok {
    80  		log.SetOutput(os.Stderr)
    81  		log.SetLevel(log.Level(cfConfig.LogLevel()))
    82  
    83  		err = extendedCmd.Setup(cfConfig, p.UI)
    84  		if err != nil {
    85  			return p.handleError(err)
    86  		}
    87  
    88  		err = extendedCmd.Execute(args)
    89  		return p.handleError(err)
    90  	}
    91  
    92  	return fmt.Errorf("command does not conform to ExtendedCommander")
    93  }
    94  
    95  func (p *CommandParser) handleError(passedErr error) error {
    96  	if passedErr == nil {
    97  		return nil
    98  	}
    99  
   100  	translatedErr := translatableerror.ConvertToTranslatableError(passedErr)
   101  
   102  	switch typedErr := translatedErr.(type) {
   103  	case translatableerror.V3V2SwitchError:
   104  		log.Info("Received a V3V2SwitchError - switch to the V2 version of the command")
   105  		return passedErr
   106  	case TriggerLegacyMain:
   107  		if typedErr.Error() != "" {
   108  			p.UI.DisplayWarning("")
   109  			p.UI.DisplayWarning(typedErr.Error())
   110  		}
   111  		cmd.Main(os.Getenv("CF_TRACE"), os.Args)
   112  	case *ssh.ExitError:
   113  		exitStatus := typedErr.ExitStatus()
   114  		if sig := typedErr.Signal(); sig != "" {
   115  			p.UI.DisplayText("Process terminated by signal: {{.Signal}}. Exited with {{.ExitCode}}", map[string]interface{}{
   116  				"Signal":   sig,
   117  				"ExitCode": exitStatus,
   118  			})
   119  		}
   120  		return passedErr
   121  	case translatableerror.CurlExit22Error:
   122  		p.UI.DisplayError(translatedErr)
   123  		return passedErr
   124  	}
   125  
   126  	p.UI.DisplayError(translatedErr)
   127  
   128  	if _, ok := translatedErr.(DisplayUsage); ok {
   129  		return ParseErr
   130  	}
   131  
   132  	return ErrFailed
   133  }
   134  
   135  func (p *CommandParser) handleFlagErrorAndCommandHelp(flagErr *flags.Error, flagsParser *flags.Parser, extraArgs []string, originalArgs []string, commandList interface{}) (int, error) {
   136  	switch flagErr.Type {
   137  	case flags.ErrHelp, flags.ErrUnknownFlag, flags.ErrExpectedArgument, flags.ErrInvalidChoice:
   138  		_, commandExists := reflect.TypeOf(common.Commands).FieldByNameFunc(
   139  			func(fieldName string) bool {
   140  				field, _ := reflect.TypeOf(common.Commands).FieldByName(fieldName)
   141  				return flagsParser.Active != nil && flagsParser.Active.Name == field.Tag.Get("command")
   142  			},
   143  		)
   144  
   145  		var helpExitCode int
   146  		var err error
   147  		if commandExists && flagErr.Type == flags.ErrUnknownFlag && (flagsParser.Active.Name == "set-env" || flagsParser.Active.Name == "v3-set-env") {
   148  			newArgs := []string{}
   149  			for _, arg := range originalArgs {
   150  				if arg[0] == '-' {
   151  					newArgs = append(newArgs, fmt.Sprintf("%s%s", flag.WorkAroundPrefix, arg))
   152  				} else {
   153  					newArgs = append(newArgs, arg)
   154  				}
   155  			}
   156  			return p.parse(newArgs, commandList)
   157  		}
   158  
   159  		if flagErr.Type == flags.ErrUnknownFlag || flagErr.Type == flags.ErrExpectedArgument || flagErr.Type == flags.ErrInvalidChoice {
   160  			fmt.Fprintf(os.Stderr, "Incorrect Usage: %s\n\n", flagErr.Error())
   161  		}
   162  
   163  		if commandExists {
   164  			helpExitCode, err = p.parse([]string{"help", flagsParser.Active.Name}, commandList)
   165  		} else {
   166  			switch len(extraArgs) {
   167  			case 0:
   168  				helpExitCode, err = p.parse([]string{"help"}, commandList)
   169  			case 1:
   170  				if !isOption(extraArgs[0]) || (len(originalArgs) > 1 && extraArgs[0] == "-a") {
   171  					helpExitCode, err = p.parse([]string{"help", extraArgs[0]}, commandList)
   172  				} else {
   173  					helpExitCode, err = p.parse([]string{"help"}, commandList)
   174  				}
   175  			default:
   176  				if isCommand(extraArgs[0]) {
   177  					helpExitCode, err = p.parse([]string{"help", extraArgs[0]}, commandList)
   178  				} else {
   179  					helpExitCode, err = p.parse(extraArgs[1:], commandList)
   180  				}
   181  			}
   182  		}
   183  
   184  		if flagErr.Type == flags.ErrHelp && helpExitCode == 0 {
   185  			return 0, nil
   186  		} else {
   187  			return 1, err
   188  		}
   189  
   190  	case flags.ErrRequired, flags.ErrMarshal:
   191  		fmt.Fprintf(os.Stderr, "Incorrect Usage: %s\n\n", flagErr.Error())
   192  		_, err := p.parse([]string{"help", originalArgs[0]}, commandList)
   193  		return 1, err
   194  
   195  	case flags.ErrUnknownCommand:
   196  		if containsHelpFlag(originalArgs) {
   197  			return p.parse([]string{"help", originalArgs[0]}, commandList)
   198  		} else {
   199  			return 0, UnknownCommandError{CommandName: originalArgs[0]}
   200  		}
   201  
   202  	case flags.ErrCommandRequired:
   203  		if common.Commands.VerboseOrVersion {
   204  			return p.parse([]string{"version"}, commandList)
   205  		} else {
   206  			return p.parse([]string{"help"}, commandList)
   207  		}
   208  
   209  	default:
   210  		fmt.Fprintf(os.Stderr, "Unexpected flag error\ntype: %s\nmessage: %s\n", flagErr.Type, flagErr.Error())
   211  	}
   212  
   213  	return 0, nil
   214  }
   215  
   216  func (p *CommandParser) parse(args []string, commandList interface{}) (int, error) {
   217  	flagsParser := flags.NewParser(commandList, flags.HelpFlag)
   218  	flagsParser.CommandHandler = p.executionWrapper
   219  	extraArgs, err := flagsParser.ParseArgs(args)
   220  	if err == nil {
   221  		return 0, nil
   222  	} else if _, ok := err.(translatableerror.V3V2SwitchError); ok {
   223  		return 1, err
   224  	} else if flagErr, ok := err.(*flags.Error); ok {
   225  		return p.handleFlagErrorAndCommandHelp(flagErr, flagsParser, extraArgs, args, commandList)
   226  	} else if err == ErrFailed {
   227  		return 1, nil
   228  	} else if err == ParseErr {
   229  		fmt.Println()
   230  		p.parse([]string{"help", args[0]}, commandList) //nolint: errcheck
   231  		return 1, err
   232  	} else if exitError, ok := err.(*ssh.ExitError); ok {
   233  		return exitError.ExitStatus(), nil
   234  	} else if curlError, ok := err.(translatableerror.CurlExit22Error); ok {
   235  		return 22, curlError
   236  	}
   237  
   238  	fmt.Fprintf(os.Stderr, "Unexpected error: %s\n", err.Error())
   239  	return 1, nil
   240  }
   241  
   242  func containsHelpFlag(args []string) bool {
   243  	for _, arg := range args {
   244  		if arg == "-h" || arg == "--help" || arg == "--h" {
   245  			return true
   246  		}
   247  	}
   248  	return false
   249  }
   250  
   251  func isCommand(s string) bool {
   252  	_, found := reflect.TypeOf(common.Commands).FieldByNameFunc(
   253  		func(fieldName string) bool {
   254  			field, _ := reflect.TypeOf(common.Commands).FieldByName(fieldName)
   255  			return s == field.Tag.Get("command") || s == field.Tag.Get("alias")
   256  		})
   257  
   258  	return found
   259  }
   260  
   261  func isOption(s string) bool {
   262  	return strings.HasPrefix(s, "-")
   263  }