github.com/ethereum/go-ethereum@v1.14.3/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/ethereum/go-ethereum/params"
    29  	"github.com/mattn/go-isatty"
    30  	"github.com/urfave/cli/v2"
    31  )
    32  
    33  // usecolor defines whether the CLI help should use colored output or normal dumb
    34  // colorless terminal formatting.
    35  var usecolor = (isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())) && os.Getenv("TERM") != "dumb"
    36  
    37  // NewApp creates an app with sane defaults.
    38  func NewApp(usage string) *cli.App {
    39  	git, _ := version.VCS()
    40  	app := cli.NewApp()
    41  	app.EnableBashCompletion = true
    42  	app.Version = params.VersionWithCommit(git.Commit, git.Date)
    43  	app.Usage = usage
    44  	app.Copyright = "Copyright 2013-2024 The go-ethereum Authors"
    45  	app.Before = func(ctx *cli.Context) error {
    46  		MigrateGlobalFlags(ctx)
    47  		return nil
    48  	}
    49  	return app
    50  }
    51  
    52  // Merge merges the given flag slices.
    53  func Merge(groups ...[]cli.Flag) []cli.Flag {
    54  	var ret []cli.Flag
    55  	for _, group := range groups {
    56  		ret = append(ret, group...)
    57  	}
    58  	return ret
    59  }
    60  
    61  var migrationApplied = map[*cli.Command]struct{}{}
    62  
    63  // MigrateGlobalFlags makes all global flag values available in the
    64  // context. This should be called as early as possible in app.Before.
    65  //
    66  // Example:
    67  //
    68  //	geth account new --keystore /tmp/mykeystore --lightkdf
    69  //
    70  // is equivalent after calling this method with:
    71  //
    72  //	geth --keystore /tmp/mykeystore --lightkdf account new
    73  //
    74  // i.e. in the subcommand Action function of 'account new', ctx.Bool("lightkdf)
    75  // will return true even if --lightkdf is set as a global option.
    76  //
    77  // This function may become unnecessary when https://github.com/urfave/cli/pull/1245 is merged.
    78  func MigrateGlobalFlags(ctx *cli.Context) {
    79  	var iterate func(cs []*cli.Command, fn func(*cli.Command))
    80  	iterate = func(cs []*cli.Command, fn func(*cli.Command)) {
    81  		for _, cmd := range cs {
    82  			if _, ok := migrationApplied[cmd]; ok {
    83  				continue
    84  			}
    85  			migrationApplied[cmd] = struct{}{}
    86  			fn(cmd)
    87  			iterate(cmd.Subcommands, fn)
    88  		}
    89  	}
    90  
    91  	// This iterates over all commands and wraps their action function.
    92  	iterate(ctx.App.Commands, func(cmd *cli.Command) {
    93  		if cmd.Action == nil {
    94  			return
    95  		}
    96  
    97  		action := cmd.Action
    98  		cmd.Action = func(ctx *cli.Context) error {
    99  			doMigrateFlags(ctx)
   100  			return action(ctx)
   101  		}
   102  	})
   103  }
   104  
   105  func doMigrateFlags(ctx *cli.Context) {
   106  	// Figure out if there are any aliases of commands. If there are, we want
   107  	// to ignore them when iterating over the flags.
   108  	aliases := make(map[string]bool)
   109  	for _, fl := range ctx.Command.Flags {
   110  		for _, alias := range fl.Names()[1:] {
   111  			aliases[alias] = true
   112  		}
   113  	}
   114  	for _, name := range ctx.FlagNames() {
   115  		for _, parent := range ctx.Lineage()[1:] {
   116  			if parent.IsSet(name) {
   117  				// When iterating across the lineage, we will be served both
   118  				// the 'canon' and alias formats of all commands. In most cases,
   119  				// it's fine to set it in the ctx multiple times (one for each
   120  				// name), however, the Slice-flags are not fine.
   121  				// The slice-flags accumulate, so if we set it once as
   122  				// "foo" and once as alias "F", then both will be present in the slice.
   123  				if _, isAlias := aliases[name]; isAlias {
   124  					continue
   125  				}
   126  				// If it is a string-slice, we need to set it as
   127  				// "alfa, beta, gamma" instead of "[alfa beta gamma]", in order
   128  				// for the backing StringSlice to parse it properly.
   129  				if result := parent.StringSlice(name); len(result) > 0 {
   130  					ctx.Set(name, strings.Join(result, ","))
   131  				} else {
   132  					ctx.Set(name, parent.String(name))
   133  				}
   134  				break
   135  			}
   136  		}
   137  	}
   138  }
   139  
   140  func init() {
   141  	if usecolor {
   142  		// Annotate all help categories with colors
   143  		cli.AppHelpTemplate = regexp.MustCompile("[A-Z ]+:").ReplaceAllString(cli.AppHelpTemplate, "\u001B[33m$0\u001B[0m")
   144  
   145  		// Annotate flag categories with colors (private template, so need to
   146  		// copy-paste the entire thing here...)
   147  		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}}")
   148  	}
   149  	cli.FlagStringer = FlagString
   150  }
   151  
   152  // FlagString prints a single flag in help.
   153  func FlagString(f cli.Flag) string {
   154  	df, ok := f.(cli.DocGenerationFlag)
   155  	if !ok {
   156  		return ""
   157  	}
   158  	needsPlaceholder := df.TakesValue()
   159  	placeholder := ""
   160  	if needsPlaceholder {
   161  		placeholder = "value"
   162  	}
   163  
   164  	namesText := cli.FlagNamePrefixer(df.Names(), placeholder)
   165  
   166  	defaultValueString := ""
   167  	if s := df.GetDefaultText(); s != "" {
   168  		defaultValueString = " (default: " + s + ")"
   169  	}
   170  	envHint := strings.TrimSpace(cli.FlagEnvHinter(df.GetEnvVars(), ""))
   171  	if envHint != "" {
   172  		envHint = " (" + envHint[1:len(envHint)-1] + ")"
   173  	}
   174  	usage := strings.TrimSpace(df.GetUsage())
   175  	usage = wordWrap(usage, 80)
   176  	usage = indent(usage, 10)
   177  
   178  	if usecolor {
   179  		return fmt.Sprintf("\n    \u001B[32m%-35s%-35s\u001B[0m%s\n%s", namesText, defaultValueString, envHint, usage)
   180  	} else {
   181  		return fmt.Sprintf("\n    %-35s%-35s%s\n%s", namesText, defaultValueString, envHint, usage)
   182  	}
   183  }
   184  
   185  func indent(s string, nspace int) string {
   186  	ind := strings.Repeat(" ", nspace)
   187  	return ind + strings.ReplaceAll(s, "\n", "\n"+ind)
   188  }
   189  
   190  func wordWrap(s string, width int) string {
   191  	var (
   192  		output     strings.Builder
   193  		lineLength = 0
   194  	)
   195  
   196  	for {
   197  		sp := strings.IndexByte(s, ' ')
   198  		var word string
   199  		if sp == -1 {
   200  			word = s
   201  		} else {
   202  			word = s[:sp]
   203  		}
   204  		wlen := len(word)
   205  		over := lineLength+wlen >= width
   206  		if over {
   207  			output.WriteByte('\n')
   208  			lineLength = 0
   209  		} else {
   210  			if lineLength != 0 {
   211  				output.WriteByte(' ')
   212  				lineLength++
   213  			}
   214  		}
   215  
   216  		output.WriteString(word)
   217  		lineLength += wlen
   218  
   219  		if sp == -1 {
   220  			break
   221  		}
   222  		s = s[wlen+1:]
   223  	}
   224  
   225  	return output.String()
   226  }
   227  
   228  // AutoEnvVars extends all the specific CLI flags with automatically generated
   229  // env vars by capitalizing the flag, replacing . with _ and prefixing it with
   230  // the specified string.
   231  //
   232  // Note, the prefix should *not* contain the separator underscore, that will be
   233  // added automatically.
   234  func AutoEnvVars(flags []cli.Flag, prefix string) {
   235  	for _, flag := range flags {
   236  		envvar := strings.ToUpper(prefix + "_" + strings.ReplaceAll(strings.ReplaceAll(flag.Names()[0], ".", "_"), "-", "_"))
   237  
   238  		switch flag := flag.(type) {
   239  		case *cli.StringFlag:
   240  			flag.EnvVars = append(flag.EnvVars, envvar)
   241  
   242  		case *cli.StringSliceFlag:
   243  			flag.EnvVars = append(flag.EnvVars, envvar)
   244  
   245  		case *cli.BoolFlag:
   246  			flag.EnvVars = append(flag.EnvVars, envvar)
   247  
   248  		case *cli.IntFlag:
   249  			flag.EnvVars = append(flag.EnvVars, envvar)
   250  
   251  		case *cli.Int64Flag:
   252  			flag.EnvVars = append(flag.EnvVars, envvar)
   253  
   254  		case *cli.Uint64Flag:
   255  			flag.EnvVars = append(flag.EnvVars, envvar)
   256  
   257  		case *cli.Float64Flag:
   258  			flag.EnvVars = append(flag.EnvVars, envvar)
   259  
   260  		case *cli.DurationFlag:
   261  			flag.EnvVars = append(flag.EnvVars, envvar)
   262  
   263  		case *cli.PathFlag:
   264  			flag.EnvVars = append(flag.EnvVars, envvar)
   265  
   266  		case *BigFlag:
   267  			flag.EnvVars = append(flag.EnvVars, envvar)
   268  
   269  		case *TextMarshalerFlag:
   270  			flag.EnvVars = append(flag.EnvVars, envvar)
   271  
   272  		case *DirectoryFlag:
   273  			flag.EnvVars = append(flag.EnvVars, envvar)
   274  		}
   275  	}
   276  }
   277  
   278  // CheckEnvVars iterates over all the environment variables and checks if any of
   279  // them look like a CLI flag but is not consumed. This can be used to detect old
   280  // or mistyped names.
   281  func CheckEnvVars(ctx *cli.Context, flags []cli.Flag, prefix string) {
   282  	known := make(map[string]string)
   283  	for _, flag := range flags {
   284  		docflag, ok := flag.(cli.DocGenerationFlag)
   285  		if !ok {
   286  			continue
   287  		}
   288  		for _, envvar := range docflag.GetEnvVars() {
   289  			known[envvar] = flag.Names()[0]
   290  		}
   291  	}
   292  	keyvals := os.Environ()
   293  	sort.Strings(keyvals)
   294  
   295  	for _, keyval := range keyvals {
   296  		key := strings.Split(keyval, "=")[0]
   297  		if !strings.HasPrefix(key, prefix) {
   298  			continue
   299  		}
   300  		if flag, ok := known[key]; ok {
   301  			if ctx.Count(flag) > 0 {
   302  				log.Info("Config environment variable found", "envvar", key, "shadowedby", "--"+flag)
   303  			} else {
   304  				log.Info("Config environment variable found", "envvar", key)
   305  			}
   306  		} else {
   307  			log.Warn("Unknown config environment variable", "envvar", key)
   308  		}
   309  	}
   310  }