diff --git a/.gitignore b/.gitignore index 06ed739b..2a9644ab 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ parts/ prime/ .snapcraft/ yq*.snap +*.swp diff --git a/examples/multidocument.yaml b/examples/multidocument.yaml new file mode 100644 index 00000000..cbcf509e --- /dev/null +++ b/examples/multidocument.yaml @@ -0,0 +1,9 @@ +--- +a: Document One +b: + c: 1 +--- +a: Document Two +b: + c: 2 + diff --git a/utils_test.go b/utils_test.go index 88789194..0396cc41 100644 --- a/utils_test.go +++ b/utils_test.go @@ -60,6 +60,18 @@ func assertResultWithContext(t *testing.T, expectedValue interface{}, actualValu } } +func assertAnyErr(t *testing.T, actualValue error) { + if actualValue == nil { + t.Error("Expected error, got nil") + } +} + +func assertNilErr(t *testing.T, actualValue error) { + if actualValue != nil { + t.Error("Expected nil error, got ", actualValue) + } +} + func writeTempYamlFile(content string) string { tmpfile, _ := ioutil.TempFile("", "testyaml") defer func() { diff --git a/yq.go b/yq.go index 64720102..c2eb92e0 100644 --- a/yq.go +++ b/yq.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "os" + "regexp" "strconv" "strings" @@ -180,16 +181,61 @@ func readProperty(cmd *cobra.Command, args []string) error { return nil } +var regexpSubdocPath = regexp.MustCompile(`^@(\d+)(?:\.)?(.*)?$`) + func read(args []string) (interface{}, error) { - var parsedData yaml.MapSlice - var path = "" + var ( + parsedData yaml.MapSlice + path = "" + multidoc bool + docNumber int + matches []string + err error + childDocument string + ) if len(args) < 1 { return nil, errors.New("Must provide filename") } else if len(args) > 1 { path = args[1] + multidoc = regexpSubdocPath.MatchString(path) } + if multidoc { + matches = regexpSubdocPath.FindStringSubmatch(path) + // matches[0] is the original path + // matches[1] is the document number + // matches[2] is the path within that document + if len(matches) == 3 { + docNumber, err = strconv.Atoi(matches[1]) + if err == nil { + path = matches[2] + } + } + childDocument, err = readSubDocument(args[0], docNumber) + if err != nil { + return nil, err + } + tmp, err := ioutil.TempFile("", "") + defer os.Remove(tmp.Name()) + if err != nil { + return nil, err + } + n, err := tmp.WriteString(childDocument) + if err != nil { + return nil, err + } + if n != len(childDocument) { + return nil, fmt.Errorf("multidoc tmp file expected to write %v bytes but wrote %v bytes", len(childDocument), n) + } + + args[0] = tmp.Name() + + } + + // If we're asking for any document beyond the first, + // we need to provide the yaml package with that document + if err := readData(args[0], &parsedData); err != nil { var generalData interface{} if err = readData(args[0], &generalData); err != nil { @@ -460,3 +506,28 @@ func readData(filename string, parsedData interface{}) error { return yaml.Unmarshal(rawData, parsedData) } + +var regexpDocumentSeparator = regexp.MustCompile(`(?m)^---$`) + +// readSubDocument takes a filename and an index. +// It looks for ^---$ markers that separate yaml documents and splits the file on those. +// It takes the zero-indexed nth document, writes it to a temporary file, and return a handle to that file. +// Is trailing whitespace legal yaml? +func readSubDocument(filename string, n int) (string, error) { + allDocs, err := ioutil.ReadFile(filename) + if err != nil { + return "", err + } + + subDocs := regexpDocumentSeparator.Split(string(allDocs), -1) + if len(subDocs) == 0 { + return "", errors.New("document was split into zero subdocuments") + } + if subDocs[0] == "" { + subDocs = subDocs[1:] + } + if len(subDocs) <= n { + return "", fmt.Errorf("requested document %v out of total %v documents", n, len(subDocs)) + } + return strings.TrimSpace(subDocs[n]), nil +} diff --git a/yq_test.go b/yq_test.go index b00cc4b4..704eb999 100644 --- a/yq_test.go +++ b/yq_test.go @@ -38,6 +38,31 @@ func TestReadString(t *testing.T) { assertResult(t, "hi", result) } +func TestReadMultipleDocuments(t *testing.T) { + t.Run("multiple documents assumes first", func(t *testing.T) { + result, _ := read([]string{"examples/multidocument.yaml", "b.c"}) + assertResult(t, 1, result) + }) + t.Run("multiple documents with @n path reads document n", func(t *testing.T) { + result, _ := read([]string{"examples/multidocument.yaml", "@0"}) + actual, err := yamlToString(result) + assertNilErr(t, err) + assertResult(t, `a: Document One +b: + c: 1`, actual) + }) + t.Run("multiple documents with leading @n path reads path from document n", func(t *testing.T) { + result, _ := read([]string{"examples/multidocument.yaml", "@0.b.c"}) + assertResult(t, 1, result) + + result, _ = read([]string{"examples/multidocument.yaml", "@1.b.c"}) + assertResult(t, 2, result) + + _, err := read([]string{"examples/multidocument.yaml", "@2.b.c"}) + assertAnyErr(t, err) + }) +} + func TestOrder(t *testing.T) { result, _ := read([]string{"examples/order.yaml"}) formattedResult, _ := yamlToString(result) @@ -122,3 +147,30 @@ func TestNewYaml_WithUnknownScript(t *testing.T) { expectedOutput := `open fake-unknown: no such file or directory` assertResult(t, expectedOutput, err.Error()) } + +func TestSplitData(t *testing.T) { + t.Run("document index zero", func(t *testing.T) { + actual, err := readSubDocument("examples/multidocument.yaml", 0) + assertNilErr(t, err) + expected := `a: Document One +b: + c: 1` + assertResult(t, expected, actual) + // assert.Equal(t, expected, actual) // needed this to find whitespace problem + }) + + t.Run("document index one", func(t *testing.T) { + actual, err := readSubDocument("examples/multidocument.yaml", 1) + assertNilErr(t, err) + expected := `a: Document Two +b: + c: 2` + assertResult(t, expected, actual) + }) + + t.Run("document index beyond available", func(t *testing.T) { + actual, err := readSubDocument("examples/multidocument.yaml", 2) + assertAnyErr(t, err) + assertResult(t, "", actual) + }) +}