vitess.io/vitess@v0.16.2/go/internal/flag/flag.go (about)

     1  /*
     2  Copyright 2022 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package flag is an internal package to allow us to gracefully transition
    18  // from the standard library's flag package to pflag. See VEP-4 for details.
    19  //
    20  // In general, this package should not be imported or depended on, except in the
    21  // cases of package servenv, and entrypoints in go/cmd. This package WILL be
    22  // deleted after the migration to pflag is completed, without any support for
    23  // compatibility.
    24  package flag
    25  
    26  import (
    27  	goflag "flag"
    28  	"fmt"
    29  	"os"
    30  	"reflect"
    31  	"strings"
    32  
    33  	flag "github.com/spf13/pflag"
    34  )
    35  
    36  // Parse wraps the standard library's flag.Parse to perform some sanity checking
    37  // and issue deprecation warnings in advance of our move to pflag.
    38  //
    39  // It also adjusts the global CommandLine's Usage func to print out flags with
    40  // double-dashes when a user requests the help, attempting to otherwise leave
    41  // the default Usage formatting unchanged.
    42  //
    43  // See VEP-4, phase 1 for details: https://github.com/vitessio/enhancements/blob/c766ea905e55409cddeb666d6073cd2ac4c9783e/veps/vep-4.md#phase-1-preparation
    44  func Parse(fs *flag.FlagSet) {
    45  	preventGlogVFlagFromClobberingVersionFlagShorthand(fs)
    46  	fs.AddGoFlagSet(goflag.CommandLine)
    47  
    48  	if fs.Lookup("help") == nil {
    49  		var help bool
    50  
    51  		if fs.ShorthandLookup("h") == nil {
    52  			fs.BoolVarP(&help, "help", "h", false, "display usage and exit")
    53  		} else {
    54  			fs.BoolVar(&help, "help", false, "display usage and exit")
    55  		}
    56  
    57  		defer func() {
    58  			if help {
    59  				Usage()
    60  				os.Exit(0)
    61  			}
    62  		}()
    63  	}
    64  
    65  	TrickGlog() // see the function doc for why.
    66  
    67  	flag.CommandLine = fs
    68  	flag.Parse()
    69  }
    70  
    71  // IsFlagProvided returns if the given flag has been provided by the user explicitly or not
    72  func IsFlagProvided(name string) bool {
    73  	fl := flag.Lookup(name)
    74  	if fl != nil {
    75  		return fl.Changed
    76  	}
    77  	return false
    78  }
    79  
    80  // TrickGlog tricks glog into understanding that flags have been parsed.
    81  //
    82  // N.B. Do not delete this function. `glog` is a persnickity package and wants
    83  // to insist that you parse flags before doing any logging, which is a totally
    84  // reasonable thing (for example, if you log something at DEBUG before parsing
    85  // the flag that tells you to only log at WARN or greater).
    86  //
    87  // However, `glog` also "insists" that you use the standard library to parse (by
    88  // checking `flag.Parsed()`), which doesn't cover cases where `glog` flags get
    89  // installed on some other parsing package, in our case pflag, and therefore are
    90  // actually being parsed before logging. This is incredibly annoying, because
    91  // all log lines end up prefixed with:
    92  //
    93  //	> "ERROR: logging before flag.Parse"
    94  //
    95  // So, we include this little shim to trick `glog` into (correctly, I must
    96  // add!!!!) realizing that CLI arguments have indeed been parsed. Then, we put
    97  // os.Args back in their rightful place, so the parsing we actually want to do
    98  // can proceed as usual.
    99  func TrickGlog() {
   100  	args := os.Args[1:]
   101  	os.Args = os.Args[0:1]
   102  	goflag.Parse()
   103  
   104  	os.Args = append(os.Args, args...)
   105  }
   106  
   107  // The default behavior of PFlagFromGoFlag (which is called on each flag when
   108  // calling AddGoFlagSet) is to allow any flags with single-character names to be
   109  // accessible both as, for example, `-v` and `--v`.
   110  //
   111  // This prevents us from exposing version via `--version|-v` (pflag will actually
   112  // panic when it goes to add the glog log-level flag), so we intervene to blank
   113  // out the Shorthand for _just_ that flag before adding the rest of the goflags
   114  // to a particular pflag FlagSet.
   115  //
   116  // IMPORTANT: This must be called prior to AddGoFlagSet in both Parse and
   117  // ParseFlagsForTest.
   118  func preventGlogVFlagFromClobberingVersionFlagShorthand(fs *flag.FlagSet) {
   119  	// N.B. we use goflag.Lookup instead of this package's Lookup, because we
   120  	// explicitly want to check only the goflags.
   121  	if f := goflag.Lookup("v"); f != nil {
   122  		if fs.Lookup("v") != nil { // This check is exactly what AddGoFlagSet does.
   123  			return
   124  		}
   125  
   126  		pf := flag.PFlagFromGoFlag(f)
   127  		pf.Shorthand = ""
   128  
   129  		fs.AddFlag(pf)
   130  	}
   131  }
   132  
   133  // Usage invokes the current CommandLine's Usage func, or if not overridden,
   134  // "prints a simple header and calls PrintDefaults".
   135  func Usage() {
   136  	flag.Usage()
   137  }
   138  
   139  // filterTestFlags returns two slices: the second one has just the flags for `go test` and the first one contains
   140  // the rest of the flags.
   141  const goTestFlagSuffix = "-test"
   142  const goTestRunFlag = "-test.run"
   143  
   144  func filterTestFlags() ([]string, []string) {
   145  	args := os.Args
   146  	var testFlags []string
   147  	var otherArgs []string
   148  	hasExtraTestRunArg := false
   149  	for i := 0; 0 < len(args) && i < len(args); i++ {
   150  		// This additional logic to check for the test.run flag is required for running single unit tests in GoLand,
   151  		// due to the way it uses "go tool test2json" to run the test. The CLI `go test` specifies the test as "-test.run=TestHeartbeat",
   152  		// but test2json as "-test.run TestHeartbeat". So in the latter case we need to also add the arg following test.run
   153  		if strings.HasPrefix(args[i], goTestFlagSuffix) || hasExtraTestRunArg {
   154  			hasExtraTestRunArg = false
   155  			testFlags = append(testFlags, args[i])
   156  			if args[i] == goTestRunFlag {
   157  				hasExtraTestRunArg = true
   158  			}
   159  			continue
   160  		}
   161  		otherArgs = append(otherArgs, args[i])
   162  	}
   163  	return otherArgs, testFlags
   164  }
   165  
   166  // ParseFlagsForTest parses `go test` flags separately from the app flags. The problem is that pflag.Parse() does not
   167  // handle `go test` flags correctly. We need to separately parse the test flags using goflags. Additionally flags
   168  // like test.Short() require that goflag.Parse() is called first.
   169  func ParseFlagsForTest() {
   170  	// We need to split up the test flags and the regular app pflags.
   171  	// Then hand them off the std flags and pflags parsers respectively.
   172  	args, testFlags := filterTestFlags()
   173  	os.Args = args
   174  
   175  	// Parse the testing flags
   176  	if err := goflag.CommandLine.Parse(testFlags); err != nil {
   177  		fmt.Println("Error parsing regular test flags:", err)
   178  	}
   179  
   180  	// parse remaining flags including the log-related ones like --alsologtostderr
   181  	preventGlogVFlagFromClobberingVersionFlagShorthand(flag.CommandLine)
   182  	flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
   183  	flag.Parse()
   184  }
   185  
   186  // Parsed returns true if the command-line flags have been parsed.
   187  //
   188  // It is agnostic to whether the standard library `flag` package or `pflag` was
   189  // used for parsing, in order to facilitate the migration to `pflag` for
   190  // VEP-4 [1].
   191  //
   192  // [1]: https://github.com/vitessio/vitess/issues/10697.
   193  func Parsed() bool {
   194  	return goflag.Parsed() || flag.Parsed()
   195  }
   196  
   197  // Lookup returns a pflag.Flag with the given name, from either the pflag or
   198  // standard library `flag` CommandLine. If found in the latter, it is converted
   199  // to a pflag.Flag first. If found in neither, this function returns nil.
   200  func Lookup(name string) *flag.Flag {
   201  	if f := flag.Lookup(name); f != nil {
   202  		return f
   203  	}
   204  
   205  	if f := goflag.Lookup(name); f != nil {
   206  		return flag.PFlagFromGoFlag(f)
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  // Args returns the positional arguments with the first double-dash ("--")
   213  // removed. If no double-dash was specified on the command-line, this is
   214  // equivalent to flag.Args() from the standard library flag package.
   215  func Args() (args []string) {
   216  	doubleDashIdx := -1
   217  	for i, arg := range flag.Args() {
   218  		if arg == "--" {
   219  			doubleDashIdx = i
   220  			break
   221  		}
   222  
   223  		args = append(args, arg)
   224  	}
   225  
   226  	if doubleDashIdx != -1 {
   227  		args = append(args, flag.Args()[doubleDashIdx+1:]...)
   228  	}
   229  
   230  	return args
   231  }
   232  
   233  // Arg returns the ith command-line argument after flags have been processed,
   234  // ignoring the first double-dash ("--") argument separator. If fewer than `i`
   235  // arguments were specified, the empty string is returned. If no double-dash was
   236  // specified, this is equivalent to flag.Arg(i) from the standard library flag
   237  // package.
   238  func Arg(i int) string {
   239  	if args := Args(); len(args) > i {
   240  		return args[i]
   241  	}
   242  
   243  	return ""
   244  }
   245  
   246  // From the standard library documentation:
   247  //
   248  //	> If a Value has an IsBoolFlag() bool method returning true, the
   249  //	> command-line parser makes -name equivalent to -name=true rather than
   250  //	> using the next command-line argument.
   251  //
   252  // This also has less-well-documented implications for the default Usage
   253  // behavior, which is why we are duplicating it.
   254  type maybeBoolFlag interface {
   255  	IsBoolFlag() bool
   256  }
   257  
   258  // isZeroValue determines whether the string represents the zero
   259  // value for a flag.
   260  // see https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/flag/flag.go;l=451-465;drc=refs%2Ftags%2Fgo1.17.7
   261  func isZeroValue(f *goflag.Flag, value string) bool {
   262  	typ := reflect.TypeOf(f.Value)
   263  	var z reflect.Value
   264  	if typ.Kind() == reflect.Ptr {
   265  		z = reflect.New(typ.Elem())
   266  	} else {
   267  		z = reflect.Zero(typ)
   268  	}
   269  	return value == z.Interface().(goflag.Value).String()
   270  }