Skip to content

Commit

Permalink
Add attribute rm command
Browse files Browse the repository at this point in the history
  • Loading branch information
minamijoyo committed Mar 6, 2020
1 parent 7bdd899 commit 1b6bbdf
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 0 deletions.
10 changes: 10 additions & 0 deletions README.md
Expand Up @@ -73,6 +73,7 @@ Usage:
Available Commands:
get Get attribute
rm Remove attribute
set Set attribute
Flags:
Expand Down Expand Up @@ -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

```
Expand Down
26 changes: 26 additions & 0 deletions cmd/attribute.go
Expand Up @@ -23,6 +23,7 @@ func newAttributeCmd() *cobra.Command {
cmd.AddCommand(
newAttributeGetCmd(),
newAttributeSetCmd(),
newAttributeRmCmd(),
)

return cmd
Expand Down Expand Up @@ -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 <ADDRESS>",
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)
}
64 changes: 64 additions & 0 deletions cmd/attribute_test.go
Expand Up @@ -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)
}
})
}
}
45 changes: 45 additions & 0 deletions 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
}
125 changes: 125 additions & 0 deletions 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)
}
})
}
}

0 comments on commit 1b6bbdf

Please sign in to comment.