diff --git a/cmd/root.go b/cmd/root.go index dc68d8e..5952bea 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -57,11 +57,15 @@ func NewCommand() *cobra.Command { // flags cmd.PersistentFlags().StringVarP(&config.File, "config", "c", ".terraform-docs.yml", "config file name") - cmd.PersistentFlags().StringSliceVar(&config.Sections.Show, "show", []string{}, "show section [header, inputs, modules, outputs, providers, requirements, resources]") - cmd.PersistentFlags().StringSliceVar(&config.Sections.Hide, "hide", []string{}, "hide section [header, inputs, modules, outputs, providers, requirements, resources]") + cmd.PersistentFlags().StringSliceVar(&config.Sections.Show, "show", []string{}, "show section ["+cli.AllSections+"]") + cmd.PersistentFlags().StringSliceVar(&config.Sections.Hide, "hide", []string{}, "hide section ["+cli.AllSections+"]") cmd.PersistentFlags().BoolVar(&config.Sections.ShowAll, "show-all", true, "show all sections") cmd.PersistentFlags().BoolVar(&config.Sections.HideAll, "hide-all", false, "hide all sections (default false)") + cmd.PersistentFlags().StringVar(&config.Output.File, "output-file", "", "File in module directory to insert output into (default \"\")") + cmd.PersistentFlags().StringVar(&config.Output.Mode, "output-mode", "inject", "Output to file method ["+cli.OutputModes+"]") + cmd.PersistentFlags().StringVar(&config.Output.Template, "output-template", cli.OutputTemplate, "Output template") + cmd.PersistentFlags().BoolVar(&config.Sort.Enabled, "sort", true, "sort items") cmd.PersistentFlags().BoolVar(&config.Sort.By.Required, "sort-by-required", false, "sort items by name and print required ones first (default false)") cmd.PersistentFlags().BoolVar(&config.Sort.By.Type, "sort-by-type", false, "sort items by type of them (default false)") diff --git a/docs/reference/asciidoc-document.md b/docs/reference/asciidoc-document.md index 991b7af..b40793f 100644 --- a/docs/reference/asciidoc-document.md +++ b/docs/reference/asciidoc-document.md @@ -32,6 +32,9 @@ terraform-docs asciidoc document [PATH] [flags] --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) --indent int indention level of AsciiDoc sections [1, 2, 3, 4, 5] (default 2) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --required show Required column or section (default true) diff --git a/docs/reference/asciidoc-table.md b/docs/reference/asciidoc-table.md index 092598c..75ec3a3 100644 --- a/docs/reference/asciidoc-table.md +++ b/docs/reference/asciidoc-table.md @@ -32,6 +32,9 @@ terraform-docs asciidoc table [PATH] [flags] --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) --indent int indention level of AsciiDoc sections [1, 2, 3, 4, 5] (default 2) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --required show Required column or section (default true) diff --git a/docs/reference/asciidoc.md b/docs/reference/asciidoc.md index ff2384c..5fb504f 100644 --- a/docs/reference/asciidoc.md +++ b/docs/reference/asciidoc.md @@ -35,6 +35,9 @@ terraform-docs asciidoc [PATH] [flags] --header-from string relative path of a file to read header from (default "main.tf") --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --show strings show section [header, inputs, modules, outputs, providers, requirements, resources] diff --git a/docs/reference/json.md b/docs/reference/json.md index ac2aedf..45303ae 100644 --- a/docs/reference/json.md +++ b/docs/reference/json.md @@ -30,6 +30,9 @@ terraform-docs json [PATH] [flags] --header-from string relative path of a file to read header from (default "main.tf") --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --show strings show section [header, inputs, modules, outputs, providers, requirements, resources] diff --git a/docs/reference/markdown-document.md b/docs/reference/markdown-document.md index 1256072..24d3684 100644 --- a/docs/reference/markdown-document.md +++ b/docs/reference/markdown-document.md @@ -33,6 +33,9 @@ terraform-docs markdown document [PATH] [flags] --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) --indent int indention level of Markdown sections [1, 2, 3, 4, 5] (default 2) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --required show Required column or section (default true) diff --git a/docs/reference/markdown-table.md b/docs/reference/markdown-table.md index f591aba..22df215 100644 --- a/docs/reference/markdown-table.md +++ b/docs/reference/markdown-table.md @@ -33,6 +33,9 @@ terraform-docs markdown table [PATH] [flags] --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) --indent int indention level of Markdown sections [1, 2, 3, 4, 5] (default 2) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --required show Required column or section (default true) diff --git a/docs/reference/markdown.md b/docs/reference/markdown.md index f48d37d..757de8e 100644 --- a/docs/reference/markdown.md +++ b/docs/reference/markdown.md @@ -36,6 +36,9 @@ terraform-docs markdown [PATH] [flags] --header-from string relative path of a file to read header from (default "main.tf") --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --show strings show section [header, inputs, modules, outputs, providers, requirements, resources] diff --git a/docs/reference/pretty.md b/docs/reference/pretty.md index a9d24b9..290f9fe 100644 --- a/docs/reference/pretty.md +++ b/docs/reference/pretty.md @@ -30,6 +30,9 @@ terraform-docs pretty [PATH] [flags] --header-from string relative path of a file to read header from (default "main.tf") --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --show strings show section [header, inputs, modules, outputs, providers, requirements, resources] diff --git a/docs/reference/terraform-docs.md b/docs/reference/terraform-docs.md index 83e4305..4e8bcd9 100644 --- a/docs/reference/terraform-docs.md +++ b/docs/reference/terraform-docs.md @@ -24,6 +24,9 @@ terraform-docs [PATH] [flags] -h, --help help for terraform-docs --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --show strings show section [header, inputs, modules, outputs, providers, requirements, resources] diff --git a/docs/reference/tfvars-hcl.md b/docs/reference/tfvars-hcl.md index 0db0e81..0fac8db 100644 --- a/docs/reference/tfvars-hcl.md +++ b/docs/reference/tfvars-hcl.md @@ -29,6 +29,9 @@ terraform-docs tfvars hcl [PATH] [flags] --header-from string relative path of a file to read header from (default "main.tf") --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --show strings show section [header, inputs, modules, outputs, providers, requirements, resources] diff --git a/docs/reference/tfvars-json.md b/docs/reference/tfvars-json.md index 091d77c..a3b1f49 100644 --- a/docs/reference/tfvars-json.md +++ b/docs/reference/tfvars-json.md @@ -29,6 +29,9 @@ terraform-docs tfvars json [PATH] [flags] --header-from string relative path of a file to read header from (default "main.tf") --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --show strings show section [header, inputs, modules, outputs, providers, requirements, resources] diff --git a/docs/reference/tfvars.md b/docs/reference/tfvars.md index ff14295..7ac87dd 100644 --- a/docs/reference/tfvars.md +++ b/docs/reference/tfvars.md @@ -25,6 +25,9 @@ Generate terraform.tfvars of inputs. --header-from string relative path of a file to read header from (default "main.tf") --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --show strings show section [header, inputs, modules, outputs, providers, requirements, resources] diff --git a/docs/reference/toml.md b/docs/reference/toml.md index ef0d94f..665b4b9 100644 --- a/docs/reference/toml.md +++ b/docs/reference/toml.md @@ -29,6 +29,9 @@ terraform-docs toml [PATH] [flags] --header-from string relative path of a file to read header from (default "main.tf") --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --show strings show section [header, inputs, modules, outputs, providers, requirements, resources] diff --git a/docs/reference/xml.md b/docs/reference/xml.md index aef488a..28d2285 100644 --- a/docs/reference/xml.md +++ b/docs/reference/xml.md @@ -29,6 +29,9 @@ terraform-docs xml [PATH] [flags] --header-from string relative path of a file to read header from (default "main.tf") --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --show strings show section [header, inputs, modules, outputs, providers, requirements, resources] diff --git a/docs/reference/yaml.md b/docs/reference/yaml.md index 1c6b42e..e6d18f5 100644 --- a/docs/reference/yaml.md +++ b/docs/reference/yaml.md @@ -29,6 +29,9 @@ terraform-docs yaml [PATH] [flags] --header-from string relative path of a file to read header from (default "main.tf") --hide strings hide section [header, inputs, modules, outputs, providers, requirements, resources] --hide-all hide all sections (default false) + --output-file string File in module directory to insert output into (default "") + --output-mode string Output to file method [inject, replace] (default "inject") + --output-template string Output template (default "\n{{ .Content }}\n") --output-values inject output values into outputs (default false) --output-values-from string inject output values from file into outputs (default "") --show strings show section [header, inputs, modules, outputs, providers, requirements, resources] diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index aa784a7..0d85f9b 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -52,6 +52,14 @@ sections: show-all: true show: [] +output: + file: "" + mode: inject + template: |- + + {{ .Content }} + + output-values: enabled: false from: "" @@ -121,3 +129,40 @@ The following options are supported and can be used for `sections.show` and - `providers` - `requirements` - `resources` + +## Output + +Insert generated output to file if `output.file` (or `--output-file string` CLI +flag) is not empty. Insersion behavior can be controlled by `output.mode` (or +`--output-mode string` CLI flag): + +- `inject` (default) + + Partially replace the `output-file` with generated output. This will fail if + `output-file` doesn't exist. + +- `replace` + + Completely replace the `output-file` with generated output. This will create + the `output-file` if it doesn't exist. + +The output generated by formatters (`markdown`, `asciidoc`, etc) will first be +inserted into a template before getting saved into the file. This template can +be customized with `output.template` or `--output-template string` CLI flag. +The default template value is: + +```go + +{{ .Content }} + +``` + +This template consists of three items, all of them are mandatory and have to be on +separate lines: + +- begin comment +- `{{ .Content }}` slug +- end comment + +You may change the wording of comment as you wish, but the comment must be present +in the template. Also note that `SPACE`s inside `{{ }}` are mandatory. diff --git a/docs/user-guide/how-to.md b/docs/user-guide/how-to.md index a7829d9..3e518d6 100644 --- a/docs/user-guide/how-to.md +++ b/docs/user-guide/how-to.md @@ -57,6 +57,16 @@ resource "foo" "bar" { ... } **Note:** This comment must start at the immediate first line of the `.tf` file before any `resource`, `variable`, `module`, etc. +## Insert Output To File + +Since `v0.12.0` generated output can be insterted directly into the file. There +are two modes of insersion: `inject` (default) or `replace`. Take a look at [output] +configuration for all the details. + +```console +terraform-docs markdown table --output-file README.md --output-mode inject /path/to/module +``` + ## Generate terraform.tfvars You can generate `terraform.tfvars` in both `hcl` and `json` format by executing @@ -124,6 +134,7 @@ done Please refer to it for complete examples and guides. [sections]: {{< ref "configuration/#sections" >}} +[output]: {{< ref "configuration/#output" >}} [terraform-docs GitHub Action]: https://github.com/terraform-docs/gh-actions [git hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks [pre-commit-terraform]: https://github.com/antonbabenko/pre-commit-terraform diff --git a/examples/.terraform-docs.yml b/examples/.terraform-docs.yml index a18830c..fe28c1d 100644 --- a/examples/.terraform-docs.yml +++ b/examples/.terraform-docs.yml @@ -7,10 +7,26 @@ sections: - inputs - providers - modules + +# output: +# file: README.md +# mode: inject +# template: |- +# +# The template can be customized with aribitrary markdown content. +# For example this can be shown before the actual content generated +# by formatters. + +# {{ .Content }} + +# You can also show something after it! +# + sort: enabled: true by: - required + settings: indent: 4 escape: false diff --git a/internal/cli/config.go b/internal/cli/config.go index 6fd9860..96ce788 100644 --- a/internal/cli/config.go +++ b/internal/cli/config.go @@ -12,11 +12,27 @@ package cli import ( "fmt" + "strings" "github.com/terraform-docs/terraform-docs/internal/print" "github.com/terraform-docs/terraform-docs/internal/terraform" ) +const ( + sectionHeader = "header" + sectionInputs = "inputs" + sectionModules = "modules" + sectionOutputs = "outputs" + sectionProviders = "providers" + sectionRequirements = "requirements" + sectionResources = "resources" +) + +var allSections = []string{sectionHeader, sectionInputs, sectionModules, sectionOutputs, sectionProviders, sectionRequirements, sectionResources} + +// AllSections list. +var AllSections = strings.Join(allSections, ", ") + type sections struct { Show []string `yaml:"show"` Hide []string `yaml:"hide"` @@ -50,17 +66,16 @@ func defaultSections() sections { } func (s *sections) validate() error { - items := []string{"header", "inputs", "modules", "outputs", "providers", "requirements", "resources"} for _, item := range s.Show { switch item { - case items[0], items[1], items[2], items[3], items[4], items[5], items[6]: + case allSections[0], allSections[1], allSections[2], allSections[3], allSections[4], allSections[5], allSections[6]: default: return fmt.Errorf("'%s' is not a valid section", item) } } for _, item := range s.Hide { switch item { - case items[0], items[1], items[2], items[3], items[4], items[5], items[6]: + case allSections[0], allSections[1], allSections[2], allSections[3], allSections[4], allSections[5], allSections[6]: default: return fmt.Errorf("'%s' is not a valid section", item) } @@ -99,6 +114,73 @@ func (s *sections) visibility(section string) bool { return false } +const ( + outputModeInject = "inject" + outputModeReplace = "replace" + + outputBeginComment = "" + outputContent = "{{ .Content }}" + outputEndComment = "" +) + +// Output to file template and modes +var ( + OutputTemplate = fmt.Sprintf("%s\n%s\n%s", outputBeginComment, outputContent, outputEndComment) + OutputModes = strings.Join([]string{outputModeInject, outputModeReplace}, ", ") +) + +type output struct { + File string `yaml:"file"` + Mode string `yaml:"mode"` + Template string `yaml:"template"` + + BeginComment string `yaml:"-"` + EndComment string `yaml:"-"` +} + +func defaultOutput() output { + return output{ + File: "", + Mode: outputModeInject, + Template: OutputTemplate, + + BeginComment: outputBeginComment, + EndComment: outputEndComment, + } +} + +func (o *output) validate() error { + if o.File != "" { + if o.Mode == "" { + return fmt.Errorf("value of '--output-mode' can't be empty") + } + if o.Template == "" { + return fmt.Errorf("value of '--output-template' can't be empty") + } + + index := strings.Index(o.Template, outputContent) + if index < 0 { + return fmt.Errorf("value of '--output-template' doesn't have '{{ .Content }}' (note that spaces inside '{{ }}' are mandatory)") + } + + lines := strings.Split(o.Template, "\n") + if len(lines) < 3 { + return fmt.Errorf("value of '--output-template' should contain at least 3 lines (begin comment, {{ .Content }}, and end comment)") + } + + if !strings.Contains(lines[0], "") { + return fmt.Errorf("value of '--output-template' is missing begin comment") + } + o.BeginComment = strings.TrimSpace(lines[0]) + + if !strings.Contains(lines[len(lines)-1], "") { + return fmt.Errorf("value of '--output-template' is missing end comment") + } + o.EndComment = strings.TrimSpace(lines[len(lines)-1]) + } + return nil +} + type outputvalues struct { Enabled bool `yaml:"enabled"` From string `yaml:"from"` @@ -187,10 +269,12 @@ func (s *settings) validate() error { // Config represents all the available config options that can be accessed and passed through CLI type Config struct { + BaseDir string `yaml:"-"` File string `yaml:"-"` Formatter string `yaml:"formatter"` HeaderFrom string `yaml:"header-from"` Sections sections `yaml:"sections"` + Output output `yaml:"output"` OutputValues outputvalues `yaml:"output-values"` Sort sort `yaml:"sort"` Settings settings `yaml:"settings"` @@ -199,10 +283,12 @@ type Config struct { // DefaultConfig returns new instance of Config with default values set func DefaultConfig() *Config { return &Config{ + BaseDir: "", File: "", Formatter: "", HeaderFrom: "main.tf", Sections: defaultSections(), + Output: defaultOutput(), OutputValues: defaultOutputValues(), Sort: defaultSort(), Settings: defaultSettings(), @@ -244,6 +330,11 @@ func (c *Config) validate() error { return err } + // output + if err := c.Output.validate(); err != nil { + return err + } + // output values if err := c.OutputValues.validate(); err != nil { return err diff --git a/internal/cli/reader.go b/internal/cli/reader.go index 40c3091..44cbae3 100644 --- a/internal/cli/reader.go +++ b/internal/cli/reader.go @@ -88,6 +88,11 @@ func (c *cfgreader) parse() error { if !el.FieldByName(field).Bool() { c.config.Sort.ByList = remove(c.config.Sort.ByList, mapping[flag]) } + case "output-file", "output-mode", "output-template": + mapping := map[string]string{"output-file": "file", "output-mode": "mode", "output-template": "template"} + if err := c.overrideValue(mapping[flag], &c.config.Output, &c.overrides.Output); err != nil { + return err + } case "output-values", "output-values-from": mapping := map[string]string{"output-values": "enabled", "output-values-from": "from"} if err := c.overrideValue(mapping[flag], &c.config.OutputValues, &c.overrides.OutputValues); err != nil { diff --git a/internal/cli/reader_test.go b/internal/cli/reader_test.go index 2f7e548..712cdc8 100644 --- a/internal/cli/reader_test.go +++ b/internal/cli/reader_test.go @@ -110,6 +110,15 @@ func TestOverrideValue(t *testing.T) { wantErr: false, errMsg: "", }, + { + name: "override values of given field", + tag: "mode", + to: func() interface{} { return &config.Output }, + from: func() interface{} { return &override.Output }, + overrideFn: func() { override.Output.Mode = "replace" }, + wantErr: false, + errMsg: "", + }, { name: "override values of unkwon field tag", tag: "not-available", diff --git a/internal/cli/run.go b/internal/cli/run.go index a4ce936..f9d4180 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -12,6 +12,7 @@ package cli import ( "fmt" + "io" "os" "path/filepath" @@ -86,6 +87,9 @@ func PreRunEFunc(config *Config) func(*cobra.Command, []string) error { return err } + // set the base moduel directory + config.BaseDir = args[0] + return nil } } @@ -94,9 +98,9 @@ func PreRunEFunc(config *Config) func(*cobra.Command, []string) error { // This functions extract print.Settings and terraform.Options from generated and // normalized Config and initializes required print.Format instance and executes it. func RunEFunc(config *Config) func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, _ []string) error { settings, options := config.extract() - options.Path = args[0] + options.Path = config.BaseDir module, err := terraform.LoadWithOptions(options) if err != nil { @@ -115,22 +119,47 @@ func RunEFunc(config *Config) func(*cobra.Command, []string) error { return fmt.Errorf("formatter '%s' not found", config.Formatter) } - output, cerr := client.Execute(pluginsdk.ExecuteArgs{ + content, cerr := client.Execute(pluginsdk.ExecuteArgs{ Module: module.Convert(), Settings: settings.Convert(), }) - return printOrDie(output, cerr) + if cerr != nil { + return cerr + } + return writeContent(config, content) } - output, err := printer.Print(module, settings) - return printOrDie(output, err) + content, err := printer.Print(module, settings) + if err != nil { + return err + } + return writeContent(config, content) } } -func printOrDie(output string, err error) error { - if err != nil { - return err +// writeContent to a Writer. This can either be os.Stdout or specific +// file (e.g. README.md) if '--output-file' is provided. +func writeContent(config *Config, content string) error { + var w io.Writer + + // writing to a file (either inject or replace) + if config.Output.File != "" { + w = &fileWriter{ + file: config.Output.File, + dir: config.BaseDir, + + mode: config.Output.Mode, + + template: config.Output.Template, + begin: config.Output.BeginComment, + end: config.Output.EndComment, + } + } else { + // writing to stdout + w = &stdoutWriter{} } - fmt.Println(output) - return nil + + _, err := io.WriteString(w, content) + + return err } diff --git a/internal/cli/testdata/writer/begin-comment-missing.md b/internal/cli/testdata/writer/begin-comment-missing.md new file mode 100644 index 0000000..7269380 --- /dev/null +++ b/internal/cli/testdata/writer/begin-comment-missing.md @@ -0,0 +1,19 @@ +# Foo + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +## Bar + +sed do eiusmod tempor incididunt ut labore et dolore magna +aliqua. + +- Ut enim ad minim veniam +- quis nostrud exercitation + +ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit + + +## Baz + +esse cillum dolore eu fugiat nulla pariatur. diff --git a/internal/cli/testdata/writer/empty-file.md b/internal/cli/testdata/writer/empty-file.md new file mode 100644 index 0000000..e69de29 diff --git a/internal/cli/testdata/writer/end-comment-before-begin.md b/internal/cli/testdata/writer/end-comment-before-begin.md new file mode 100644 index 0000000..eeb3814 --- /dev/null +++ b/internal/cli/testdata/writer/end-comment-before-begin.md @@ -0,0 +1,20 @@ +# Foo + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +## Bar + +sed do eiusmod tempor incididunt ut labore et dolore magna +aliqua. + +- Ut enim ad minim veniam +- quis nostrud exercitation + + +ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit + + +## Baz + +esse cillum dolore eu fugiat nulla pariatur. diff --git a/internal/cli/testdata/writer/end-comment-missing.md b/internal/cli/testdata/writer/end-comment-missing.md new file mode 100644 index 0000000..bf97136 --- /dev/null +++ b/internal/cli/testdata/writer/end-comment-missing.md @@ -0,0 +1,19 @@ +# Foo + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +## Bar + +sed do eiusmod tempor incididunt ut labore et dolore magna +aliqua. + +- Ut enim ad minim veniam +- quis nostrud exercitation + + +ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit + +## Baz + +esse cillum dolore eu fugiat nulla pariatur. diff --git a/internal/cli/writer.go b/internal/cli/writer.go new file mode 100644 index 0000000..0a70197 --- /dev/null +++ b/internal/cli/writer.go @@ -0,0 +1,110 @@ +/* +Copyright 2021 The terraform-docs Authors. + +Licensed under the MIT license (the "License"); you may not +use this file except in compliance with the License. + +You may obtain a copy of the License at the LICENSE file in +the root directory of this source tree. +*/ + +package cli + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "strings" + "text/template" +) + +const ( + errFileEmpty = "file content is empty" + errTemplateEmpty = "template is missing" + errBeginCommentMissing = "begin comment is missing" + errEndCommentMissing = "end comment is missing" + errEndCommentBeforeBegin = "end comment is before begin comment" +) + +// stdoutWriter writes content to os.Stdout. +type stdoutWriter struct{} + +func (sw *stdoutWriter) Write(p []byte) (int, error) { + return os.Stdout.Write([]byte(string(p) + "\n")) +} + +// fileWriter writes content to file. +// +// First of all it will process 'content' into provided 'template'. +// +// If 'mode' is 'replace' it replaces the whole content of 'dir/file' +// with output of executed template. Note that this will create 'dir/file' +// if it doesn't exist. +// +// If 'mode' is 'inject' it will attempt to inject the output of executed +// template into 'dir/file' between the 'begin' and 'end' comment. Note that +// this will fail if 'dir/file' doesn't exist, or doesn't contain 'begin' or +// 'end' comment. +type fileWriter struct { + file string + dir string + + mode string + + template string + begin string + end string +} + +func (fw *fileWriter) Write(p []byte) (int, error) { + var buf bytes.Buffer + + if fw.template == "" { + return 0, errors.New(errTemplateEmpty) + } + + tmpl := template.Must(template.New("content").Parse(fw.template)) + if err := tmpl.ExecuteTemplate(&buf, "content", struct { + Content string + }{ + Content: string(p), + }); err != nil { + return 0, err + } + + content := buf.String() + filename := filepath.Join(fw.dir, fw.file) + + if fw.mode == outputModeInject { + f, err := os.ReadFile(filename) + if err != nil { + return 0, err + } + + fc := string(f) + if fc == "" { + return 0, errors.New(errFileEmpty) + } + + before := strings.Index(fc, fw.begin) + if before < 0 { + return 0, errors.New(errBeginCommentMissing) + } + content = fc[:before] + content + + after := strings.Index(fc, fw.end) + if after < 0 { + return 0, errors.New(errEndCommentMissing) + } + if after < before { + return 0, errors.New(errEndCommentBeforeBegin) + } + content = content + fc[after+len(fw.end):] + } + + n := len(content) + err := os.WriteFile(filename, []byte(content), 0644) + + return n, err +} diff --git a/internal/cli/writer_test.go b/internal/cli/writer_test.go new file mode 100644 index 0000000..391250d --- /dev/null +++ b/internal/cli/writer_test.go @@ -0,0 +1,101 @@ +/* +Copyright 2021 The terraform-docs Authors. + +Licensed under the MIT license (the "License"); you may not +use this file except in compliance with the License. + +You may obtain a copy of the License at the LICENSE file in +the root directory of this source tree. +*/ + +package cli + +import ( + "io" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFileWriter(t *testing.T) { + content := "Lorem ipsum dolor sit amet, consectetur adipiscing elit" + tests := map[string]struct { + file string + mode string + template string + begin string + end string + errMsg string + }{ + "ModeInjectNoFile": { + file: "file-missing.md", + mode: "inject", + template: OutputTemplate, + begin: outputBeginComment, + end: outputEndComment, + errMsg: "open testdata/writer/file-missing.md: no such file or directory", + }, + "EmptyTemplate": { + file: "not-applicable.md", + mode: "inject", + template: "", + begin: outputBeginComment, + end: outputEndComment, + errMsg: "template is missing", + }, + "EmptyFile": { + file: "empty-file.md", + mode: "inject", + template: OutputTemplate, + begin: outputBeginComment, + end: outputEndComment, + errMsg: "file content is empty", + }, + "BeginCommentMissing": { + file: "begin-comment-missing.md", + mode: "inject", + template: OutputTemplate, + begin: outputBeginComment, + end: outputEndComment, + errMsg: "begin comment is missing", + }, + "EndCommentMissing": { + file: "end-comment-missing.md", + mode: "inject", + template: OutputTemplate, + begin: outputBeginComment, + end: outputEndComment, + errMsg: "end comment is missing", + }, + "EndCommentBeforeBegin": { + file: "end-comment-before-begin.md", + mode: "inject", + template: OutputTemplate, + begin: outputBeginComment, + end: outputEndComment, + errMsg: "end comment is before begin comment", + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + writer := &fileWriter{ + file: tt.file, + dir: filepath.Join("testdata", "writer"), + + mode: tt.mode, + + template: tt.template, + begin: tt.begin, + end: tt.end, + } + + _, err := io.WriteString(writer, content) + + assert.NotNil(err) + assert.Equal(tt.errMsg, err.Error()) + }) + } +}