From 1b6bbdf80b692474059f51e5e246592d85a15c8f Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Fri, 6 Mar 2020 17:05:12 +0900 Subject: [PATCH] Add attribute rm command --- README.md | 10 +++ cmd/attribute.go | 26 ++++++++ cmd/attribute_test.go | 64 ++++++++++++++++++ editor/attribute_rm.go | 45 +++++++++++++ editor/attribute_rm_test.go | 125 ++++++++++++++++++++++++++++++++++++ 5 files changed, 270 insertions(+) create mode 100644 editor/attribute_rm.go create mode 100644 editor/attribute_rm_test.go diff --git a/README.md b/README.md index 9f88745..5faf53a 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Usage: Available Commands: get Get attribute + rm Remove attribute set Set attribute Flags: @@ -107,6 +108,15 @@ resource "foo" "bar" { } ``` +``` +$ cat tmp/attr.hcl | hcledit attribute rm resource.foo.bar.attr1 +resource "foo" "bar" { + nested { + attr2 = "val2" + } +} +``` + ### block ``` diff --git a/cmd/attribute.go b/cmd/attribute.go index 9f146cf..b008c24 100644 --- a/cmd/attribute.go +++ b/cmd/attribute.go @@ -23,6 +23,7 @@ func newAttributeCmd() *cobra.Command { cmd.AddCommand( newAttributeGetCmd(), newAttributeSetCmd(), + newAttributeRmCmd(), ) return cmd @@ -83,3 +84,28 @@ func runAttributeSetCmd(cmd *cobra.Command, args []string) error { return editor.SetAttribute(cmd.InOrStdin(), cmd.OutOrStdout(), "-", address, value) } + +func newAttributeRmCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "rm
", + Short: "Remove attribute", + Long: `Remove a matched attribute at a given address + +Arguments: + ADDRESS An address of attribute to remove. +`, + RunE: runAttributeRmCmd, + } + + return cmd +} + +func runAttributeRmCmd(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("expected 1 argument, but got %d arguments", len(args)) + } + + address := args[0] + + return editor.RemoveAttribute(cmd.InOrStdin(), cmd.OutOrStdout(), "-", address) +} diff --git a/cmd/attribute_test.go b/cmd/attribute_test.go index 915c1db..7767c0a 100644 --- a/cmd/attribute_test.go +++ b/cmd/attribute_test.go @@ -169,3 +169,67 @@ module "hoge" { }) } } + +func TestAttributeRm(t *testing.T) { + src := `locals { + service = "hoge" + env = "dev" + region = "ap-northeast-1" +}` + + cases := []struct { + name string + args []string + ok bool + want string + }{ + { + name: "remove an unused local variable", + args: []string{"locals.region"}, + ok: true, + want: `locals { + service = "hoge" + env = "dev" +}`, + }, + { + name: "no match", + args: []string{"hoge"}, + ok: true, + want: src, + }, + { + name: "no args", + args: []string{}, + ok: false, + want: "", + }, + { + name: "too many args", + args: []string{"hoge", "fuga"}, + ok: false, + want: "", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cmd := newMockCmd(runAttributeGetCmd, src) + + err := runAttributeRmCmd(cmd, tc.args) + stderr := mockErr(cmd) + if tc.ok && err != nil { + t.Fatalf("unexpected err = %s, stderr: \n%s", err, stderr) + } + + stdout := mockOut(cmd) + if !tc.ok && err == nil { + t.Fatalf("expected to return an error, but no error, stdout: \n%s", stdout) + } + + if stdout != tc.want { + t.Fatalf("got:\n%s\nwant:\n%s", stdout, tc.want) + } + }) + } +} diff --git a/editor/attribute_rm.go b/editor/attribute_rm.go new file mode 100644 index 0000000..39486b0 --- /dev/null +++ b/editor/attribute_rm.go @@ -0,0 +1,45 @@ +package editor + +import ( + "io" + "strings" + + "github.com/hashicorp/hcl/v2/hclwrite" +) + +// RemoveAttribute reads HCL from io.Reader, and remove a matched attribute, +// and writes the updated HCL to io.Writer. +// Note that a filename is used only for an error message. +// If an error occurs, Nothing is written to the output stream. +func RemoveAttribute(r io.Reader, w io.Writer, filename string, address string) error { + e := &Editor{ + source: &parser{filename: filename}, + filters: []Filter{ + &attributeRemove{address: address}, + }, + sink: &formater{}, + } + + return e.Apply(r, w) +} + +// attributeRemove is a filter implementation for attribute. +type attributeRemove struct { + address string +} + +// Filter reads HCL and remove a matched attribute at a given address. +func (f *attributeRemove) Filter(inFile *hclwrite.File) (*hclwrite.File, error) { + attr, body, err := findAttribute(inFile.Body(), f.address) + if err != nil { + return nil, err + } + + if attr != nil { + a := strings.Split(f.address, ".") + attrName := a[len(a)-1] + body.RemoveAttribute(attrName) + } + + return inFile, nil +} diff --git a/editor/attribute_rm_test.go b/editor/attribute_rm_test.go new file mode 100644 index 0000000..6e15bcc --- /dev/null +++ b/editor/attribute_rm_test.go @@ -0,0 +1,125 @@ +package editor + +import ( + "bytes" + "testing" +) + +func TestAttributeRemove(t *testing.T) { + cases := []struct { + name string + src string + address string + ok bool + want string + }{ + { + name: "simple top level attribute", + src: ` +a0 = v0 +a1 = v1 +`, + address: "a0", + ok: true, + want: ` +a1 = v1 +`, + }, + { + name: "simple top level attribute (with comments)", + src: ` +// before attr +a0 = "v0" // inline +a1 = "v1" +`, + address: "a0", + ok: true, + want: ` +a1 = "v1" +`, // Unfortunately we can't keep the before attr comment. + }, + { + name: "attribute in block", + src: ` +a0 = v0 +b1 "l1" { + a1 = v1 + a2 = v2 +} +`, + address: "b1.l1.a1", + ok: true, + want: ` +a0 = v0 +b1 "l1" { + a2 = v2 +} +`, + }, + { + name: "top level attribute not found", + src: ` +a0 = v0 +`, + address: "a1", + ok: true, + want: ` +a0 = v0 +`, + }, + { + name: "attribute not found in block", + src: ` +a0 = v0 +b1 "l1" { + a1 = v1 +} +`, + address: "b1.l1.a2", + ok: true, + want: ` +a0 = v0 +b1 "l1" { + a1 = v1 +} +`, + }, + { + name: "block not found", + src: ` +a0 = v0 +b1 "l1" { + a1 = v1 +} +`, + address: "b2.l1.a1", + ok: true, + want: ` +a0 = v0 +b1 "l1" { + a1 = v1 +} +`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + inStream := bytes.NewBufferString(tc.src) + outStream := new(bytes.Buffer) + err := RemoveAttribute(inStream, outStream, "test", tc.address) + if tc.ok && err != nil { + t.Fatalf("unexpected err = %s", err) + } + + got := outStream.String() + if !tc.ok && err == nil { + t.Fatalf("expected to return an error, but no error, outStream: \n%s", got) + } + + if got != tc.want { + t.Fatalf("got:\n%s\nwant:\n%s", got, tc.want) + } + }) + } +}