diff --git a/README.md b/README.md
index 3ddaf81..0ea1b2a 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
- Schemaless: No dependency on specific HCL application binary or schema
- Support HCL2 (not HCL1)
- Available operations:
- - attribute append / get / mv / rm / set
+ - attribute append / get / mv / replace / rm / set
- block append / get / list / mv / new / rm
- body get
- fmt
@@ -84,6 +84,7 @@ Available Commands:
append Append attribute
get Get attribute
mv Move attribute (Rename attribute key)
+ replace Replace both the name and value of attribute
rm Remove attribute
set Set attribute
@@ -133,6 +134,16 @@ resource "foo" "bar" {
}
```
+```
+$ cat tmp/attr.hcl | hcledit attribute replace resource.foo.bar.nested.attr2 attr3 '"val3"'
+resource "foo" "bar" {
+ attr1 = "val1"
+ nested {
+ attr3 = "val3"
+ }
+}
+```
+
```
$ cat tmp/attr.hcl | hcledit attribute rm resource.foo.bar.attr1
resource "foo" "bar" {
diff --git a/cmd/attribute.go b/cmd/attribute.go
index 3a60b73..7f95eaf 100644
--- a/cmd/attribute.go
+++ b/cmd/attribute.go
@@ -26,6 +26,7 @@ func newAttributeCmd() *cobra.Command {
newAttributeGetCmd(),
newAttributeSetCmd(),
newAttributeMvCmd(),
+ newAttributeReplaceCmd(),
newAttributeRmCmd(),
newAttributeAppendCmd(),
)
@@ -80,9 +81,9 @@ Arguments:
ADDRESS An address of attribute to set.
VALUE A new value of attribute.
The value is set literally, even if references or expressions.
- Thus, if you want to set a string literal "hoge", be sure to
+ Thus, if you want to set a string literal "foo", be sure to
escape double quotes so that they are not discarded by your shell.
- e.g.) hcledit attribute set aaa.bbb.ccc '"hoge"'
+ e.g.) hcledit attribute set aaa.bbb.ccc '"foo"'
`,
RunE: runAttributeSetCmd,
}
@@ -151,6 +152,43 @@ func runAttributeMvCmd(cmd *cobra.Command, args []string) error {
return c.Edit(file, update, filter)
}
+func newAttributeReplaceCmd() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "replace
",
+ Short: "Replace both the name and value of attribute",
+ Long: `Replace both the name and value of matched attribute at a given address
+
+Arguments:
+ ADDRESS An address of attribute to be replaced.
+ NAME A new name (key) of attribute.
+ VALUE A new value of attribute.
+ The value is set literally, even if references or expressions.
+ Thus, if you want to set a string literal "bar", be sure to
+ escape double quotes so that they are not discarded by your shell.
+ e.g.) hcledit attribute replace aaa.bbb.ccc foo '"bar"'
+`,
+ RunE: runAttributeReplaceCmd,
+ }
+
+ return cmd
+}
+
+func runAttributeReplaceCmd(cmd *cobra.Command, args []string) error {
+ if len(args) != 3 {
+ return fmt.Errorf("expected 3 argument, but got %d arguments", len(args))
+ }
+
+ address := args[0]
+ name := args[1]
+ value := args[2]
+ file := viper.GetString("file")
+ update := viper.GetBool("update")
+
+ filter := editor.NewAttributeReplaceFilter(address, name, value)
+ c := newDefaultClient(cmd)
+ return c.Edit(file, update, filter)
+}
+
func runAttributeRmCmd(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("expected 1 argument, but got %d arguments", len(args))
diff --git a/cmd/attribute_test.go b/cmd/attribute_test.go
index 835c8c0..06aa4f6 100644
--- a/cmd/attribute_test.go
+++ b/cmd/attribute_test.go
@@ -260,6 +260,85 @@ resource "foo" "bar" {
}
}
+func TestAttributeReplace(t *testing.T) {
+ src := `terraform {
+ backend "s3" {
+ region = "ap-northeast-1"
+ bucket = "my-s3lock-test"
+ key = "dir1/terraform.tfstate"
+ dynamodb_table = "tflock"
+ profile = "foo"
+ }
+}
+`
+
+ cases := []struct {
+ name string
+ args []string
+ ok bool
+ want string
+ }{
+ {
+ name: "simple",
+ args: []string{"terraform.backend.s3.dynamodb_table", "use_lockfile", "true"},
+ ok: true,
+ want: `terraform {
+ backend "s3" {
+ region = "ap-northeast-1"
+ bucket = "my-s3lock-test"
+ key = "dir1/terraform.tfstate"
+ use_lockfile = true
+ profile = "foo"
+ }
+}
+`,
+ },
+ {
+ name: "no match",
+ args: []string{"terraform.backend.s3.foo_table", "use_lockfile", "true"},
+ ok: true,
+ want: src,
+ },
+ {
+ name: "duplicated",
+ args: []string{"terraform.backend.s3.dynamodb_table", "profile", "true"},
+ ok: false,
+ want: "",
+ },
+ {
+ name: "no args",
+ args: []string{},
+ ok: false,
+ want: "",
+ },
+ {
+ name: "1 arg",
+ args: []string{"foo"},
+ ok: false,
+ want: "",
+ },
+ {
+ name: "2 args",
+ args: []string{"foo", "bar"},
+ ok: false,
+ want: "",
+ },
+ {
+ name: "too many args",
+ args: []string{"foo", "bar", "baz", "qux"},
+ ok: false,
+ want: "",
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ cmd := newMockCmd(newAttributeReplaceCmd(), src)
+ assertMockCmd(t, cmd, tc.args, tc.ok, tc.want)
+ })
+ }
+}
+
func TestAttributeRm(t *testing.T) {
src := `locals {
service = "hoge"
diff --git a/editor/filter_attribute_replace.go b/editor/filter_attribute_replace.go
new file mode 100644
index 0000000..7f12198
--- /dev/null
+++ b/editor/filter_attribute_replace.go
@@ -0,0 +1,62 @@
+package editor
+
+import (
+ "fmt"
+
+ "github.com/hashicorp/hcl/v2/hclwrite"
+)
+
+// AttributeReplaceFilter is a filter implementation for replacing attribute.
+type AttributeReplaceFilter struct {
+ address string
+ name string
+ value string
+}
+
+var _ Filter = (*AttributeReplaceFilter)(nil)
+
+// NewAttributeReplaceFilter creates a new instance of AttributeReplaceFilter.
+func NewAttributeReplaceFilter(address string, name string, value string) Filter {
+ return &AttributeReplaceFilter{
+ address: address,
+ name: name,
+ value: value,
+ }
+}
+
+// Filter reads HCL and replaces both the name and value of matched an
+// attribute at a given address.
+func (f *AttributeReplaceFilter) Filter(inFile *hclwrite.File) (*hclwrite.File, error) {
+ attr, body, err := findAttribute(inFile.Body(), f.address)
+ if err != nil {
+ return nil, err
+ }
+
+ if attr != nil {
+ _, fromAttributeName, err := parseAttributeAddress(f.address)
+ if err != nil {
+ return nil, err
+ }
+ toAttributeName := f.name
+
+ // The Body.RenameAttribute() returns false if fromName does not exist or
+ // toName already exists. However, here, we want to return an error only
+ // if toName already exists, so we check it ourselves.
+ toAttr := body.GetAttribute(toAttributeName)
+ if toAttr != nil {
+ return nil, fmt.Errorf("attribute already exists: %s", toAttributeName)
+ }
+
+ _ = body.RenameAttribute(fromAttributeName, toAttributeName)
+
+ // To delegate expression parsing to the hclwrite parser,
+ // We build a new expression and set back to the attribute by tokens.
+ expr, err := buildExpression(toAttributeName, f.value)
+ if err != nil {
+ return nil, err
+ }
+ body.SetAttributeRaw(toAttributeName, expr.BuildTokens(nil))
+ }
+
+ return inFile, nil
+}
diff --git a/editor/filter_attribute_replace_test.go b/editor/filter_attribute_replace_test.go
new file mode 100644
index 0000000..8791883
--- /dev/null
+++ b/editor/filter_attribute_replace_test.go
@@ -0,0 +1,151 @@
+package editor
+
+import (
+ "testing"
+)
+
+func TestAttributeReplaceFilter(t *testing.T) {
+ cases := []struct {
+ name string
+ src string
+ address string
+ toName string
+ toValue string
+ ok bool
+ want string
+ }{
+ {
+ name: "simple",
+ src: `
+a0 = v0
+a1 = v1
+`,
+ address: "a0",
+ toName: "a2",
+ toValue: "v2",
+ ok: true,
+ want: `
+a2 = v2
+a1 = v1
+`,
+ },
+ {
+ name: "with comments",
+ src: `
+# before attr
+a0 = "v0" # inline
+a1 = "v1"
+`,
+ address: "a0",
+ toName: "a2",
+ toValue: `"v2"`,
+ ok: true,
+ want: `
+# before attr
+a2 = "v2" # inline
+a1 = "v1"
+`,
+ },
+ {
+ name: "attribute in block",
+ src: `
+a0 = v0
+b1 "l1" {
+ a1 = v1
+}
+`,
+ address: "b1.l1.a1",
+ toName: "a2",
+ toValue: "v2",
+ ok: true,
+ want: `
+a0 = v0
+b1 "l1" {
+ a2 = v2
+}
+`,
+ },
+ {
+ name: "not found",
+ src: `
+a0 = v0
+`,
+ address: "a1",
+ toName: "a2",
+ toValue: "v2",
+ ok: true,
+ want: `
+a0 = v0
+`,
+ },
+ {
+ name: "attribute not found in block",
+ src: `
+a0 = v0
+b1 "l1" {
+ a1 = v1
+}
+`,
+ address: "b1.l1.a2",
+ toName: "a3",
+ toValue: "v3",
+ ok: true,
+ want: `
+a0 = v0
+b1 "l1" {
+ a1 = v1
+}
+`,
+ },
+ {
+ name: "block not found",
+ src: `
+a0 = v0
+b1 "l1" {
+ a1 = v1
+}
+`,
+ address: "b2.l1.a1",
+ toName: "a2",
+ toValue: "v2",
+ ok: true,
+ want: `
+a0 = v0
+b1 "l1" {
+ a1 = v1
+}
+`,
+ },
+ {
+ name: "attribute already exists",
+ src: `
+a0 = v0
+a1 = v1
+`,
+ address: "a0",
+ toName: "a1",
+ toValue: "v2",
+ ok: false,
+ want: "",
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ o := NewEditOperator(NewAttributeReplaceFilter(tc.address, tc.toName, tc.toValue))
+ output, err := o.Apply([]byte(tc.src), "test")
+ if tc.ok && err != nil {
+ t.Fatalf("unexpected err = %s", err)
+ }
+
+ got := string(output)
+ 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)
+ }
+ })
+ }
+}