Repository: terraform-docs/terraform-docs (go)

Issue:
Return name of file when using file output
### What problem are you facing?

Using GitLab CI to check if terraform docs are up to date. There is no way to know if running `terraform-docs` updated a file when run. 

Using `terraform fmt` as an example, we can easily check if a file changed and fail the pipeline if true. 

### How could terraform-docs help solve your problem?

When a file changed, returning the filename would allow us to know if a file changed and fail if true. Meaning the docs were not updated when the module changed. 



Pull request description:
Add output-check flag option
<!--
Thank you for helping to improve terraform-docs!

Please read through https://git.io/JtEzg if this is your first time opening a
terraform-docs pull request. Find us in https://slack.terraform-docs.io/messages/dev if
you need any help contributing.
-->

### Description of your changes

<!--
Briefly describe what this pull request does. Be sure to direct your reviewers'
attention to anything that needs special consideration.

We love pull requests that resolve an open terraform-docs issue. If yours does, you
can uncomment the below line to indicate which issue your PR fixes, for example
"Fixes #500":
-->

**This is the exact copy of #458, which I accidentally closed!**

- Add a `--output-check` flag that only works when outputting to a file.
   - Exit code 0 when no changes
   - Exit code 1 when changes found

Also: 
- Return filename when output file updated (or differences found)

Fixes #456 

I have:

- [x] Read and followed terraform-docs' [contribution process].
- [x] All tests pass when I run `make test`.

### How has this code been tested

<!--
Before reviewers can be confident in the correctness of this pull request, it
needs to tested and shown to be correct. Briefly describe the testing that has
already been done or which is planned for this change.
-->

[contribution process]: https://git.io/JtEzg


Patch:
diff --git a/cmd/root.go b/cmd/root.go
index 53b3f84..8c8f03c 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -65,6 +65,7 @@ func NewCommand() *cobra.Command {
 	cmd.PersistentFlags().StringVar(&config.Output.File, "output-file", "", "file path 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.Output.Check, "output-check", false, "check if content of output file is up to date (default false)")
 
 	cmd.PersistentFlags().BoolVar(&config.Sort.Enabled, "sort", true, "sort items")
 	cmd.PersistentFlags().StringVar(&config.Sort.By, "sort-by", "name", "sort items by criteria ["+cli.SortTypes+"]")
diff --git a/docs/reference/asciidoc-document.md b/docs/reference/asciidoc-document.md
index 496fd97..24c2bc0 100644
--- a/docs/reference/asciidoc-document.md
+++ b/docs/reference/asciidoc-document.md
@@ -32,6 +32,7 @@ terraform-docs asciidoc document [PATH] [flags]
       --header-from string          relative path of a file to read header from (default "main.tf")
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
       --indent int                  indention level of AsciiDoc sections [1, 2, 3, 4, 5] (default 2)
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/asciidoc-table.md b/docs/reference/asciidoc-table.md
index 2d6bf3c..3565e1e 100644
--- a/docs/reference/asciidoc-table.md
+++ b/docs/reference/asciidoc-table.md
@@ -32,6 +32,7 @@ terraform-docs asciidoc table [PATH] [flags]
       --header-from string          relative path of a file to read header from (default "main.tf")
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
       --indent int                  indention level of AsciiDoc sections [1, 2, 3, 4, 5] (default 2)
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/asciidoc.md b/docs/reference/asciidoc.md
index 1a5843b..f8d09ff 100644
--- a/docs/reference/asciidoc.md
+++ b/docs/reference/asciidoc.md
@@ -35,6 +35,7 @@ terraform-docs asciidoc [PATH] [flags]
       --footer-from string          relative path of a file to read footer from (default "")
       --header-from string          relative path of a file to read header from (default "main.tf")
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/json.md b/docs/reference/json.md
index 46da130..2b36d32 100644
--- a/docs/reference/json.md
+++ b/docs/reference/json.md
@@ -30,6 +30,7 @@ terraform-docs json [PATH] [flags]
       --footer-from string          relative path of a file to read footer from (default "")
       --header-from string          relative path of a file to read header from (default "main.tf")
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/markdown-document.md b/docs/reference/markdown-document.md
index 6e2448b..61010b1 100644
--- a/docs/reference/markdown-document.md
+++ b/docs/reference/markdown-document.md
@@ -34,6 +34,7 @@ terraform-docs markdown document [PATH] [flags]
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
       --html                        use HTML tags in genereted output (default true)
       --indent int                  indention level of Markdown sections [1, 2, 3, 4, 5] (default 2)
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/markdown-table.md b/docs/reference/markdown-table.md
index 8b154ec..9cdb613 100644
--- a/docs/reference/markdown-table.md
+++ b/docs/reference/markdown-table.md
@@ -34,6 +34,7 @@ terraform-docs markdown table [PATH] [flags]
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
       --html                        use HTML tags in genereted output (default true)
       --indent int                  indention level of Markdown sections [1, 2, 3, 4, 5] (default 2)
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/markdown.md b/docs/reference/markdown.md
index ef48ffe..242255f 100644
--- a/docs/reference/markdown.md
+++ b/docs/reference/markdown.md
@@ -37,6 +37,7 @@ terraform-docs markdown [PATH] [flags]
       --footer-from string          relative path of a file to read footer from (default "")
       --header-from string          relative path of a file to read header from (default "main.tf")
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/pretty.md b/docs/reference/pretty.md
index 922985c..d18a556 100644
--- a/docs/reference/pretty.md
+++ b/docs/reference/pretty.md
@@ -30,6 +30,7 @@ terraform-docs pretty [PATH] [flags]
       --footer-from string          relative path of a file to read footer from (default "")
       --header-from string          relative path of a file to read header from (default "main.tf")
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/terraform-docs.md b/docs/reference/terraform-docs.md
index f06b228..9aec99b 100644
--- a/docs/reference/terraform-docs.md
+++ b/docs/reference/terraform-docs.md
@@ -24,6 +24,7 @@ terraform-docs [PATH] [flags]
       --header-from string          relative path of a file to read header from (default "main.tf")
   -h, --help                        help for terraform-docs
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/tfvars-hcl.md b/docs/reference/tfvars-hcl.md
index e191af0..04ead66 100644
--- a/docs/reference/tfvars-hcl.md
+++ b/docs/reference/tfvars-hcl.md
@@ -30,6 +30,7 @@ terraform-docs tfvars hcl [PATH] [flags]
       --footer-from string          relative path of a file to read footer from (default "")
       --header-from string          relative path of a file to read header from (default "main.tf")
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/tfvars-json.md b/docs/reference/tfvars-json.md
index f7d2ef5..41477ef 100644
--- a/docs/reference/tfvars-json.md
+++ b/docs/reference/tfvars-json.md
@@ -29,6 +29,7 @@ terraform-docs tfvars json [PATH] [flags]
       --footer-from string          relative path of a file to read footer from (default "")
       --header-from string          relative path of a file to read header from (default "main.tf")
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/tfvars.md b/docs/reference/tfvars.md
index 6947fca..ba312af 100644
--- a/docs/reference/tfvars.md
+++ b/docs/reference/tfvars.md
@@ -25,6 +25,7 @@ Generate terraform.tfvars of inputs.
       --footer-from string          relative path of a file to read footer from (default "")
       --header-from string          relative path of a file to read header from (default "main.tf")
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/toml.md b/docs/reference/toml.md
index 2d31326..c4f0edc 100644
--- a/docs/reference/toml.md
+++ b/docs/reference/toml.md
@@ -29,6 +29,7 @@ terraform-docs toml [PATH] [flags]
       --footer-from string          relative path of a file to read footer from (default "")
       --header-from string          relative path of a file to read header from (default "main.tf")
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/xml.md b/docs/reference/xml.md
index 4580cd4..080a40c 100644
--- a/docs/reference/xml.md
+++ b/docs/reference/xml.md
@@ -29,6 +29,7 @@ terraform-docs xml [PATH] [flags]
       --footer-from string          relative path of a file to read footer from (default "")
       --header-from string          relative path of a file to read header from (default "main.tf")
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/docs/reference/yaml.md b/docs/reference/yaml.md
index f09e7c7..ea2f395 100644
--- a/docs/reference/yaml.md
+++ b/docs/reference/yaml.md
@@ -29,6 +29,7 @@ terraform-docs yaml [PATH] [flags]
       --footer-from string          relative path of a file to read footer from (default "")
       --header-from string          relative path of a file to read header from (default "main.tf")
       --hide strings                hide section [all, data-sources, footer, header, inputs, modules, outputs, providers, requirements, resources]
+      --output-check                check if content of output file is up to date (default false)
       --output-file string          file path 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 -->")
diff --git a/internal/cli/config.go b/internal/cli/config.go
index ed3c7e8..1f3cbc7 100644
--- a/internal/cli/config.go
+++ b/internal/cli/config.go
@@ -203,6 +203,7 @@ type output struct {
 	File     string `mapstructure:"file"`
 	Mode     string `mapstructure:"mode"`
 	Template string `mapstructure:"template"`
+	Check    bool
 
 	beginComment string
 	endComment   string
@@ -213,6 +214,7 @@ func defaultOutput() output {
 		File:     "",
 		Mode:     outputModeInject,
 		Template: OutputTemplate,
+		Check:    false,
 
 		beginComment: outputBeginComment,
 		endComment:   outputEndComment,
diff --git a/internal/cli/run.go b/internal/cli/run.go
index bc4e6d7..46d7277 100644
--- a/internal/cli/run.go
+++ b/internal/cli/run.go
@@ -229,6 +229,8 @@ func writeContent(config *Config, content string) error {
 
 			mode: config.Output.Mode,
 
+			check: config.Output.Check,
+
 			template: config.Output.Template,
 			begin:    config.Output.beginComment,
 			end:      config.Output.endComment,
diff --git a/internal/cli/writer.go b/internal/cli/writer.go
index b9c351d..661d710 100644
--- a/internal/cli/writer.go
+++ b/internal/cli/writer.go
@@ -13,6 +13,7 @@ package cli
 import (
 	"bytes"
 	"errors"
+	"fmt"
 	"io"
 	"os"
 	"path/filepath"
@@ -46,6 +47,8 @@ type fileWriter struct {
 
 	mode string
 
+	check bool
+
 	template string
 	begin    string
 	end      string
@@ -154,8 +157,26 @@ func (fw *fileWriter) inject(filename string, content string, generated string)
 // wrtie the content to io.Writer. If no io.Writer is available,
 // it will be written to 'filename'.
 func (fw *fileWriter) write(filename string, p []byte) (int, error) {
+	// if run in check mode return exit 1
+	if fw.check {
+		f, err := os.ReadFile(filename)
+		if err != nil {
+			return 0, err
+		}
+
+		// check for changes and print changed file
+		if !bytes.Equal(f, p) {
+			return 0, fmt.Errorf("%s is out of date", filename)
+		}
+
+		fmt.Printf("%s is up to date\n", filename)
+		return 0, nil
+	}
+
 	if fw.writer != nil {
 		return fw.writer.Write(p)
 	}
+
+	fmt.Printf("%s updated successfully\n", filename)
 	return len(p), os.WriteFile(filename, p, 0644)
 }


Test patch:
diff --git a/internal/cli/writer_test.go b/internal/cli/writer_test.go
index 774e178..6b6aae8 100644
--- a/internal/cli/writer_test.go
+++ b/internal/cli/writer_test.go
@@ -58,6 +58,7 @@ func TestFileWriter(t *testing.T) {
 	tests := map[string]struct {
 		file     string
 		mode     string
+		check    bool
 		template string
 		begin    string
 		end      string
@@ -71,6 +72,7 @@ func TestFileWriter(t *testing.T) {
 		"ModeInject": {
 			file:     "mode-inject.md",
 			mode:     "inject",
+			check:    false,
 			template: OutputTemplate,
 			begin:    outputBeginComment,
 			end:      outputEndComment,
@@ -83,6 +85,7 @@ func TestFileWriter(t *testing.T) {
 		"ModeInjectEmptyFile": {
 			file:     "empty-file.md",
 			mode:     "inject",
+			check:    false,
 			template: OutputTemplate,
 			begin:    outputBeginComment,
 			end:      outputEndComment,
@@ -95,6 +98,7 @@ func TestFileWriter(t *testing.T) {
 		"ModeInjectNoCommentAppend": {
 			file:     "mode-inject-no-comment.md",
 			mode:     "inject",
+			check:    false,
 			template: OutputTemplate,
 			begin:    outputBeginComment,
 			end:      outputEndComment,
@@ -107,6 +111,7 @@ func TestFileWriter(t *testing.T) {
 		"ModeInjectFileMissing": {
 			file:     "file-missing.md",
 			mode:     "inject",
+			check:    false,
 			template: OutputTemplate,
 			begin:    outputBeginComment,
 			end:      outputEndComment,
@@ -119,6 +124,7 @@ func TestFileWriter(t *testing.T) {
 		"ModeReplaceWithComment": {
 			file:     "mode-replace.md",
 			mode:     "replace",
+			check:    false,
 			template: OutputTemplate,
 			begin:    outputBeginComment,
 			end:      outputEndComment,
@@ -131,6 +137,7 @@ func TestFileWriter(t *testing.T) {
 		"ModeReplaceWithCommentEmptyFile": {
 			file:     "mode-replace.md",
 			mode:     "replace",
+			check:    false,
 			template: OutputTemplate,
 			begin:    outputBeginComment,
 			end:      outputEndComment,
@@ -143,6 +150,7 @@ func TestFileWriter(t *testing.T) {
 		"ModeReplaceWithCommentFileMissing": {
 			file:     "file-missing.md",
 			mode:     "replace",
+			check:    false,
 			template: OutputTemplate,
 			begin:    outputBeginComment,
 			end:      outputEndComment,
@@ -155,6 +163,7 @@ func TestFileWriter(t *testing.T) {
 		"ModeReplaceWithoutComment": {
 			file:     "mode-replace.md",
 			mode:     "replace",
+			check:    false,
 			template: outputContent,
 			begin:    "",
 			end:      "",
@@ -167,6 +176,7 @@ func TestFileWriter(t *testing.T) {
 		"ModeReplaceWithoutTemplate": {
 			file:     "mode-replace.md",
 			mode:     "replace",
+			check:    false,
 			template: "",
 			begin:    "",
 			end:      "",
@@ -181,6 +191,7 @@ func TestFileWriter(t *testing.T) {
 		"EmptyTemplate": {
 			file:     "not-applicable.md",
 			mode:     "inject",
+			check:    false,
 			template: "",
 			begin:    outputBeginComment,
 			end:      outputEndComment,
@@ -193,6 +204,7 @@ func TestFileWriter(t *testing.T) {
 		"BeginCommentMissing": {
 			file:     "begin-comment-missing.md",
 			mode:     "inject",
+			check:    false,
 			template: OutputTemplate,
 			begin:    outputBeginComment,
 			end:      outputEndComment,
@@ -205,6 +217,7 @@ func TestFileWriter(t *testing.T) {
 		"EndCommentMissing": {
 			file:     "end-comment-missing.md",
 			mode:     "inject",
+			check:    false,
 			template: OutputTemplate,
 			begin:    outputBeginComment,
 			end:      outputEndComment,
@@ -217,6 +230,7 @@ func TestFileWriter(t *testing.T) {
 		"EndCommentBeforeBegin": {
 			file:     "end-comment-before-begin.md",
 			mode:     "inject",
+			check:    false,
 			template: OutputTemplate,
 			begin:    outputBeginComment,
 			end:      outputEndComment,
@@ -226,6 +240,19 @@ func TestFileWriter(t *testing.T) {
 			wantErr:  true,
 			errMsg:   "end comment is before begin comment",
 		},
+		"ModeReplaceOutOfDate": {
+			file:     "mode-replace.md",
+			mode:     "replace",
+			check:    true,
+			template: OutputTemplate,
+			begin:    outputBeginComment,
+			end:      outputEndComment,
+			writer:   nil,
+
+			expected: "",
+			wantErr:  true,
+			errMsg:   "testdata/writer/mode-replace.md is out of date",
+		},
 	}
 	for name, tt := range tests {
 		t.Run(name, func(t *testing.T) {
@@ -237,6 +264,8 @@ func TestFileWriter(t *testing.T) {
 
 				mode: tt.mode,
 
+				check: tt.check,
+
 				template: tt.template,
 				begin:    tt.begin,
 				end:      tt.end,