github.com/ethereum/go-ethereum@v1.16.1/internal/flags/helpers.go (about) 1 // Copyright 2020 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package flags 18 19 import ( 20 "fmt" 21 "os" 22 "regexp" 23 "sort" 24 "strings" 25 26 "github.com/ethereum/go-ethereum/internal/version" 27 "github.com/ethereum/go-ethereum/log" 28 "github.com/mattn/go-isatty" 29 "github.com/urfave/cli/v2" 30 ) 31 32 // usecolor defines whether the CLI help should use colored output or normal dumb 33 // colorless terminal formatting. 34 var usecolor = (isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())) && os.Getenv("TERM") != "dumb" 35 36 // NewApp creates an app with sane defaults. 37 func NewApp(usage string) *cli.App { 38 git, _ := version.VCS() 39 app := cli.NewApp() 40 app.EnableBashCompletion = true 41 app.Version = version.WithCommit(git.Commit, git.Date) 42 app.Usage = usage 43 app.Copyright = "Copyright 2013-2025 The go-ethereum Authors" 44 app.Before = func(ctx *cli.Context) error { 45 MigrateGlobalFlags(ctx) 46 return nil 47 } 48 return app 49 } 50 51 var migrationApplied = map[*cli.Command]struct{}{} 52 53 // MigrateGlobalFlags makes all global flag values available in the 54 // context. This should be called as early as possible in app.Before. 55 // 56 // Example: 57 // 58 // geth account new --keystore /tmp/mykeystore --lightkdf 59 // 60 // is equivalent after calling this method with: 61 // 62 // geth --keystore /tmp/mykeystore --lightkdf account new 63 // 64 // i.e. in the subcommand Action function of 'account new', ctx.Bool("lightkdf) 65 // will return true even if --lightkdf is set as a global option. 66 // 67 // This function may become unnecessary when https://github.com/urfave/cli/pull/1245 is merged. 68 func MigrateGlobalFlags(ctx *cli.Context) { 69 var iterate func(cs []*cli.Command, fn func(*cli.Command)) 70 iterate = func(cs []*cli.Command, fn func(*cli.Command)) { 71 for _, cmd := range cs { 72 if _, ok := migrationApplied[cmd]; ok { 73 continue 74 } 75 migrationApplied[cmd] = struct{}{} 76 fn(cmd) 77 iterate(cmd.Subcommands, fn) 78 } 79 } 80 81 // This iterates over all commands and wraps their action function. 82 iterate(ctx.App.Commands, func(cmd *cli.Command) { 83 if cmd.Action == nil { 84 return 85 } 86 87 action := cmd.Action 88 cmd.Action = func(ctx *cli.Context) error { 89 doMigrateFlags(ctx) 90 return action(ctx) 91 } 92 }) 93 } 94 95 func doMigrateFlags(ctx *cli.Context) { 96 // Figure out if there are any aliases of commands. If there are, we want 97 // to ignore them when iterating over the flags. 98 aliases := make(map[string]bool) 99 for _, fl := range ctx.Command.Flags { 100 for _, alias := range fl.Names()[1:] { 101 aliases[alias] = true 102 } 103 } 104 for _, name := range ctx.FlagNames() { 105 for _, parent := range ctx.Lineage()[1:] { 106 if parent.IsSet(name) { 107 // When iterating across the lineage, we will be served both 108 // the 'canon' and alias formats of all commands. In most cases, 109 // it's fine to set it in the ctx multiple times (one for each 110 // name), however, the Slice-flags are not fine. 111 // The slice-flags accumulate, so if we set it once as 112 // "foo" and once as alias "F", then both will be present in the slice. 113 if _, isAlias := aliases[name]; isAlias { 114 continue 115 } 116 // If it is a string-slice, we need to set it as 117 // "alfa, beta, gamma" instead of "[alfa beta gamma]", in order 118 // for the backing StringSlice to parse it properly. 119 if result := parent.StringSlice(name); len(result) > 0 { 120 ctx.Set(name, strings.Join(result, ",")) 121 } else { 122 ctx.Set(name, parent.String(name)) 123 } 124 break 125 } 126 } 127 } 128 } 129 130 func init() { 131 if usecolor { 132 // Annotate all help categories with colors 133 cli.AppHelpTemplate = regexp.MustCompile("[A-Z ]+:").ReplaceAllString(cli.AppHelpTemplate, "\u001B[33m$0\u001B[0m") 134 135 // Annotate flag categories with colors (private template, so need to 136 // copy-paste the entire thing here...) 137 cli.AppHelpTemplate = strings.ReplaceAll(cli.AppHelpTemplate, "{{template \"visibleFlagCategoryTemplate\" .}}", "{{range .VisibleFlagCategories}}\n {{if .Name}}\u001B[33m{{.Name}}\u001B[0m\n\n {{end}}{{$flglen := len .Flags}}{{range $i, $e := .Flags}}{{if eq (subtract $flglen $i) 1}}{{$e}}\n{{else}}{{$e}}\n {{end}}{{end}}{{end}}") 138 } 139 cli.FlagStringer = FlagString 140 } 141 142 // FlagString prints a single flag in help. 143 func FlagString(f cli.Flag) string { 144 df, ok := f.(cli.DocGenerationFlag) 145 if !ok { 146 return "" 147 } 148 needsPlaceholder := df.TakesValue() 149 placeholder := "" 150 if needsPlaceholder { 151 placeholder = "value" 152 } 153 154 namesText := cli.FlagNamePrefixer(df.Names(), placeholder) 155 156 defaultValueString := "" 157 if s := df.GetDefaultText(); s != "" { 158 defaultValueString = " (default: " + s + ")" 159 } 160 envHint := strings.TrimSpace(cli.FlagEnvHinter(df.GetEnvVars(), "")) 161 if envHint != "" { 162 envHint = " (" + envHint[1:len(envHint)-1] + ")" 163 } 164 usage := strings.TrimSpace(df.GetUsage()) 165 usage = wordWrap(usage, 80) 166 usage = indent(usage, 10) 167 168 if usecolor { 169 return fmt.Sprintf("\n \u001B[32m%-35s%-35s\u001B[0m%s\n%s", namesText, defaultValueString, envHint, usage) 170 } else { 171 return fmt.Sprintf("\n %-35s%-35s%s\n%s", namesText, defaultValueString, envHint, usage) 172 } 173 } 174 175 func indent(s string, nspace int) string { 176 ind := strings.Repeat(" ", nspace) 177 return ind + strings.ReplaceAll(s, "\n", "\n"+ind) 178 } 179 180 func wordWrap(s string, width int) string { 181 var ( 182 output strings.Builder 183 lineLength = 0 184 ) 185 186 for { 187 sp := strings.IndexByte(s, ' ') 188 var word string 189 if sp == -1 { 190 word = s 191 } else { 192 word = s[:sp] 193 } 194 wlen := len(word) 195 over := lineLength+wlen >= width 196 if over { 197 output.WriteByte('\n') 198 lineLength = 0 199 } else { 200 if lineLength != 0 { 201 output.WriteByte(' ') 202 lineLength++ 203 } 204 } 205 206 output.WriteString(word) 207 lineLength += wlen 208 209 if sp == -1 { 210 break 211 } 212 s = s[wlen+1:] 213 } 214 215 return output.String() 216 } 217 218 // AutoEnvVars extends all the specific CLI flags with automatically generated 219 // env vars by capitalizing the flag, replacing . with _ and prefixing it with 220 // the specified string. 221 // 222 // Note, the prefix should *not* contain the separator underscore, that will be 223 // added automatically. 224 func AutoEnvVars(flags []cli.Flag, prefix string) { 225 for _, flag := range flags { 226 envvar := strings.ToUpper(prefix + "_" + strings.ReplaceAll(strings.ReplaceAll(flag.Names()[0], ".", "_"), "-", "_")) 227 228 switch flag := flag.(type) { 229 case *cli.StringFlag: 230 flag.EnvVars = append(flag.EnvVars, envvar) 231 232 case *cli.StringSliceFlag: 233 flag.EnvVars = append(flag.EnvVars, envvar) 234 235 case *cli.BoolFlag: 236 flag.EnvVars = append(flag.EnvVars, envvar) 237 238 case *cli.IntFlag: 239 flag.EnvVars = append(flag.EnvVars, envvar) 240 241 case *cli.Int64Flag: 242 flag.EnvVars = append(flag.EnvVars, envvar) 243 244 case *cli.Uint64Flag: 245 flag.EnvVars = append(flag.EnvVars, envvar) 246 247 case *cli.Float64Flag: 248 flag.EnvVars = append(flag.EnvVars, envvar) 249 250 case *cli.DurationFlag: 251 flag.EnvVars = append(flag.EnvVars, envvar) 252 253 case *cli.PathFlag: 254 flag.EnvVars = append(flag.EnvVars, envvar) 255 256 case *BigFlag: 257 flag.EnvVars = append(flag.EnvVars, envvar) 258 259 case *DirectoryFlag: 260 flag.EnvVars = append(flag.EnvVars, envvar) 261 } 262 } 263 } 264 265 // CheckEnvVars iterates over all the environment variables and checks if any of 266 // them look like a CLI flag but is not consumed. This can be used to detect old 267 // or mistyped names. 268 func CheckEnvVars(ctx *cli.Context, flags []cli.Flag, prefix string) { 269 known := make(map[string]string) 270 for _, flag := range flags { 271 docflag, ok := flag.(cli.DocGenerationFlag) 272 if !ok { 273 continue 274 } 275 for _, envvar := range docflag.GetEnvVars() { 276 known[envvar] = flag.Names()[0] 277 } 278 } 279 keyvals := os.Environ() 280 sort.Strings(keyvals) 281 282 for _, keyval := range keyvals { 283 key := strings.Split(keyval, "=")[0] 284 if !strings.HasPrefix(key, prefix) { 285 continue 286 } 287 if flag, ok := known[key]; ok { 288 if ctx.Count(flag) > 0 { 289 log.Info("Config environment variable found", "envvar", key, "shadowedby", "--"+flag) 290 } else { 291 log.Info("Config environment variable found", "envvar", key) 292 } 293 } else { 294 log.Warn("Unknown config environment variable", "envvar", key) 295 } 296 } 297 } 298 299 // CheckExclusive verifies that only a single instance of the provided flags was 300 // set by the user. Each flag might optionally be followed by a string type to 301 // specialize it further. 302 func CheckExclusive(ctx *cli.Context, args ...any) { 303 set := make([]string, 0, 1) 304 for i := 0; i < len(args); i++ { 305 // Make sure the next argument is a flag and skip if not set 306 flag, ok := args[i].(cli.Flag) 307 if !ok { 308 panic(fmt.Sprintf("invalid argument, not cli.Flag type: %T", args[i])) 309 } 310 // Check if next arg extends current and expand its name if so 311 name := flag.Names()[0] 312 313 if i+1 < len(args) { 314 switch option := args[i+1].(type) { 315 case string: 316 // Extended flag check, make sure value set doesn't conflict with passed in option 317 if ctx.String(flag.Names()[0]) == option { 318 name += "=" + option 319 set = append(set, "--"+name) 320 } 321 // shift arguments and continue 322 i++ 323 continue 324 325 case cli.Flag: 326 default: 327 panic(fmt.Sprintf("invalid argument, not cli.Flag or string extension: %T", args[i+1])) 328 } 329 } 330 // Mark the flag if it's set 331 if ctx.IsSet(flag.Names()[0]) { 332 set = append(set, "--"+name) 333 } 334 } 335 if len(set) > 1 { 336 fmt.Fprintf(os.Stderr, "Flags %v can't be used at the same time", strings.Join(set, ", ")) 337 os.Exit(1) 338 } 339 }