github.com/google/yamlfmt@v0.12.2-0.20240514121411-7f77800e2681/cmd/yamlfmt/config.go (about) 1 // Copyright 2024 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package main 16 17 import ( 18 "errors" 19 "flag" 20 "fmt" 21 "os" 22 "path/filepath" 23 "runtime" 24 "strconv" 25 "strings" 26 27 "github.com/braydonk/yaml" 28 "github.com/google/yamlfmt" 29 "github.com/google/yamlfmt/command" 30 "github.com/google/yamlfmt/internal/collections" 31 "github.com/google/yamlfmt/internal/logger" 32 "github.com/mitchellh/mapstructure" 33 ) 34 35 var configFileNames = collections.Set[string]{ 36 ".yamlfmt": {}, 37 ".yamlfmt.yml": {}, 38 ".yamlfmt.yaml": {}, 39 "yamlfmt.yml": {}, 40 "yamlfmt.yaml": {}, 41 } 42 43 const configHomeDir string = "yamlfmt" 44 45 var ( 46 errNoConfFlag = errors.New("config path not specified in --conf") 47 errConfPathInvalid = errors.New("config path specified in --conf was invalid") 48 errConfPathNotExist = errors.New("no config file found") 49 errConfPathIsDir = errors.New("config path is dir") 50 errNoConfigHome = errors.New("missing required env var for config home") 51 ) 52 53 type configPathError struct { 54 path string 55 err error 56 } 57 58 func (e *configPathError) Error() string { 59 if errors.Is(e.err, errConfPathInvalid) { 60 return fmt.Sprintf("config path %s was invalid", e.path) 61 } 62 if errors.Is(e.err, errConfPathNotExist) { 63 return fmt.Sprintf("no config file found in directory %s", filepath.Dir(e.path)) 64 } 65 if errors.Is(e.err, errConfPathIsDir) { 66 return fmt.Sprintf("config path %s is a directory", e.path) 67 } 68 return e.err.Error() 69 } 70 71 func (e *configPathError) Unwrap() error { 72 return e.err 73 } 74 75 func readConfig(path string) (map[string]any, error) { 76 yamlBytes, err := os.ReadFile(path) 77 if err != nil { 78 return nil, err 79 } 80 var configData map[string]interface{} 81 err = yaml.Unmarshal(yamlBytes, &configData) 82 if err != nil { 83 return nil, err 84 } 85 return configData, nil 86 } 87 88 func getConfigPath() (string, error) { 89 // First priority: specified in cli flag 90 configPath, err := getConfigPathFromFlag() 91 if err != nil { 92 // If they don't provide a conf flag, we continue. If 93 // a conf flag is provided and it's wrong, we consider 94 // that a failure state. 95 if !errors.Is(err, errNoConfFlag) { 96 return "", err 97 } 98 } else { 99 return configPath, nil 100 } 101 102 // Second priority: in the working directory 103 configPath, err = getConfigPathFromDirTree() 104 // In this scenario, no errors are considered a failure state, 105 // so we continue to the next fallback if there are no errors. 106 if err == nil { 107 return configPath, nil 108 } 109 110 if !*flagDisableGlobalConf { 111 // Third priority: in home config directory 112 configPath, err = getConfigPathFromConfigHome() 113 // In this scenario, no errors are considered a failure state, 114 // so we continue to the next fallback if there are no errors. 115 if err == nil { 116 return configPath, nil 117 } 118 } 119 120 // All else fails, no path and no error (signals to 121 // use default config). 122 logger.Debug(logger.DebugCodeConfig, "No config file found, using default config") 123 return "", nil 124 } 125 126 func getConfigPathFromFlag() (string, error) { 127 // First check if the global configuration was explicitly requested as that takes precedence. 128 if *flagGlobalConf { 129 logger.Debug(logger.DebugCodeConfig, "Using -global_conf flag") 130 return getConfigPathFromXdgConfigHome() 131 } 132 // If the global config wasn't explicitly requested, check if there was a specific configuration path supplied. 133 configPath := *flagConf 134 if configPath != "" { 135 logger.Debug(logger.DebugCodeConfig, "Using config path %s from -conf flag", configPath) 136 return configPath, validatePath(configPath) 137 } 138 139 logger.Debug(logger.DebugCodeConfig, "No config path specified in -conf") 140 return configPath, errNoConfFlag 141 } 142 143 // This function searches up the directory tree until it finds 144 // a config file. 145 func getConfigPathFromDirTree() (string, error) { 146 wd, err := os.Getwd() 147 if err != nil { 148 return "", err 149 } 150 absPath, err := filepath.Abs(wd) 151 if err != nil { 152 return "", err 153 } 154 dir := absPath 155 for dir != filepath.Dir(dir) { 156 configPath, err := getConfigPathFromDir(dir) 157 if err == nil { 158 logger.Debug(logger.DebugCodeConfig, "Found config at %s", configPath) 159 return configPath, nil 160 } 161 dir = filepath.Dir(dir) 162 } 163 return "", errConfPathNotExist 164 } 165 166 func getConfigPathFromConfigHome() (string, error) { 167 // Build tags are a veritable pain in the behind, 168 // I'm putting both config home functions in this 169 // file. You can't stop me. 170 if runtime.GOOS == "windows" { 171 return getConfigPathFromAppDataLocal() 172 } 173 return getConfigPathFromXdgConfigHome() 174 } 175 176 func getConfigPathFromXdgConfigHome() (string, error) { 177 configHome, configHomePresent := os.LookupEnv("XDG_CONFIG_HOME") 178 if !configHomePresent { 179 home, homePresent := os.LookupEnv("HOME") 180 if !homePresent { 181 // I fear whom's'tever does not have a $HOME set 182 return "", errNoConfigHome 183 } 184 configHome = filepath.Join(home, ".config") 185 } 186 homeConfigPath := filepath.Join(configHome, configHomeDir) 187 return getConfigPathFromDir(homeConfigPath) 188 } 189 190 func getConfigPathFromAppDataLocal() (string, error) { 191 configHome, configHomePresent := os.LookupEnv("LOCALAPPDATA") 192 if !configHomePresent { 193 // I think you'd have to go out of your way to unset this, 194 // so this should only happen to sickos with broken setups. 195 return "", errNoConfigHome 196 } 197 homeConfigPath := filepath.Join(configHome, configHomeDir) 198 return getConfigPathFromDir(homeConfigPath) 199 } 200 201 func getConfigPathFromDir(dir string) (string, error) { 202 for filename := range configFileNames { 203 configPath := filepath.Join(dir, filename) 204 if err := validatePath(configPath); err == nil { 205 logger.Debug(logger.DebugCodeConfig, "Found config at %s", configPath) 206 return configPath, nil 207 } 208 } 209 logger.Debug(logger.DebugCodeConfig, "No config file found in %s", dir) 210 return "", errConfPathNotExist 211 } 212 213 func validatePath(path string) error { 214 info, err := os.Stat(path) 215 if err != nil { 216 if os.IsNotExist(err) { 217 return &configPathError{ 218 path: path, 219 err: errConfPathNotExist, 220 } 221 } 222 if info.IsDir() { 223 return &configPathError{ 224 path: path, 225 err: errConfPathIsDir, 226 } 227 } 228 return &configPathError{ 229 path: path, 230 err: err, 231 } 232 } 233 return nil 234 } 235 236 func makeCommandConfigFromData(configData map[string]any) (*command.Config, error) { 237 config := command.Config{FormatterConfig: command.NewFormatterConfig()} 238 err := mapstructure.Decode(configData, &config) 239 if err != nil { 240 return nil, err 241 } 242 243 // Parse overrides for formatter configuration 244 if len(flagFormatter) > 0 { 245 overrides, err := parseFormatterConfigFlag(flagFormatter) 246 if err != nil { 247 return nil, err 248 } 249 for k, v := range overrides { 250 if k == "type" { 251 config.FormatterConfig.Type = v.(string) 252 } 253 config.FormatterConfig.FormatterSettings[k] = v 254 } 255 } 256 257 // Default to OS line endings 258 if config.LineEnding == "" { 259 config.LineEnding = yamlfmt.LineBreakStyleLF 260 if runtime.GOOS == "windows" { 261 config.LineEnding = yamlfmt.LineBreakStyleCRLF 262 } 263 } 264 265 // Default to yaml and yml extensions 266 if len(config.Extensions) == 0 { 267 config.Extensions = []string{"yaml", "yml"} 268 } 269 270 // Apply the general rule that the config takes precedence over 271 // the command line flags. 272 if !config.Doublestar { 273 config.Doublestar = *flagDoublestar 274 } 275 if !config.ContinueOnError { 276 config.ContinueOnError = *flagContinueOnError 277 } 278 if !config.GitignoreExcludes { 279 config.GitignoreExcludes = *flagGitignoreExcludes 280 } 281 if config.GitignorePath == "" { 282 config.GitignorePath = *flagGitignorePath 283 } 284 if config.OutputFormat == "" { 285 config.OutputFormat = getOutputFormatFromFlag() 286 } 287 288 // Overwrite config if includes are provided through args 289 if len(flag.Args()) > 0 { 290 config.Include = flag.Args() 291 } 292 293 // Append any additional data from array flags 294 config.Exclude = append(config.Exclude, flagExclude...) 295 config.Extensions = append(config.Extensions, flagExtensions...) 296 297 return &config, nil 298 } 299 300 func parseFormatterConfigFlag(flagValues []string) (map[string]any, error) { 301 formatterValues := map[string]any{} 302 flagErrors := collections.Errors{} 303 304 // Expected format: fieldname=value 305 for _, configField := range flagValues { 306 if strings.Count(configField, "=") != 1 { 307 flagErrors = append( 308 flagErrors, 309 fmt.Errorf("badly formatted config field: %s", configField), 310 ) 311 continue 312 } 313 314 kv := strings.Split(configField, "=") 315 316 // Try to parse as integer 317 vInt, err := strconv.ParseInt(kv[1], 10, 64) 318 if err == nil { 319 formatterValues[kv[0]] = vInt 320 continue 321 } 322 323 // Try to parse as boolean 324 vBool, err := strconv.ParseBool(kv[1]) 325 if err == nil { 326 formatterValues[kv[0]] = vBool 327 continue 328 } 329 330 // Fall through to parsing as string 331 formatterValues[kv[0]] = kv[1] 332 } 333 334 return formatterValues, flagErrors.Combine() 335 }