github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/cli/command.go (about) 1 package cli 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "regexp" 9 "strings" 10 11 "github.com/spf13/cobra" 12 "github.com/spf13/pflag" 13 "github.com/spf13/viper" 14 15 "github.com/pyroscope-io/pyroscope/pkg/util/slices" 16 ) 17 18 // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02 19 const OptionsEnd = "--" 20 21 type CmdRunFn func(cmd *cobra.Command, args []string) error 22 23 func CreateCmdRunFn(cfg interface{}, vpr *viper.Viper, fn CmdRunFn) CmdRunFn { 24 return func(cmd *cobra.Command, args []string) error { 25 var err error 26 var xargs []string 27 28 args, xargs = splitArgs(cmd.Flags(), args) 29 if slices.StringContains(xargs, "--help") { 30 _ = cmd.Help() 31 return nil 32 } 33 34 if err = vpr.BindPFlags(cmd.Flags()); err != nil { 35 return err 36 } 37 38 // Here's the correct order for configuration precedence: 39 // * command line arguments 40 // * environment variables 41 // * config file 42 // * defaults 43 // also documented here: https://pyroscope.io/docs/server-configuration 44 45 // Parsing arguments for the first time. 46 // The only reason we do this here is so that if you provide -config argument we use the right config path 47 if err = cmd.Flags().Parse(xargs); err != nil { 48 return err 49 } 50 51 // Read configuration from file, if applicable. 52 if err = loadConfigFile(cmd, vpr); err != nil { 53 return err 54 } 55 // Viper deals with both environment variable mappings as well as config files. 56 // That's why this is not included in the previous if statement 57 if err = Unmarshal(vpr, cfg); err != nil { 58 return err 59 } 60 61 // Parsing arguments one more time to override anything set in environment variables or config file 62 if err = cmd.Flags().Parse(xargs); err != nil { 63 return err 64 } 65 66 if err = fn(cmd, args); err != nil { 67 cmd.SilenceUsage = true 68 } 69 return err 70 } 71 } 72 73 func NewViper(prefix string) *viper.Viper { 74 v := viper.New() 75 v.SetEnvPrefix(prefix) 76 v.SetConfigType("yaml") 77 v.AutomaticEnv() 78 v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) 79 return v 80 } 81 82 // splitArgs splits raw arguments into 83 func splitArgs(flags *pflag.FlagSet, args []string) ([]string, []string) { 84 var xargs []string 85 x := firstArgumentIndex(flags, args) 86 if x >= 0 { 87 xargs = args[:x] 88 args = args[x:] 89 } else { 90 xargs = args 91 args = nil 92 } 93 return args, prependDash(xargs) 94 } 95 96 func prependDash1(arg string) string { 97 if len(arg) > 2 && strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") { 98 return "-" + arg 99 } 100 return arg 101 } 102 103 func prependDash(args []string) []string { 104 for i, arg := range args { 105 args[i] = prependDash1(arg) 106 } 107 return args 108 } 109 110 // firstArgumentIndex returns index of the first encountered argument. 111 // If args does not contain arguments, or contains undefined flags, 112 // the call returns -1. 113 func firstArgumentIndex(flags *pflag.FlagSet, args []string) int { 114 for i := 0; i < len(args); i++ { 115 a := prependDash1(args[i]) 116 switch { 117 default: 118 return i 119 case a == OptionsEnd: 120 return i + 1 121 case strings.HasPrefix(a, OptionsEnd) && len(a) > 2: 122 x := strings.SplitN(a[2:], "=", 2) 123 f := flags.Lookup(x[0]) 124 if f == nil { 125 return -1 126 } 127 if f.Value.Type() == "bool" { 128 continue 129 } 130 if len(x) == 1 { 131 i++ 132 } 133 } 134 } 135 // Should have returned earlier. 136 return -1 137 } 138 139 func loadConfigFile(cmd *cobra.Command, vpr *viper.Viper) error { 140 cf := cmd.Flags().Lookup("config") 141 if cf == nil { 142 return nil 143 } 144 var configPath string 145 configPath = cf.Value.String() 146 // Note that Changed is set to true even if the specified flag value 147 // is equal to the default one. For backward compatibility we only 148 // consider an option as user-defined, if its value is different; 149 // which may be unexpected. 150 userDefined := cf.Changed && configPath != cf.DefValue 151 // If configuration file path is overridden with the environment variable 152 // and the flag is not specified, read config by the path from the env var. 153 if !userDefined { 154 if v := os.Getenv("PYROSCOPE_CONFIG"); v != "" { 155 configPath = v 156 userDefined = true 157 } 158 } 159 if configPath == "" { 160 // Must never happen. 161 return nil 162 } 163 vpr.SetConfigFile(configPath) 164 data, err := ioutil.ReadFile(configPath) 165 if err != nil && errors.Is(err, os.ErrNotExist) && !userDefined { 166 // If user did not specify the config file, and the file does not exist, 167 // this is okay 168 return nil 169 } 170 171 if err == nil { 172 return vpr.ReadConfig(strings.NewReader(performSubstitutions(data))) 173 } 174 175 return fmt.Errorf("loading configuration file: %w", err) 176 } 177 178 var digitCheck = regexp.MustCompile(`^[0-9]`) 179 180 func performSubstitutions(data []byte) string { 181 // return string(data) 182 return os.Expand(string(data), func(name string) string { 183 // this is here so that $1, $2, etc. work in the config file 184 if digitCheck.MatchString(name) { 185 return "$" + name 186 } 187 s := os.Getenv(name) 188 return s 189 }) 190 }