Repository: TomWright/dasel
Pull request #38 (2020-11-12 15:47:30)

Escape selectors
Allow escape characters in selectors.


Patch:
diff --git a/README.md b/README.md
index 85132ea..41324ae 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/node.go b/node.go
index 085534c..bcda4b8 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 a64eae4..9acf26c 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.


Test patch:
diff --git a/internal/command/root_put_test.go b/internal/command/root_put_test.go
index 77bfcff..b180ac7 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 05dabeb..07e3e20 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/selector_test.go b/selector_test.go
new file mode 100644
index 0000000..f20e4a6
--- /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))
+}