Save generated content directly into a file

There are two modes for saving into file:

- inject: partially replace the target file with generated output
- replace: completely replace the target file with generated output

The output generated by formatters (markdown, asciidoc, etc) will be
inserted into a template before getting saved into the file. This
template can be customized with '--output-template' CLI flag or
corresponding item in '.terraform-docs.yml' config file. Its default
value is:

```
<!-- BEGIN_TF_DOCS -->
{{ .Content }}
<!-- END_TF_DOCS -->
```

This consists of three items, all of them are mandatory:

- begin comment
- content variable
- end comment

You may change the wording of comment as you wish, but the comment must
be present in the template.

Signed-off-by: Khosrow Moossavi <khos2ow@gmail.com>
This commit is contained in:
Khosrow Moossavi
2021-03-07 14:01:35 -05:00
parent f39601bd5c
commit a3bafd6018
29 changed files with 540 additions and 16 deletions

View File

@@ -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)")

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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)

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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)

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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]

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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]

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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)

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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)

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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]

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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]

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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]

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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]

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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]

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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]

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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]

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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]

View File

@@ -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 "<!-- BEGIN_TF_DOCS -->\n{{ .Content }}\n<!-- END_TF_DOCS -->")
--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]

View File

@@ -52,6 +52,14 @@ sections:
show-all: true
show: []
output:
file: ""
mode: inject
template: |-
<!-- BEGIN_TF_DOCS -->
{{ .Content }}
<!-- END_TF_DOCS -->
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
<!-- BEGIN_TF_DOCS -->
{{ .Content }}
<!-- END_TF_DOCS -->
```
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.

View File

@@ -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

View File

@@ -7,10 +7,26 @@ sections:
- inputs
- providers
- modules
# output:
# file: README.md
# mode: inject
# template: |-
# <!-- BEGIN_TF_DOCS -->
# 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!
# <!-- END_TF_DOCS -->
sort:
enabled: true
by:
- required
settings:
indent: 4
escape: false

View File

@@ -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 = "<!-- BEGIN_TF_DOCS -->"
outputContent = "{{ .Content }}"
outputEndComment = "<!-- END_TF_DOCS -->"
)
// 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], "<!--") || !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], "<!--") || !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

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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
<!-- END_TF_DOCS -->
## Baz
esse cillum dolore eu fugiat nulla pariatur.

View File

View File

@@ -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
<!-- END_TF_DOCS -->
ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit
<!-- BEGIN_TF_DOCS -->
## Baz
esse cillum dolore eu fugiat nulla pariatur.

View File

@@ -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
<!-- BEGIN_TF_DOCS -->
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.

110
internal/cli/writer.go Normal file
View File

@@ -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
}

101
internal/cli/writer_test.go Normal file
View File

@@ -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())
})
}
}