From c4af37ed680bea8fa54dfa7da75ad102f93b0362 Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Sat, 3 Oct 2015 15:10:29 +1000 Subject: [PATCH] Custom path parsing --- README.md | 18 ++++++++++---- data_navigator.go | 1 + data_navigator_test.go | 5 ++-- path_parser.go | 55 ++++++++++++++++++++++++++++++++++++++++++ path_parser_test.go | 42 ++++++++++++++++++++++++++++++++ yaml.go | 6 ++--- 6 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 path_parser.go create mode 100644 path_parser_test.go diff --git a/README.md b/README.md index 80cbb79b..f2aaf7d9 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,18 @@ yaml sample.yaml b.c ``` will output the value of '2'. +### Handling '.' in the yaml key +Given a sample.yaml file of: +```yaml +b.x: + c: 2 +``` +then +```bash +yaml sample.yaml \"b.x\".c +``` +will output the value of '2'. + ### Arrays You can give an index to access a specific element: e.g.: given a sample file of @@ -40,7 +52,7 @@ b: ``` then ``` -yaml sample.yaml b.e.1.name +yaml sample.yaml b.e[1].name ``` will output 'sam' @@ -59,7 +71,3 @@ will output: b: c: cat ``` - - -## TODO -* Handling '.' in path names diff --git a/data_navigator.go b/data_navigator.go index efe8a4a7..4983c36e 100644 --- a/data_navigator.go +++ b/data_navigator.go @@ -1,6 +1,7 @@ package main import ( + // "fmt" "log" "strconv" ) diff --git a/data_navigator_test.go b/data_navigator_test.go index 1e7451ee..71f34d80 100644 --- a/data_navigator_test.go +++ b/data_navigator_test.go @@ -65,8 +65,9 @@ func assertResult(t *testing.T, expectedValue interface{}, actualValue interface } } -func assertResultWithContext(t *testing.T, expectedValue interface{}, actualValue interface{}, testDescription string) { +func assertResultWithContext(t *testing.T, expectedValue interface{}, actualValue interface{}, context interface{}) { if expectedValue != actualValue { - t.Error(testDescription, ": expected <", expectedValue, "> but got <", actualValue, ">") + t.Error(context) + t.Error(": expected <", expectedValue, "> but got <", actualValue, ">") } } diff --git a/path_parser.go b/path_parser.go new file mode 100644 index 00000000..80ce0dd5 --- /dev/null +++ b/path_parser.go @@ -0,0 +1,55 @@ +package main + +func parsePath(path string) []string { + return parsePathAccum([]string{}, path) +} + +func parsePathAccum(paths []string, remaining string) []string { + head, tail := nextYamlPath(remaining) + if tail == "" { + return append(paths, head) + } + return parsePathAccum(append(paths, head), tail) +} + +func nextYamlPath(path string) (pathElement string, remaining string) { + switch path[0] { + case '[': + // e.g [0].blah.cat -> we need to return "0" and "blah.cat" + return search(path[1:len(path)], []uint8{']'}, true) + case '"': + // e.g "a.b".blah.cat -> we need to return "a.b" and "blah.cat" + return search(path[1:len(path)], []uint8{'"'}, true) + default: + // e.g "a.blah.cat" -> return "a" and "blah.cat" + return search(path[0:len(path)], []uint8{'.', '['}, false) + } +} + +func search(path string, matchingChars []uint8, skipNext bool) (pathElement string, remaining string) { + for i := 0; i < len(path); i++ { + var char = path[i] + if contains(matchingChars, char) { + var remainingStart = i + 1 + if skipNext { + remainingStart = remainingStart + 1 + } else if !skipNext && char != '.' { + remainingStart = i + } + if remainingStart > len(path) { + remainingStart = len(path) + } + return path[0:i], path[remainingStart:len(path)] + } + } + return path, "" +} + +func contains(matchingChars []uint8, candidate uint8) bool { + for _, a := range matchingChars { + if a == candidate { + return true + } + } + return false +} diff --git a/path_parser_test.go b/path_parser_test.go new file mode 100644 index 00000000..c93163d7 --- /dev/null +++ b/path_parser_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "testing" +) + +var parsePathsTests = []struct { + path string + expectedPaths []string +}{ + {"a.b", []string{"a", "b"}}, + {"a.b[0]", []string{"a", "b", "0"}}, +} + +func testParsePath(t *testing.T) { + for _, tt := range parsePathsTests { + assertResultWithContext(t, tt.expectedPaths, parsePath(tt.path), tt) + } +} + +var nextYamlPathTests = []struct { + path string + expectedElement string + expectedRemaining string +}{ + {"a.b", "a", "b"}, + {"a", "a", ""}, + {"a.b.c", "a", "b.c"}, + {"\"a.b\".c", "a.b", "c"}, + {"a.\"b.c\".d", "a", "\"b.c\".d"}, + {"[1].a.d", "1", "a.d"}, + {"a[0].c", "a", "[0].c"}, + {"[0]", "0", ""}, +} + +func TestNextYamlPath(t *testing.T) { + for _, tt := range nextYamlPathTests { + var element, remaining = nextYamlPath(tt.path) + assertResultWithContext(t, tt.expectedElement, element, tt) + assertResultWithContext(t, tt.expectedRemaining, remaining, tt) + } +} diff --git a/yaml.go b/yaml.go index 9b985d43..13966fa1 100644 --- a/yaml.go +++ b/yaml.go @@ -51,8 +51,7 @@ func readProperty(c *cli.Context) { os.Exit(0) } - var path = c.Args()[1] - var paths = strings.Split(path, ".") + var paths = parsePath(c.Args()[1]) printYaml(readMap(parsedData, paths[0], paths[1:len(paths)]), c.Bool("trim")) } @@ -71,8 +70,7 @@ func writeProperty(c *cli.Context) { forceString = true } - var path = c.Args()[1] - var paths = strings.Split(path, ".") + var paths = parsePath(c.Args()[1]) write(parsedData, paths[0], paths[1:len(paths)], getValue(c.Args()[2], forceString))