github.com/errata-ai/vale/v3@v3.4.2/internal/core/source.go (about) 1 package core 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path" 8 "path/filepath" 9 "strings" 10 11 "github.com/errata-ai/ini" 12 ) 13 14 // ConfigSrc is a source of configuration values. 15 // 16 // This could be a local file, a string, or a remote URL. 17 type ConfigSrc int 18 19 const ( 20 FileSrc ConfigSrc = iota 21 StringSrc 22 ) 23 24 // ReadPipeline loads Vale's configuration according to the local search 25 // process. 26 // 27 // A `dry` run means that we can't expect the `StylesPath` to fully formed yet. 28 // For example, some assets may not have been downloaded yet via the `sync` 29 // command. 30 func ReadPipeline(flags *CLIFlags, dry bool) (*Config, error) { 31 config, err := NewConfig(flags) 32 if err != nil { 33 return config, err 34 } else if err = validateFlags(config); err != nil { 35 return config, err 36 } 37 38 _, err = FromFile(config, dry) 39 if err != nil { 40 return config, err 41 } 42 43 sources, err := pipeConfig(config) 44 if err != nil { 45 return config, err 46 } 47 48 if len(sources) > 0 { 49 config.Flags.Sources = strings.Join(sources, ",") 50 51 _, err = FromFile(config, true) 52 if err != nil { 53 return config, err 54 } 55 } 56 57 return config, nil 58 } 59 60 // from updates an existing configuration with values From a user-provided 61 // source. 62 func from(provider ConfigSrc, src string, cfg *Config, dry bool) (*ini.File, error) { 63 switch provider { 64 case FileSrc: 65 return loadINI(cfg, dry) 66 case StringSrc: 67 return loadStdin(src, cfg, dry) 68 default: 69 return nil, NewE100( 70 "source/From", fmt.Errorf("unknown provider '%v'", provider)) 71 } 72 } 73 74 // FromFile loads an INI configuration from a file. 75 func FromFile(cfg *Config, dry bool) (*ini.File, error) { 76 return from(FileSrc, "", cfg, dry) 77 } 78 79 // FromString loads an INI configuration from a string. 80 func FromString(src string, cfg *Config, dry bool) (*ini.File, error) { 81 return from(StringSrc, src, cfg, dry) 82 } 83 84 func validateFlags(cfg *Config) error { 85 if cfg.Flags.Path != "" && !FileExists(cfg.Flags.Path) { 86 return NewE100( 87 "--config", 88 fmt.Errorf("path '%s' does not exist", cfg.Flags.Path)) 89 } 90 return nil 91 } 92 93 func loadStdin(src string, cfg *Config, dry bool) (*ini.File, error) { 94 uCfg, err := shadowLoad([]byte(src)) 95 if err != nil { 96 return nil, NewE100("loadStdin", err) 97 } 98 return processConfig(uCfg, cfg, dry) 99 } 100 101 func loadINI(cfg *Config, dry bool) (*ini.File, error) { 102 uCfg := ini.Empty(ini.LoadOptions{ 103 AllowShadows: true, 104 Loose: true, 105 SpaceBeforeInlineComment: true, 106 }) 107 108 base, err := loadConfig(configNames) 109 if err != nil { 110 return nil, NewE100("loadINI/homedir", err) 111 } 112 cfg.RootINI = base 113 114 if cfg.Flags.Sources != "" { 115 // NOTE: This case shouldn't be accessible from the CLI, but it can 116 // still be triggered by packages that include config files. 117 var sources []string 118 119 for _, source := range strings.Split(cfg.Flags.Sources, ",") { 120 abs, _ := filepath.Abs(source) 121 sources = append(sources, abs) 122 } 123 124 // We have multiple sources -- e.g., local config + remote package(s). 125 // 126 // See fixtures/config.feature#451 for an explanation of how this has 127 // changed since Vale Server was deprecated. 128 uCfg, err = processSources(cfg, sources) 129 if err != nil { 130 return nil, NewE100("config pipeline failed", err) 131 } 132 } else if cfg.Flags.Path != "" { 133 // We've been given a value through `--config`. 134 err = uCfg.Append(cfg.Flags.Path) 135 if err != nil { 136 return nil, NewE100("invalid --config", err) 137 } 138 cfg.AddConfigFile(cfg.Flags.Path) 139 } else if fromEnv, hasEnv := os.LookupEnv("VALE_CONFIG_PATH"); hasEnv { 140 // We've been given a value through `VALE_CONFIG_PATH`. 141 err = uCfg.Append(fromEnv) 142 if err != nil { 143 return nil, NewE100("invalid VALE_CONFIG_PATH", err) 144 } 145 cfg.AddConfigFile(fromEnv) 146 } else if base != "" { 147 // We're using a config file found using a local search process. 148 err = uCfg.Append(base) 149 if err != nil { 150 return nil, NewE100(".vale.ini not found", err) 151 } 152 cfg.AddConfigFile(base) 153 } 154 155 if StringInSlice(cfg.Flags.AlertLevel, AlertLevels) { 156 cfg.MinAlertLevel = LevelToInt[cfg.Flags.AlertLevel] 157 } 158 159 // NOTE: In v3.0, we now use the user's config directory as the default 160 // location. 161 // 162 // This is different from the other config-defining options (`--config`, 163 // `VALE_CONFIG_PATH`, etc.) in that it's loaded in addition to, rather 164 // than instead of, any other configuration sources. 165 // 166 // In other words, this config file is *always* loaded and is read after 167 // any other sources to allow for project-agnostic customization. 168 defaultCfg, _ := DefaultConfig() 169 170 if FileExists(defaultCfg) && !cfg.Flags.IgnoreGlobal && !dry { 171 err = uCfg.Append(defaultCfg) 172 if err != nil { 173 return nil, NewE100("default/ini", err) 174 } 175 cfg.Flags.Local = true 176 cfg.AddConfigFile(defaultCfg) 177 } else if base == "" && len(cfg.ConfigFiles) == 0 && !dry { 178 return nil, NewE100(".vale.ini not found", errors.New("no config file found")) 179 } 180 181 uCfg.BlockMode = false 182 return processConfig(uCfg, cfg, dry) 183 } 184 185 // loadConfig loads the .vale file. It checks the ancestors of the current 186 // directory, stopping on the first occurrence of a .vale or _vale file. If 187 // no ancestor of the current directory has a configuration file, it checks 188 // the user's home directory for a configuration file. 189 func loadConfig(names []string) (string, error) { 190 var parent string 191 192 cwd, err := os.Getwd() 193 if err != nil { 194 return "", err 195 } 196 197 for { 198 parent = filepath.Dir(cwd) 199 200 for _, name := range names { 201 loc := path.Join(cwd, name) 202 if FileExists(loc) && !IsDir(loc) { 203 return loc, nil 204 } 205 } 206 207 if cwd == parent { 208 break 209 } 210 cwd = parent 211 } 212 213 homeDir, _ := os.UserHomeDir() 214 if homeDir == "" { 215 return "", nil 216 } 217 218 for _, name := range names { 219 loc := path.Join(homeDir, name) 220 if FileExists(loc) && !IsDir(loc) { 221 return loc, nil 222 } 223 } 224 225 return "", nil 226 }