From fea2d8d1c8856f5afb65453d8e78fedc9b859057 Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Fri, 11 Aug 2023 10:35:12 +0000 Subject: [PATCH] feat: add support for escaping . in address --- README.md | 20 +++++++ editor/address.go | 42 +++++++++++++++ editor/address_test.go | 75 ++++++++++++++++++++++++++ editor/filter_attribute_append.go | 5 +- editor/filter_attribute_append_test.go | 20 +++++++ editor/filter_attribute_remove_test.go | 18 +++++++ editor/filter_attribute_set_test.go | 18 +++++++ editor/filter_block_append_test.go | 27 ++++++++++ editor/filter_block_get.go | 3 +- editor/filter_block_get_test.go | 20 +++++++ editor/filter_block_remove_test.go | 15 ++++++ editor/filter_block_rename_test.go | 23 ++++++++ editor/filter_body_get_test.go | 16 ++++++ editor/sink_attribute_get.go | 13 +++-- editor/sink_attribute_get_test.go | 11 ++++ editor/sink_block_list.go | 5 +- editor/sink_block_list_test.go | 4 ++ 17 files changed, 320 insertions(+), 15 deletions(-) create mode 100644 editor/address.go create mode 100644 editor/address_test.go diff --git a/README.md b/README.md index 62f826d..8dd85f7 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,26 @@ resource "foo" "bar" { } ``` +### Address escaping + +Address escaping is supported for labels that contain `.`. + +Given the following file: + +```body.hcl +resource "foo.bar" { + attr1 = "val1" + nested { + attr2 = "val2" + } +} +``` + +``` +$ cat tmp/attr.hcl | hcledit attribute get 'resource.foo\.bar.nested.attr2' +"val2" +``` + ## License MIT diff --git a/editor/address.go b/editor/address.go new file mode 100644 index 0000000..af5491c --- /dev/null +++ b/editor/address.go @@ -0,0 +1,42 @@ +package editor + +import ( + "strings" +) + +func createAddressFromString(address string) []string { + var separator byte = '.' + var escapeString byte = '\\' + + var result []string + var token []byte + for i := 0; i < len(address); i++ { + if address[i] == separator { + result = append(result, string(token)) + token = token[:0] + } else if address[i] == escapeString && i+1 < len(address) { + i++ + token = append(token, address[i]) + } else { + token = append(token, address[i]) + } + } + result = append(result, string(token)) + return result +} + +func createStringFromAddress(address []string) string { + separator := "." + escapeString := "\\" + + result := "" + + for i, s := range address { + if i > 0 { + result = result + separator + } + result = result + strings.ReplaceAll(s, separator, escapeString+separator) + } + + return result +} diff --git a/editor/address_test.go b/editor/address_test.go new file mode 100644 index 0000000..a10fe1e --- /dev/null +++ b/editor/address_test.go @@ -0,0 +1,75 @@ +package editor + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestCreateAddressFromString(t *testing.T) { + cases := []struct { + name string + address string + want []string + }{ + { + name: "simple address", + address: "b1", + want: []string{"b1"}, + }, + { + name: "attribute address", + address: "b1.l1.a1", + want: []string{"b1", "l1", "a1"}, + }, + { + name: "escaped address", + address: `b1.l\.1.a1`, + want: []string{"b1", "l.1", "a1"}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := createAddressFromString(tc.address) + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("got:\n%s\nwant:\n%s\ndiff(-want +got):\n%v", got, tc.want, diff) + } + }) + } +} + +func TestCreateStringFromAddress(t *testing.T) { + cases := []struct { + name string + address []string + want string + }{ + { + name: "simple address", + address: []string{"b1"}, + want: "b1", + }, + { + name: "simple address", + address: []string{"b1", "l1", "a1"}, + want: "b1.l1.a1", + }, + { + name: "simple address", + address: []string{"b1", `l.1`, "a1"}, + want: `b1.l\.1.a1`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := createStringFromAddress(tc.address) + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("got:\n%s\nwant:\n%s\ndiff(-want +got):\n%v", got, tc.want, diff) + } + }) + } +} diff --git a/editor/filter_attribute_append.go b/editor/filter_attribute_append.go index e255d20..2b15698 100644 --- a/editor/filter_attribute_append.go +++ b/editor/filter_attribute_append.go @@ -2,7 +2,6 @@ package editor import ( "fmt" - "strings" "github.com/hashicorp/hcl/v2/hclwrite" ) @@ -33,12 +32,12 @@ func (f *AttributeAppendFilter) Filter(inFile *hclwrite.File) (*hclwrite.File, e attrName := f.address body := inFile.Body() - a := strings.Split(f.address, ".") + a := createAddressFromString(f.address) if len(a) > 1 { // if address contains dots, the last element is an attribute name, // and the rest is the address of the block. attrName = a[len(a)-1] - blockAddr := strings.Join(a[:len(a)-1], ".") + blockAddr := createStringFromAddress(a[:len(a)-1]) blocks, err := findLongestMatchingBlocks(body, blockAddr) if err != nil { return nil, err diff --git a/editor/filter_attribute_append_test.go b/editor/filter_attribute_append_test.go index bfb0f53..4148540 100644 --- a/editor/filter_attribute_append_test.go +++ b/editor/filter_attribute_append_test.go @@ -102,6 +102,26 @@ b1 "l1" { ok: false, want: ``, }, + { + name: "escaped address", + src: ` +a0 = v0 +b1 "l.1" { + a1 = v1 +} +`, + address: `b1.l\.1.a2`, + value: "v2", + newline: false, + ok: true, + want: ` +a0 = v0 +b1 "l.1" { + a1 = v1 + a2 = v2 +} +`, + }, } for _, tc := range cases { diff --git a/editor/filter_attribute_remove_test.go b/editor/filter_attribute_remove_test.go index 87f003f..173168e 100644 --- a/editor/filter_attribute_remove_test.go +++ b/editor/filter_attribute_remove_test.go @@ -98,6 +98,24 @@ a0 = v0 b1 "l1" { a1 = v1 } +`, + }, + { + name: "escaped address", + src: ` +a0 = v0 +b1 "l.1" { + a1 = v1 + a2 = v2 +} +`, + address: `b1.l\.1.a1`, + ok: true, + want: ` +a0 = v0 +b1 "l.1" { + a2 = v2 +} `, }, } diff --git a/editor/filter_attribute_set_test.go b/editor/filter_attribute_set_test.go index 275b305..695f711 100644 --- a/editor/filter_attribute_set_test.go +++ b/editor/filter_attribute_set_test.go @@ -149,6 +149,24 @@ a0 = v0 b1 "l1" { a1 = v1 } +`, + }, + { + name: "escaped address", + src: ` +a0 = v0 +b1 "l.1" { + a1 = v1 +} +`, + address: `b1.l\.1.a1`, + value: "v2", + ok: true, + want: ` +a0 = v0 +b1 "l.1" { + a1 = v2 +} `, }, } diff --git a/editor/filter_block_append_test.go b/editor/filter_block_append_test.go index c91447a..334a6a0 100644 --- a/editor/filter_block_append_test.go +++ b/editor/filter_block_append_test.go @@ -150,6 +150,33 @@ b1 { b11 { } } +`, + }, + { + name: "escaped address", + src: ` +a0 = v0 +b1 { + a2 = v2 +} + +b1 "l.1" { +} +`, + parent: `b1.l\.1`, + child: `b11.l\.1.l12`, + newline: false, + ok: true, + want: ` +a0 = v0 +b1 { + a2 = v2 +} + +b1 "l.1" { + b11 "l.1" "l12" { + } +} `, }, } diff --git a/editor/filter_block_get.go b/editor/filter_block_get.go index fb72d92..073d5a3 100644 --- a/editor/filter_block_get.go +++ b/editor/filter_block_get.go @@ -2,7 +2,6 @@ package editor import ( "fmt" - "strings" "github.com/hashicorp/hcl/v2/hclwrite" ) @@ -59,7 +58,7 @@ func parseAddress(address string) (string, []string, error) { return "", []string{}, fmt.Errorf("failed to parse address: %s", address) } - a := strings.Split(address, ".") + a := createAddressFromString(address) typeName := a[0] labels := []string{} if len(a) > 1 { diff --git a/editor/filter_block_get_test.go b/editor/filter_block_get_test.go index cfa11ab..6e6253a 100644 --- a/editor/filter_block_get_test.go +++ b/editor/filter_block_get_test.go @@ -237,6 +237,26 @@ b1 { want: `b3 { a3 = v3 } +`, + }, + { + name: "escaped address", + src: ` +b1 "b.2" { + a1 = v1 + b2 { + a1 = v2 + } +} +`, + address: `b1.b\.2`, + ok: true, + want: `b1 "b.2" { + a1 = v1 + b2 { + a1 = v2 + } +} `, }, } diff --git a/editor/filter_block_remove_test.go b/editor/filter_block_remove_test.go index fefb06b..2125443 100644 --- a/editor/filter_block_remove_test.go +++ b/editor/filter_block_remove_test.go @@ -114,6 +114,21 @@ b1 l1 l3 { b1 l1 { } +`, + }, + { + name: "escaped address", + src: ` +b1 { +} + +b1 "l.1" { +} +`, + address: `b1.l\.1`, + ok: true, + want: `b1 { +} `, }, } diff --git a/editor/filter_block_rename_test.go b/editor/filter_block_rename_test.go index 9c44b96..3af343f 100644 --- a/editor/filter_block_rename_test.go +++ b/editor/filter_block_rename_test.go @@ -31,6 +31,29 @@ b1 "l2" { a2 = v2 } +b2 "l2" { +} +`, + }, + + { + name: "escaped address", + src: `a0 = v0 +b1 "l.1" { + a2 = v2 +} + +b2 "l2" { +} +`, + from: `b1.l\.1`, + to: `b1.l\.2`, + ok: true, + want: `a0 = v0 +b1 "l.2" { + a2 = v2 +} + b2 "l2" { } `, diff --git a/editor/filter_body_get_test.go b/editor/filter_body_get_test.go index bab04fa..3cb8748 100644 --- a/editor/filter_body_get_test.go +++ b/editor/filter_body_get_test.go @@ -113,6 +113,22 @@ b1 { b2 { a2 = v2 } +`, + }, + { + name: "escaped address", + src: ` +b1 { + a1 = v1 +} + +b1 "l.1" { + a2 = v2 +} +`, + address: `b1.l\.1`, + ok: true, + want: `a2 = v2 `, }, } diff --git a/editor/sink_attribute_get.go b/editor/sink_attribute_get.go index 7ca282d..7611ecd 100644 --- a/editor/sink_attribute_get.go +++ b/editor/sink_attribute_get.go @@ -36,7 +36,6 @@ func (s *AttributeGetSink) Sink(inFile *hclwrite.File) ([]byte, error) { // treat expr as a string without interpreting its meaning. out, err := GetAttributeValueAsString(attr) - if err != nil { return []byte{}, err } @@ -45,7 +44,7 @@ func (s *AttributeGetSink) Sink(inFile *hclwrite.File) ([]byte, error) { } // findAttribute returns first matching attribute at a given address. -// If the address does not cantain any dots, find attribute in the body. +// If the address does not contain any dots, find attribute in the body. // If the address contains dots, the last element is an attribute name, // and the rest is the address of the block. // The block is fetched by findLongestMatchingBlocks. @@ -55,9 +54,9 @@ func findAttribute(body *hclwrite.Body, address string) (*hclwrite.Attribute, *h return nil, nil, errors.New("failed to parse address. address is empty") } - a := strings.Split(address, ".") + a := createAddressFromString(address) if len(a) == 1 { - // if the address does not cantain any dots, find attribute in the body. + // if the address does not contain any dots, find attribute in the body. attr := body.GetAttribute(a[0]) return attr, body, nil } @@ -65,7 +64,7 @@ func findAttribute(body *hclwrite.Body, address string) (*hclwrite.Attribute, *h // if address contains dots, the last element is an attribute name, // and the rest is the address of the block. attrName := a[len(a)-1] - blockAddr := strings.Join(a[:len(a)-1], ".") + blockAddr := createStringFromAddress(a[:len(a)-1]) blocks, err := findLongestMatchingBlocks(body, blockAddr) if err != nil { return nil, nil, err @@ -113,7 +112,7 @@ func findLongestMatchingBlocks(body *hclwrite.Body, address string) ([]*hclwrite return nil, errors.New("failed to parse address. address is empty") } - a := strings.Split(address, ".") + a := createAddressFromString(address) typeName := a[0] blocks := allMatchingBlocksByType(body, typeName) @@ -136,7 +135,7 @@ func findLongestMatchingBlocks(body *hclwrite.Body, address string) ([]*hclwrite } if len(matchedlabels) < (len(a)-1) || len(labels) == 0 { // if the block has no labels or partially matched ones, find the nested block - nestedAddr := strings.Join(a[1+len(matchedlabels):], ".") + nestedAddr := createStringFromAddress(a[1+len(matchedlabels):]) nested, err := findLongestMatchingBlocks(b.Body(), nestedAddr) if err != nil { return nil, err diff --git a/editor/sink_attribute_get_test.go b/editor/sink_attribute_get_test.go index 8dd052a..545abeb 100644 --- a/editor/sink_attribute_get_test.go +++ b/editor/sink_attribute_get_test.go @@ -223,6 +223,17 @@ b1 "l1" "l2" { ok: true, want: "v1\n", }, + { + name: "attribute in block with a escaped address", + src: ` +b1 "l.1" { + a1 = v1 +} +`, + address: `b1.l\.1.a1`, + ok: true, + want: "v1\n", + }, } for _, tc := range cases { diff --git a/editor/sink_block_list.go b/editor/sink_block_list.go index 99d36e5..67b70aa 100644 --- a/editor/sink_block_list.go +++ b/editor/sink_block_list.go @@ -7,8 +7,7 @@ import ( ) // BlockListSink is a sink implementation for getting a list of block addresses. -type BlockListSink struct { -} +type BlockListSink struct{} var _ Sink = (*BlockListSink)(nil) @@ -36,5 +35,5 @@ func toAddress(b *hclwrite.Block) string { addr := []string{} addr = append(addr, b.Type()) addr = append(addr, (b.Labels())...) - return strings.Join(addr, ".") + return createStringFromAddress(addr) } diff --git a/editor/sink_block_list_test.go b/editor/sink_block_list_test.go index e107bc9..5647910 100644 --- a/editor/sink_block_list_test.go +++ b/editor/sink_block_list_test.go @@ -21,10 +21,14 @@ b1 { b2 l1 { } + +b2 "l.1" { +} `, ok: true, want: `b1 b2.l1 +b2.l\.1 `, }, {