Skip to content

Commit 80d76cc

Browse files
authored
Merge pull request #111 from minamijoyo/attribute-replace
Add attribute mv and replace commands
2 parents f1b2e5d + 50f0f6c commit 80d76cc

File tree

10 files changed

+714
-21
lines changed

10 files changed

+714
-21
lines changed

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
- Schemaless: No dependency on specific HCL application binary or schema
1111
- Support HCL2 (not HCL1)
1212
- Available operations:
13-
- attribute append/get/rm/set
14-
- block append/get/list/mv/new/rm
13+
- attribute append / get / mv / replace / rm / set
14+
- block append / get / list / mv / new / rm
1515
- body get
1616
- fmt
1717

@@ -83,6 +83,8 @@ Usage:
8383
Available Commands:
8484
append Append attribute
8585
get Get attribute
86+
mv Move attribute (Rename attribute key)
87+
replace Replace both the name and value of attribute
8688
rm Remove attribute
8789
set Set attribute
8890
@@ -122,6 +124,26 @@ resource "foo" "bar" {
122124
}
123125
```
124126

127+
```
128+
$ cat tmp/attr.hcl | hcledit attribute mv resource.foo.bar.nested.attr2 resource.foo.bar.nested.attr3
129+
resource "foo" "bar" {
130+
attr1 = "val1"
131+
nested {
132+
attr3 = "val2"
133+
}
134+
}
135+
```
136+
137+
```
138+
$ cat tmp/attr.hcl | hcledit attribute replace resource.foo.bar.nested.attr2 attr3 '"val3"'
139+
resource "foo" "bar" {
140+
attr1 = "val1"
141+
nested {
142+
attr3 = "val3"
143+
}
144+
}
145+
```
146+
125147
```
126148
$ cat tmp/attr.hcl | hcledit attribute rm resource.foo.bar.attr1
127149
resource "foo" "bar" {

cmd/attribute.go

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ func newAttributeCmd() *cobra.Command {
2525
cmd.AddCommand(
2626
newAttributeGetCmd(),
2727
newAttributeSetCmd(),
28+
newAttributeMvCmd(),
29+
newAttributeReplaceCmd(),
2830
newAttributeRmCmd(),
2931
newAttributeAppendCmd(),
3032
)
@@ -79,9 +81,9 @@ Arguments:
7981
ADDRESS An address of attribute to set.
8082
VALUE A new value of attribute.
8183
The value is set literally, even if references or expressions.
82-
Thus, if you want to set a string literal "hoge", be sure to
84+
Thus, if you want to set a string literal "foo", be sure to
8385
escape double quotes so that they are not discarded by your shell.
84-
e.g.) hcledit attribute set aaa.bbb.ccc '"hoge"'
86+
e.g.) hcledit attribute set aaa.bbb.ccc '"foo"'
8587
`,
8688
RunE: runAttributeSetCmd,
8789
}
@@ -119,6 +121,74 @@ Arguments:
119121
return cmd
120122
}
121123

124+
func newAttributeMvCmd() *cobra.Command {
125+
cmd := &cobra.Command{
126+
Use: "mv <FROM_ADDRESS> <TO_ADDRESS>",
127+
Short: "Move attribute (Rename attribute key)",
128+
Long: `Move attribute (Rename attribute key)
129+
130+
Arguments:
131+
FROM_ADDRESS An old address of attribute.
132+
TO_ADDRESS A new address of attribute.
133+
`,
134+
RunE: runAttributeMvCmd,
135+
}
136+
137+
return cmd
138+
}
139+
140+
func runAttributeMvCmd(cmd *cobra.Command, args []string) error {
141+
if len(args) != 2 {
142+
return fmt.Errorf("expected 2 argument, but got %d arguments", len(args))
143+
}
144+
145+
from := args[0]
146+
to := args[1]
147+
file := viper.GetString("file")
148+
update := viper.GetBool("update")
149+
150+
filter := editor.NewAttributeRenameFilter(from, to)
151+
c := newDefaultClient(cmd)
152+
return c.Edit(file, update, filter)
153+
}
154+
155+
func newAttributeReplaceCmd() *cobra.Command {
156+
cmd := &cobra.Command{
157+
Use: "replace <ADDRESS> <NAME> <VALUE>",
158+
Short: "Replace both the name and value of attribute",
159+
Long: `Replace both the name and value of matched attribute at a given address
160+
161+
Arguments:
162+
ADDRESS An address of attribute to be replaced.
163+
NAME A new name (key) of attribute.
164+
VALUE A new value of attribute.
165+
The value is set literally, even if references or expressions.
166+
Thus, if you want to set a string literal "bar", be sure to
167+
escape double quotes so that they are not discarded by your shell.
168+
e.g.) hcledit attribute replace aaa.bbb.ccc foo '"bar"'
169+
`,
170+
RunE: runAttributeReplaceCmd,
171+
}
172+
173+
return cmd
174+
}
175+
176+
func runAttributeReplaceCmd(cmd *cobra.Command, args []string) error {
177+
if len(args) != 3 {
178+
return fmt.Errorf("expected 3 argument, but got %d arguments", len(args))
179+
}
180+
181+
address := args[0]
182+
name := args[1]
183+
value := args[2]
184+
file := viper.GetString("file")
185+
update := viper.GetBool("update")
186+
187+
filter := editor.NewAttributeReplaceFilter(address, name, value)
188+
c := newDefaultClient(cmd)
189+
return c.Edit(file, update, filter)
190+
}
191+
122192
func runAttributeRmCmd(cmd *cobra.Command, args []string) error {
123193
if len(args) != 1 {
124194
return fmt.Errorf("expected 1 argument, but got %d arguments", len(args))

cmd/attribute_test.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,162 @@ module "hoge" {
183183
}
184184
}
185185

186+
func TestAttributeMv(t *testing.T) {
187+
src := `locals {
188+
foo1 = "bar1"
189+
foo2 = "bar2"
190+
}
191+
192+
resource "foo" "bar" {
193+
foo3 = "bar3"
194+
}
195+
`
196+
197+
cases := []struct {
198+
name string
199+
args []string
200+
ok bool
201+
want string
202+
}{
203+
{
204+
name: "simple",
205+
args: []string{"locals.foo1", "locals.foo3"},
206+
ok: true,
207+
want: `locals {
208+
foo3 = "bar1"
209+
foo2 = "bar2"
210+
}
211+
212+
resource "foo" "bar" {
213+
foo3 = "bar3"
214+
}
215+
`,
216+
},
217+
{
218+
name: "no match",
219+
args: []string{"locals.foo3", "locals.foo4"},
220+
ok: true,
221+
want: src,
222+
},
223+
{
224+
name: "duplicated",
225+
args: []string{"locals.foo1", "locals.foo2"},
226+
ok: false,
227+
want: "",
228+
},
229+
{
230+
name: "move an attribute accross blocks",
231+
args: []string{"locals.foo1", "resource.foo.bar.foo1"},
232+
ok: false,
233+
want: "",
234+
},
235+
{
236+
name: "no args",
237+
args: []string{},
238+
ok: false,
239+
want: "",
240+
},
241+
{
242+
name: "1 arg",
243+
args: []string{"hoge"},
244+
ok: false,
245+
want: "",
246+
},
247+
{
248+
name: "too many args",
249+
args: []string{"hoge", "fuga", "piyo"},
250+
ok: false,
251+
want: "",
252+
},
253+
}
254+
255+
for _, tc := range cases {
256+
t.Run(tc.name, func(t *testing.T) {
257+
cmd := newMockCmd(newAttributeMvCmd(), src)
258+
assertMockCmd(t, cmd, tc.args, tc.ok, tc.want)
259+
})
260+
}
261+
}
262+
263+
func TestAttributeReplace(t *testing.T) {
264+
src := `terraform {
265+
backend "s3" {
266+
region = "ap-northeast-1"
267+
bucket = "my-s3lock-test"
268+
key = "dir1/terraform.tfstate"
269+
dynamodb_table = "tflock"
270+
profile = "foo"
271+
}
272+
}
273+
`
274+
275+
cases := []struct {
276+
name string
277+
args []string
278+
ok bool
279+
want string
280+
}{
281+
{
282+
name: "simple",
283+
args: []string{"terraform.backend.s3.dynamodb_table", "use_lockfile", "true"},
284+
ok: true,
285+
want: `terraform {
286+
backend "s3" {
287+
region = "ap-northeast-1"
288+
bucket = "my-s3lock-test"
289+
key = "dir1/terraform.tfstate"
290+
use_lockfile = true
291+
profile = "foo"
292+
}
293+
}
294+
`,
295+
},
296+
{
297+
name: "no match",
298+
args: []string{"terraform.backend.s3.foo_table", "use_lockfile", "true"},
299+
ok: true,
300+
want: src,
301+
},
302+
{
303+
name: "duplicated",
304+
args: []string{"terraform.backend.s3.dynamodb_table", "profile", "true"},
305+
ok: false,
306+
want: "",
307+
},
308+
{
309+
name: "no args",
310+
args: []string{},
311+
ok: false,
312+
want: "",
313+
},
314+
{
315+
name: "1 arg",
316+
args: []string{"foo"},
317+
ok: false,
318+
want: "",
319+
},
320+
{
321+
name: "2 args",
322+
args: []string{"foo", "bar"},
323+
ok: false,
324+
want: "",
325+
},
326+
{
327+
name: "too many args",
328+
args: []string{"foo", "bar", "baz", "qux"},
329+
ok: false,
330+
want: "",
331+
},
332+
}
333+
334+
for _, tc := range cases {
335+
t.Run(tc.name, func(t *testing.T) {
336+
cmd := newMockCmd(newAttributeReplaceCmd(), src)
337+
assertMockCmd(t, cmd, tc.args, tc.ok, tc.want)
338+
})
339+
}
340+
}
341+
186342
func TestAttributeRm(t *testing.T) {
187343
src := `locals {
188344
service = "hoge"

editor/filter_attribute_rename.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package editor
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/hcl/v2/hclwrite"
7+
)
8+
9+
// AttributeRenameFilter is a filter implementation for renaming attribute.
10+
type AttributeRenameFilter struct {
11+
from string
12+
to string
13+
}
14+
15+
var _ Filter = (*AttributeRenameFilter)(nil)
16+
17+
// NewAttributeRenameFilter creates a new instance of AttributeRenameFilter.
18+
func NewAttributeRenameFilter(from string, to string) Filter {
19+
return &AttributeRenameFilter{
20+
from: from,
21+
to: to,
22+
}
23+
}
24+
25+
// Filter reads HCL and renames matched an attribute at a given address.
26+
// The current implementation does not allow moving an attribute across blocks,
27+
// but it accepts addresses as arguments, which allows for future extensions.
28+
func (f *AttributeRenameFilter) Filter(inFile *hclwrite.File) (*hclwrite.File, error) {
29+
fromAttr, fromBody, err := findAttribute(inFile.Body(), f.from)
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
if fromAttr != nil {
35+
fromBlockAddress, fromAttributeName, err := parseAttributeAddress(f.from)
36+
if err != nil {
37+
return nil, err
38+
}
39+
toBlockAddress, toAttributeName, err := parseAttributeAddress(f.to)
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
if fromBlockAddress == toBlockAddress {
45+
// The Body.RenameAttribute() returns false if fromName does not exist or
46+
// toName already exists. However, here, we want to return an error only
47+
// if toName already exists, so we check it ourselves.
48+
toAttr := fromBody.GetAttribute(toAttributeName)
49+
if toAttr != nil {
50+
return nil, fmt.Errorf("attribute already exists: %s", f.to)
51+
}
52+
53+
_ = fromBody.RenameAttribute(fromAttributeName, toAttributeName)
54+
} else {
55+
return nil, fmt.Errorf("moving an attribute across blocks has not been implemented yet: %s -> %s", f.from, f.to)
56+
}
57+
}
58+
59+
return inFile, nil
60+
}

0 commit comments

Comments
 (0)