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  }