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  }