// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package command import ( "bufio" "errors" "fmt" "io" "os" "github.com/google/yamlfmt" "github.com/google/yamlfmt/engine" "github.com/mitchellh/mapstructure" "github.com/braydonk/yaml" ) type FormatterConfig struct { Type string `mapstructure:"type"` FormatterSettings map[string]any `mapstructure:",remain"` } // NewFormatterConfig returns an empty formatter config with all fields initialized. func NewFormatterConfig() *FormatterConfig { return &FormatterConfig{FormatterSettings: make(map[string]any)} } type Config struct { Extensions []string `mapstructure:"extensions"` MatchType yamlfmt.MatchType `mapstructure:"match_type"` Include []string `mapstructure:"include"` Exclude []string `mapstructure:"exclude"` RegexExclude []string `mapstructure:"regex_exclude"` FormatterConfig *FormatterConfig `mapstructure:"formatter,omitempty"` Doublestar bool `mapstructure:"doublestar"` ContinueOnError bool `mapstructure:"continue_on_error"` LineEnding yamlfmt.LineBreakStyle `mapstructure:"line_ending"` GitignoreExcludes bool `mapstructure:"gitignore_excludes"` GitignorePath string `mapstructure:"gitignore_path"` OutputFormat engine.EngineOutputFormat `mapstructure:"output_format"` } type Command struct { Operation yamlfmt.Operation Registry *yamlfmt.Registry Config *Config Quiet bool } func (c *Command) Run() error { formatter, err := c.getFormatter() if err != nil { return err } lineSepChar, err := c.Config.LineEnding.Separator() if err != nil { return err } eng := &engine.ConsecutiveEngine{ LineSepCharacter: lineSepChar, Formatter: formatter, Quiet: c.Quiet, ContinueOnError: c.Config.ContinueOnError, OutputFormat: c.Config.OutputFormat, } collectedPaths, err := c.collectPaths() if err != nil { return err } if c.Config.GitignoreExcludes { newPaths, err := yamlfmt.ExcludeWithGitignore(c.Config.GitignorePath, collectedPaths) if err != nil { return err } collectedPaths = newPaths } paths, err := c.analyzePaths(collectedPaths) if err != nil { fmt.Printf("path analysis found the following errors:\n%v", err) fmt.Println("Continuing...") } switch c.Operation { case yamlfmt.OperationFormat: out, err := eng.Format(paths) if out != nil { fmt.Print(out) } if err != nil { return err } case yamlfmt.OperationLint: out, err := eng.Lint(paths) if err != nil { return err } if out != nil { // This will be picked up by log.Fatal in main() and // cause an exit code of 1, which is a critical // component of the lint functionality. return errors.New(out.String()) } case yamlfmt.OperationDry: out, err := eng.DryRun(paths) if err != nil { return err } if out != nil { fmt.Print(out) } else if !c.Quiet { fmt.Println("No files will be changed.") } case yamlfmt.OperationStdin: stdinYaml, err := readFromStdin() if err != nil { return err } out, err := eng.FormatContent(stdinYaml) if err != nil { return err } fmt.Print(string(out)) case yamlfmt.OperationPrintConfig: commandConfig := map[string]any{} err = mapstructure.Decode(c.Config, &commandConfig) if err != nil { return err } delete(commandConfig, "formatter") out, err := yaml.Marshal(commandConfig) if err != nil { return err } fmt.Print(string(out)) formatterConfigMap, err := formatter.ConfigMap() if err != nil { return err } out, err = yaml.Marshal(map[string]any{ "formatter": formatterConfigMap, }) if err != nil { return err } fmt.Print(string(out)) } return nil } func (c *Command) getFormatter() (yamlfmt.Formatter, error) { var factoryType string // In the existing codepaths, this value is always set. But // it's a habit of mine to check anything that can possibly be nil // if I remember that to be the case. :) if c.Config.FormatterConfig != nil { factoryType = c.Config.FormatterConfig.Type // The line ending set within the formatter settings takes precedence over setting // it from the top level config. If it's not set in formatter settings, then // we use the value from the top level. if _, ok := c.Config.FormatterConfig.FormatterSettings["line_ending"]; !ok { c.Config.FormatterConfig.FormatterSettings["line_ending"] = c.Config.LineEnding } } factory, err := c.Registry.GetFactory(factoryType) if err != nil { return nil, err } return factory.NewFormatter(c.Config.FormatterConfig.FormatterSettings) } func (c *Command) collectPaths() ([]string, error) { collector, err := c.makePathCollector() if err != nil { return nil, err } return collector.CollectPaths() } func (c *Command) analyzePaths(paths []string) ([]string, error) { analyzer, err := c.makeAnalyzer() if err != nil { return nil, err } includePaths, _, err := analyzer.ExcludePathsByContent(paths) return includePaths, err } func (c *Command) makePathCollector() (yamlfmt.PathCollector, error) { switch c.Config.MatchType { case yamlfmt.MatchTypeDoublestar: return &yamlfmt.DoublestarCollector{ Include: c.Config.Include, Exclude: c.Config.Exclude, }, nil case yamlfmt.MatchTypeGitignore: files := c.Config.Include if len(files) == 0 { files = []string{yamlfmt.DefaultPatternFile} } patternFile, err := yamlfmt.NewPatternFileCollector(files...) if err != nil { return nil, fmt.Errorf("NewPatternFile(%q): %w", files, err) } return patternFile, nil default: return &yamlfmt.FilepathCollector{ Include: c.Config.Include, Exclude: c.Config.Exclude, Extensions: c.Config.Extensions, }, nil } } func (c *Command) makeAnalyzer() (yamlfmt.ContentAnalyzer, error) { return yamlfmt.NewBasicContentAnalyzer(c.Config.RegexExclude) } func readFromStdin() ([]byte, error) { stdin := bufio.NewReader(os.Stdin) data := []byte{} for { b, err := stdin.ReadByte() if err != nil { if err == io.EOF { break } else { return nil, err } } data = append(data, b) } return data, nil }