diff --git a/README.md b/README.md index 85132ea6..41324ae7 100644 --- a/README.md +++ b/README.md @@ -449,6 +449,8 @@ colourCodes: rgb: 0000ff ``` +You can escape values in selectors using a backslash `\`. The main use for this is to allow you to target fields that contain a dot or space in their name. + ### Property Property selectors are used to reference a single property of an object. @@ -483,7 +485,7 @@ The next available index selector is used when adding to a list of items. It all #### Any Index The any index selector is used to select *all* items of a list. -- `colours[*]` +- `colours.[*]` This must be used in conjunction with `-m`,`--multiple`. diff --git a/internal/command/root_put_test.go b/internal/command/root_put_test.go index 77bfcff9..b180ac7c 100644 --- a/internal/command/root_put_test.go +++ b/internal/command/root_put_test.go @@ -37,23 +37,23 @@ func TestRootCMD_Put(t *testing.T) { t.Run("InvalidSingleSelector", expectErrFromInput( `{"name": "Tom"}`, []string{"put", "string", "-f", "stdin", "-o", "stdout", "-p", "json", "-s", "[-]", "Frank"}, - "selector is not supported here: [-]", + "invalid index: -", )) t.Run("InvalidMultiSelector", expectErrFromInput( `{"name": "Tom"}`, []string{"put", "string", "-f", "stdin", "-o", "stdout", "-p", "json", "-m", "-s", "[-]", "Frank"}, - "selector is not supported here: [-]", + "invalid index: -", )) t.Run("InvalidObjectSingleSelector", expectErrFromInput( `{"name": "Tom"}`, []string{"put", "object", "-f", "stdin", "-o", "stdout", "-p", "json", "-t", "string", "-s", "[-]", "Frank"}, - "selector is not supported here: [-]", + "invalid index: -", )) t.Run("InvalidMultiSelector", expectErrFromInput( `{"name": "Tom"}`, []string{"put", "object", "-f", "stdin", "-o", "stdout", "-p", "json", "-m", "-t", "string", "-s", "[-]", "Frank"}, - "selector is not supported here: [-]", + "invalid index: -", )) } @@ -317,6 +317,14 @@ id: x id: "1" --- id: z +`, nil)) + + t.Run("StringWithDotInName", putStringTest(` +id: "asd" +my.name: "Tom" +`, "yaml", `my\.name`, "Jim", ` +id: asd +my.name: Jim `, nil)) } diff --git a/internal/command/root_select_test.go b/internal/command/root_select_test.go index 05dabeb4..07e3e205 100644 --- a/internal/command/root_select_test.go +++ b/internal/command/root_select_test.go @@ -125,12 +125,12 @@ func TestRootCMD_Select(t *testing.T) { t.Run("InvalidSingleSelector", expectErrFromInput( `{"name": "Tom"}`, []string{"select", "-p", "json", "-s", "[-]"}, - "selector is not supported here: [-]", + "invalid index: -", )) t.Run("InvalidMultiSelector", expectErrFromInput( `{"name": "Tom"}`, []string{"select", "-p", "json", "-m", "-s", "[-]"}, - "selector is not supported here: [-]", + "invalid index: -", )) } @@ -229,7 +229,7 @@ func TestRootCmd_Select_JSON(t *testing.T) { t.Run("DynamicString", selectTest(jsonData, "json", ".details.addresses.(postcode=YYY YYY).street", newline(`"34 Another Street"`), nil)) t.Run("QueryFromFile", selectTestFromFile("./../../tests/assets/example.json", ".preferences.favouriteColour", newline(`"red"`), nil)) - t.Run("MultiProperty", selectTest(jsonData, "json", ".details.addresses[*].street", newline(`"101 Some Street" + t.Run("MultiProperty", selectTest(jsonData, "json", ".details.addresses.[*].street", newline(`"101 Some Street" "34 Another Street"`), nil, "-m")) t.Run("MultiRoot", selectTest(jsonDataSingle, "json", ".", newline(`{ diff --git a/node.go b/node.go index 085534c0..bcda4b8c 100644 --- a/node.go +++ b/node.go @@ -104,39 +104,18 @@ func ParseSelector(selector string) (Selector, error) { Conditions: make([]Condition, 0), } - if match := propertyRegexp.FindStringSubmatch(selector); len(match) != 0 { - sel.Type = "PROPERTY" - sel.Current = match[0] - sel.Property = match[1] - } else if match := indexRegexp.FindStringSubmatch(selector); len(match) != 0 { - sel.Current = match[0] - switch match[1] { - case "": - sel.Type = "NEXT_AVAILABLE_INDEX" - case "*": - sel.Type = "INDEX_ANY" - default: - sel.Type = "INDEX" - var err error - index, err := strconv.ParseInt(match[1], 10, 32) - if err != nil { - return sel, &InvalidIndexErr{Index: match[1]} - } - sel.Index = int(index) - } - } else { - // todo : re-work this logic to base the entire parsing using ExtractNextSelector. - // This will be much easier instead of regex. - nextSelectorString := ExtractNextSelector(selector) - - // Check if the selector starts with an open bracket. - if !strings.HasPrefix(strings.TrimPrefix(nextSelectorString, "."), "(") { - return sel, &UnsupportedSelector{Selector: nextSelectorString} - } + { + nextSelector, read := ExtractNextSelector(sel.Raw) + sel.Current = nextSelector + sel.Remaining = sel.Raw[read:] + } - sel.Current = nextSelectorString + nextSel := strings.TrimPrefix(sel.Current, ".") - dynamicGroups, err := DynamicSelectorToGroups(nextSelectorString) + switch { + case strings.HasPrefix(nextSel, "(") && strings.HasSuffix(nextSel, ")"): + sel.Type = "DYNAMIC" + dynamicGroups, err := DynamicSelectorToGroups(nextSel) if err != nil { return sel, err } @@ -158,10 +137,25 @@ func ParseSelector(selector string) (Selector, error) { sel.Conditions = append(sel.Conditions, cond) } - sel.Type = "DYNAMIC" - } + case nextSel == "[]": + sel.Type = "NEXT_AVAILABLE_INDEX" - sel.Remaining = strings.TrimPrefix(sel.Raw, sel.Current) + case nextSel == "[*]": + sel.Type = "INDEX_ANY" + + case strings.HasPrefix(nextSel, "[") && strings.HasSuffix(nextSel, "]"): + sel.Type = "INDEX" + indexStr := nextSel[1 : len(nextSel)-1] + index, err := strconv.ParseInt(indexStr, 10, 32) + if err != nil { + return sel, &InvalidIndexErr{Index: indexStr} + } + sel.Index = int(index) + + default: + sel.Type = "PROPERTY" + sel.Property = nextSel + } return sel, nil } diff --git a/dynamic_parser.go b/selector.go similarity index 79% rename from dynamic_parser.go rename to selector.go index a64eae4c..9acf26c7 100644 --- a/dynamic_parser.go +++ b/selector.go @@ -9,22 +9,38 @@ import ( var ErrDynamicSelectorBracketMismatch = errors.New("dynamic selector bracket mismatch") // ExtractNextSelector returns the next selector from the given input. -func ExtractNextSelector(input string) string { +func ExtractNextSelector(input string) (string, int) { + escapedIndex := -1 res := "" i := 0 + read := 0 for k, v := range input { + if escapedIndex == k-1 && k != 0 { + // last character was escape character + res += string(v) + read++ + continue + } + if v == '(' || v == '[' { i++ } else if v == ')' || v == ']' { i-- } + if v == '\\' { + escapedIndex = k + read++ + continue + } + if i == 0 && v == '.' && k != 0 { break } res += string(v) + read++ } - return res + return res, read } // DynamicSelectorToGroups takes a dynamic selector and splits it into groups. diff --git a/selector_test.go b/selector_test.go new file mode 100644 index 00000000..f20e4a62 --- /dev/null +++ b/selector_test.go @@ -0,0 +1,28 @@ +package dasel_test + +import ( + "github.com/tomwright/dasel" + "testing" +) + +func testExtractNextSelector(in string, exp string, expRead int) func(t *testing.T) { + return func(t *testing.T) { + got, read := dasel.ExtractNextSelector(in) + if exp != got { + t.Errorf("expected %v, got %v", exp, got) + } + if read != expRead { + t.Errorf("expected read of %d, got %d", expRead, read) + } + } +} + +func TestExtractNextSelector(t *testing.T) { + t.Run("Simple", testExtractNextSelector(`.metadata.name`, `.metadata`, 9)) + t.Run("EscapedDot", testExtractNextSelector(`.before\.after.name`, `.before.after`, 14)) + t.Run("EscapedSpace", testExtractNextSelector(`.before\ after.name`, `.before after`, 14)) + t.Run("DynamicWithPath", testExtractNextSelector(`.(.before.a=b).after.name`, `.(.before.a=b)`, 14)) + t.Run("EscapedFirstDot", testExtractNextSelector(`\.name`, `.name`, 6)) + t.Run("SimpleProp", testExtractNextSelector(`.name`, `.name`, 5)) + t.Run("SimpleIndex", testExtractNextSelector(`.[123]`, `.[123]`, 6)) +}