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 }