diff --git a/Dockerfile b/Dockerfile index abb609fa..ba152036 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24.4 AS builder +FROM golang:1.24.5 AS builder WORKDIR /go/src/mikefarah/yq diff --git a/Dockerfile.dev b/Dockerfile.dev index 881f15c4..d1dddc91 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM golang:1.24.4 +FROM golang:1.24.5 RUN apt-get update && \ apt-get install -y npm && \ diff --git a/cmd/constant.go b/cmd/constant.go index 762897d0..bf15c4fb 100644 --- a/cmd/constant.go +++ b/cmd/constant.go @@ -2,6 +2,8 @@ package cmd var unwrapScalarFlag = newUnwrapFlag() +var printNodeInfo = false + var unwrapScalar = false var writeInplace = false diff --git a/cmd/evaluate_sequence_command.go b/cmd/evaluate_sequence_command.go index e722415e..8bba7677 100644 --- a/cmd/evaluate_sequence_command.go +++ b/cmd/evaluate_sequence_command.go @@ -105,6 +105,11 @@ func evaluateSequence(cmd *cobra.Command, args []string) (cmdError error) { } printer := yqlib.NewPrinter(encoder, printerWriter) + + if printNodeInfo { + printer = yqlib.NewNodeInfoPrinter(printerWriter) + } + if nulSepOutput { printer.SetNulSepOutput(true) } diff --git a/cmd/root.go b/cmd/root.go index b7360d3c..268ff169 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -99,6 +99,7 @@ yq -P -oy sample.json } rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode") + rootCmd.PersistentFlags().BoolVarP(&printNodeInfo, "debug-node-info", "", false, "debug node info") rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "(deprecated) output as json. Set indent to 0 to print json in one line.") err := rootCmd.PersistentFlags().MarkDeprecated("tojson", "please use -o=json instead") @@ -196,6 +197,7 @@ yq -P -oy sample.json panic(err) } rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.LeadingContentPreProcessing, "header-preprocess", "", true, "Slurp any header comments and separators before processing expression.") + rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.FixMergeAnchorToSpec, "yaml-fix-merge-anchor-to-spec", "", false, "Fix merge anchor to match YAML spec. Will default to true in late 2025") rootCmd.PersistentFlags().StringVarP(&splitFileExp, "split-exp", "s", "", "print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. The necessary directories will be created.") if err = rootCmd.RegisterFlagCompletionFunc("split-exp", cobra.NoFileCompletions); err != nil { diff --git a/cmd/utils.go b/cmd/utils.go index 534615d5..0b37b3ee 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -18,52 +18,100 @@ func isAutomaticOutputFormat() bool { func initCommand(cmd *cobra.Command, args []string) (string, []string, error) { cmd.SilenceUsage = true - fileInfo, _ := os.Stdout.Stat() - - if forceColor || (!forceNoColor && (fileInfo.Mode()&os.ModeCharDevice) != 0) { - colorsEnabled = true - } + setupColors() expression, args, err := processArgs(args) if err != nil { return "", nil, err } + if err := loadSplitFileExpression(); err != nil { + return "", nil, err + } + + handleBackwardsCompatibility() + + if err := validateCommandFlags(args); err != nil { + return "", nil, err + } + + if err := configureFormats(args); err != nil { + return "", nil, err + } + + configureUnwrapScalar() + + return expression, args, nil +} + +func setupColors() { + fileInfo, _ := os.Stdout.Stat() + + if forceColor || (!forceNoColor && (fileInfo.Mode()&os.ModeCharDevice) != 0) { + colorsEnabled = true + } +} + +func loadSplitFileExpression() error { if splitFileExpFile != "" { splitExpressionBytes, err := os.ReadFile(splitFileExpFile) if err != nil { - return "", nil, err + return err } splitFileExp = string(splitExpressionBytes) } + return nil +} +func handleBackwardsCompatibility() { // backwards compatibility if outputToJSON { outputFormat = "json" } +} +func validateCommandFlags(args []string) error { if writeInplace && (len(args) == 0 || args[0] == "-") { - return "", nil, fmt.Errorf("write in place flag only applicable when giving an expression and at least one file") + return fmt.Errorf("write in place flag only applicable when giving an expression and at least one file") } if frontMatter != "" && len(args) == 0 { - return "", nil, fmt.Errorf("front matter flag only applicable when giving an expression and at least one file") + return fmt.Errorf("front matter flag only applicable when giving an expression and at least one file") } if writeInplace && splitFileExp != "" { - return "", nil, fmt.Errorf("write in place cannot be used with split file") + return fmt.Errorf("write in place cannot be used with split file") } if nullInput && len(args) > 0 { - return "", nil, fmt.Errorf("cannot pass files in when using null-input flag") + return fmt.Errorf("cannot pass files in when using null-input flag") } + return nil +} + +func configureFormats(args []string) error { inputFilename := "" if len(args) > 0 { inputFilename = args[0] } - if inputFormat == "" || inputFormat == "auto" || inputFormat == "a" { + if err := configureInputFormat(inputFilename); err != nil { + return err + } + + if err := configureOutputFormat(); err != nil { + return err + } + + yqlib.GetLogger().Debug("Using input format %v", inputFormat) + yqlib.GetLogger().Debug("Using output format %v", outputFormat) + + return nil +} + +func configureInputFormat(inputFilename string) error { + if inputFormat == "" || inputFormat == "auto" || inputFormat == "a" { inputFormat = yqlib.FormatStringFromFilename(inputFilename) _, err := yqlib.FormatFromString(inputFormat) @@ -88,24 +136,27 @@ func initCommand(cmd *cobra.Command, args []string) (string, []string, error) { } outputFormat = "yaml" } + return nil +} +func configureOutputFormat() error { outputFormatType, err := yqlib.FormatFromString(outputFormat) - if err != nil { - return "", nil, err + return err } - yqlib.GetLogger().Debug("Using input format %v", inputFormat) - yqlib.GetLogger().Debug("Using output format %v", outputFormat) if outputFormatType == yqlib.YamlFormat || outputFormatType == yqlib.PropertiesFormat { unwrapScalar = true } + + return nil +} + +func configureUnwrapScalar() { if unwrapScalarFlag.IsExplicitlySet() { unwrapScalar = unwrapScalarFlag.IsSet() } - - return expression, args, nil } func configureDecoder(evaluateTogether bool) (yqlib.Decoder, error) { diff --git a/cmd/utils_test.go b/cmd/utils_test.go new file mode 100644 index 00000000..67d45110 --- /dev/null +++ b/cmd/utils_test.go @@ -0,0 +1,1435 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/mikefarah/yq/v4/pkg/yqlib" + "github.com/spf13/cobra" +) + +func TestIsAutomaticOutputFormat(t *testing.T) { + tests := []struct { + name string + format string + expected bool + }{ + {"empty format", "", true}, + {"auto format", "auto", true}, + {"short auto format", "a", true}, + {"json format", "json", false}, + {"yaml format", "yaml", false}, + {"xml format", "xml", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original value + originalFormat := outputFormat + defer func() { outputFormat = originalFormat }() + + outputFormat = tt.format + result := isAutomaticOutputFormat() + if result != tt.expected { + t.Errorf("isAutomaticOutputFormat() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestMaybeFile(t *testing.T) { + // Create a temporary file for testing + tempFile, err := os.CreateTemp("", "test") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tempFile.Name()) + tempFile.Close() + + // Create a temporary directory for testing + tempDir, err := os.MkdirTemp("", "test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + tests := []struct { + name string + path string + expected bool + }{ + {"existing file", tempFile.Name(), true}, + {"existing directory", tempDir, false}, + {"non-existent path", "/path/that/does/not/exist", false}, + {"empty string", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := maybeFile(tt.path) + if result != tt.expected { + t.Errorf("maybeFile(%q) = %v, want %v", tt.path, result, tt.expected) + } + }) + } +} + +func TestProcessArgs(t *testing.T) { + // Create a temporary file for testing + tempFile, err := os.CreateTemp("", "test") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tempFile.Name()) + tempFile.Close() + + // Create a temporary .yq file for testing + tempYqFile, err := os.Create("test.yq") + if err != nil { + t.Fatalf("Failed to create temp yq file: %v", err) + } + defer os.Remove(tempYqFile.Name()) + if _, err = tempYqFile.WriteString(".a.b"); err != nil { + t.Fatalf("Failed to write to temp yq file: %v", err) + } + tempYqFile.Close() + + tests := []struct { + name string + args []string + forceExpression string + expressionFile string + expectedExpr string + expectedArgs []string + expectError bool + }{ + { + name: "empty args", + args: []string{}, + forceExpression: "", + expressionFile: "", + expectedExpr: "", + expectedArgs: []string{}, + expectError: false, + }, + { + name: "force expression", + args: []string{"file1"}, + forceExpression: ".a.b", + expressionFile: "", + expectedExpr: ".a.b", + expectedArgs: []string{"file1"}, + expectError: false, + }, + { + name: "expression as first arg", + args: []string{".a.b", "file1"}, + forceExpression: "", + expressionFile: "", + expectedExpr: ".a.b", + expectedArgs: []string{"file1"}, + expectError: false, + }, + { + name: "file as first arg", + args: []string{tempFile.Name()}, + forceExpression: "", + expressionFile: "", + expectedExpr: "", + expectedArgs: []string{tempFile.Name()}, + expectError: false, + }, + { + name: "yq file as first arg", + args: []string{tempYqFile.Name(), "things"}, + forceExpression: "", + expressionFile: "", + expectedExpr: ".a.b", + expectedArgs: []string{"things"}, + expectError: false, + }, + { + name: "dash as first arg", + args: []string{"-"}, + forceExpression: "", + expressionFile: "", + expectedExpr: "", + expectedArgs: []string{"-"}, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original values + originalForceExpression := forceExpression + originalExpressionFile := expressionFile + defer func() { + forceExpression = originalForceExpression + expressionFile = originalExpressionFile + }() + + forceExpression = tt.forceExpression + expressionFile = tt.expressionFile + + expr, args, err := processArgs(tt.args) + if tt.expectError { + if err == nil { + t.Errorf("processArgs() expected error but got none") + } + return + } + + if err != nil { + t.Errorf("processArgs() unexpected error: %v", err) + return + } + + if expr != tt.expectedExpr { + t.Errorf("processArgs() expression = %v, want %v", expr, tt.expectedExpr) + } + + if !stringsEqual(args, tt.expectedArgs) { + t.Errorf("processArgs() args = %v, want %v", args, tt.expectedArgs) + } + }) + } +} + +func TestConfigureDecoder(t *testing.T) { + tests := []struct { + name string + inputFormat string + evaluateTogether bool + expectError bool + expectType string + }{ + { + name: "yaml format", + inputFormat: "yaml", + evaluateTogether: false, + expectError: false, + expectType: "yamlDecoder", + }, + { + name: "json format", + inputFormat: "json", + evaluateTogether: true, + expectError: false, + expectType: "jsonDecoder", + }, + { + name: "xml format", + inputFormat: "xml", + evaluateTogether: false, + expectError: false, + expectType: "xmlDecoder", + }, + { + name: "invalid format", + inputFormat: "invalid", + evaluateTogether: false, + expectError: true, + expectType: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original value + originalInputFormat := inputFormat + defer func() { inputFormat = originalInputFormat }() + + inputFormat = tt.inputFormat + + decoder, err := configureDecoder(tt.evaluateTogether) + if tt.expectError { + if err == nil { + t.Errorf("configureDecoder() expected error but got none") + } + if decoder != nil { + t.Errorf("configureDecoder() expected nil decoder but got %v", decoder) + } + return + } + + if err != nil { + t.Errorf("configureDecoder() unexpected error: %v", err) + return + } + + if decoder == nil { + t.Errorf("configureDecoder() expected decoder but got nil") + return + } + + typeStr := fmt.Sprintf("%T", decoder) + if !strings.Contains(typeStr, tt.expectType) { + t.Errorf("configureDecoder() expected type to contain %q but got %q", tt.expectType, typeStr) + } + }) + } +} + +func TestConfigurePrinterWriter(t *testing.T) { + yqlib.InitExpressionParser() + + tests := []struct { + name string + splitFileExp string + format *yqlib.Format + forceColor bool + expectError bool + expectMulti bool + expectColorsEnabled bool + }{ + { + name: "single printer writer", + splitFileExp: "", + format: &yqlib.Format{}, + forceColor: false, + expectError: false, + expectMulti: false, + expectColorsEnabled: false, + }, + { + name: "multi printer writer with valid expression", + splitFileExp: ".a.b", + format: &yqlib.Format{}, + forceColor: true, + expectError: false, + expectMulti: true, + expectColorsEnabled: true, + }, + { + name: "multi printer writer with invalid expression", + splitFileExp: "[invalid", + format: &yqlib.Format{}, + forceColor: false, + expectError: true, + expectMulti: false, + expectColorsEnabled: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original values + originalSplitFileExp := splitFileExp + originalForceColor := forceColor + originalColorsEnabled := colorsEnabled + defer func() { + splitFileExp = originalSplitFileExp + forceColor = originalForceColor + colorsEnabled = originalColorsEnabled + }() + + splitFileExp = tt.splitFileExp + forceColor = tt.forceColor + colorsEnabled = false // Reset to test the setting + + writer, err := configurePrinterWriter(tt.format, os.Stdout) + if tt.expectError { + if err == nil { + t.Errorf("configurePrinterWriter() expected error but got none") + } + if writer != nil { + t.Errorf("configurePrinterWriter() expected nil writer but got %v", writer) + } + return + } + + if err != nil { + t.Errorf("configurePrinterWriter() unexpected error: %v", err) + return + } + + if writer == nil { + t.Errorf("configurePrinterWriter() expected writer but got nil") + return + } + + // Explicitly check colorsEnabled + if colorsEnabled != tt.expectColorsEnabled { + t.Errorf("configurePrinterWriter() colorsEnabled = %v, want %v", colorsEnabled, tt.expectColorsEnabled) + } + + // Check the type of the returned writer + writerType := fmt.Sprintf("%T", writer) + if tt.expectMulti { + if !strings.Contains(writerType, "multiPrintWriter") { + t.Errorf("configurePrinterWriter() expected multiPrintWriter but got %s", writerType) + } + } else { + if !strings.Contains(writerType, "singlePrinterWriter") { + t.Errorf("configurePrinterWriter() expected singlePrinterWriter but got %s", writerType) + } + } + }) + } +} + +func TestConfigureEncoder(t *testing.T) { + tests := []struct { + name string + outputFormat string + expectError bool + expectType string + }{ + { + name: "yaml format", + outputFormat: "yaml", + expectError: false, + expectType: "yamlEncoder", + }, + { + name: "json format", + outputFormat: "json", + expectError: false, + expectType: "jsonEncoder", + }, + { + name: "xml format", + outputFormat: "xml", + expectError: false, + expectType: "xmlEncoder", + }, + { + name: "properties format", + outputFormat: "properties", + expectError: false, + expectType: "propertiesEncoder", + }, + { + name: "invalid format", + outputFormat: "invalid", + expectError: true, + expectType: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original values + originalOutputFormat := outputFormat + originalIndent := indent + originalUnwrapScalar := unwrapScalar + originalColorsEnabled := colorsEnabled + originalNoDocSeparators := noDocSeparators + defer func() { + outputFormat = originalOutputFormat + indent = originalIndent + unwrapScalar = originalUnwrapScalar + colorsEnabled = originalColorsEnabled + noDocSeparators = originalNoDocSeparators + }() + + outputFormat = tt.outputFormat + indent = 2 + unwrapScalar = false + colorsEnabled = false + noDocSeparators = false + + encoder, err := configureEncoder() + if tt.expectError { + if err == nil { + t.Errorf("configureEncoder() expected error but got none") + } + if encoder != nil { + t.Errorf("configureEncoder() expected nil encoder but got %v", encoder) + } + return + } + + if err != nil { + t.Errorf("configureEncoder() unexpected error: %v", err) + return + } + + if encoder == nil { + t.Errorf("configureEncoder() expected encoder but got nil") + return + } + + typeStr := fmt.Sprintf("%T", encoder) + if !strings.Contains(typeStr, tt.expectType) { + t.Errorf("configureEncoder() expected type to contain %q but got %q", tt.expectType, typeStr) + } + }) + } +} + +func TestInitCommand(t *testing.T) { + // Create a temporary file for testing + tempFile, err := os.CreateTemp("", "test") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tempFile.Name()) + tempFile.Close() + + // Create a temporary split file + tempSplitFile, err := os.CreateTemp("", "split") + if err != nil { + t.Fatalf("Failed to create temp split file: %v", err) + } + defer os.Remove(tempSplitFile.Name()) + if _, err = tempSplitFile.WriteString(".a.b"); err != nil { + t.Fatalf("Failed to write to temp split file: %v", err) + } + tempSplitFile.Close() + + tests := []struct { + name string + args []string + writeInplace bool + frontMatter string + nullInput bool + splitFileExpFile string + splitFileExp string + outputToJSON bool + expectError bool + errorContains string + expectExpr string + expectArgs []string + }{ + { + name: "basic command", + args: []string{tempFile.Name()}, + writeInplace: false, + frontMatter: "", + nullInput: false, + expectError: false, + expectExpr: "", + expectArgs: []string{tempFile.Name()}, + }, + { + name: "write inplace with no args", + args: []string{}, + writeInplace: true, + frontMatter: "", + nullInput: false, + expectError: true, + errorContains: "write in place flag only applicable when giving an expression and at least one file", + }, + { + name: "split file expression from file", + args: []string{tempFile.Name()}, + writeInplace: false, + frontMatter: "", + nullInput: false, + splitFileExpFile: tempSplitFile.Name(), + expectError: false, + expectExpr: "", + expectArgs: []string{tempFile.Name()}, + }, + { + name: "output to JSON", + args: []string{tempFile.Name()}, + writeInplace: false, + frontMatter: "", + nullInput: false, + outputToJSON: true, + expectError: false, + expectExpr: "", + expectArgs: []string{tempFile.Name()}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original values + originalWriteInplace := writeInplace + originalFrontMatter := frontMatter + originalNullInput := nullInput + originalSplitFileExpFile := splitFileExpFile + originalSplitFileExp := splitFileExp + originalOutputToJSON := outputToJSON + originalInputFormat := inputFormat + originalOutputFormat := outputFormat + originalForceColor := forceColor + originalForceNoColor := forceNoColor + originalColorsEnabled := colorsEnabled + defer func() { + writeInplace = originalWriteInplace + frontMatter = originalFrontMatter + nullInput = originalNullInput + splitFileExpFile = originalSplitFileExpFile + splitFileExp = originalSplitFileExp + outputToJSON = originalOutputToJSON + inputFormat = originalInputFormat + outputFormat = originalOutputFormat + forceColor = originalForceColor + forceNoColor = originalForceNoColor + colorsEnabled = originalColorsEnabled + }() + + writeInplace = tt.writeInplace + frontMatter = tt.frontMatter + nullInput = tt.nullInput + splitFileExpFile = tt.splitFileExpFile + splitFileExp = tt.splitFileExp + outputToJSON = tt.outputToJSON + inputFormat = "auto" + outputFormat = "auto" + forceColor = false + forceNoColor = false + colorsEnabled = false + + cmd := &cobra.Command{} + expr, args, err := initCommand(cmd, tt.args) + if tt.expectError { + if err == nil { + t.Errorf("initCommand() expected error but got none") + return + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("initCommand() error '%v' does not contain '%v'", err.Error(), tt.errorContains) + } + return + } + + if err != nil { + t.Errorf("initCommand() unexpected error: %v", err) + return + } + + if expr != tt.expectExpr { + t.Errorf("initCommand() expr = %v, want %v", expr, tt.expectExpr) + } + if !stringsEqual(args, tt.expectArgs) { + t.Errorf("initCommand() args = %v, want %v", args, tt.expectArgs) + } + }) + } +} + +func TestProcessArgsWithExpressionFile(t *testing.T) { + // Create a temporary .yq file with Windows line endings + tempYqFile, err := os.CreateTemp("", "test.yq") + if err != nil { + t.Fatalf("Failed to create temp yq file: %v", err) + } + defer os.Remove(tempYqFile.Name()) + if _, err = tempYqFile.WriteString(".a.b\r\n.c.d"); err != nil { + t.Fatalf("Failed to write to temp yq file: %v", err) + } + tempYqFile.Close() + + // Save original values + originalExpressionFile := expressionFile + defer func() { expressionFile = originalExpressionFile }() + + expressionFile = tempYqFile.Name() + + expr, args, err := processArgs([]string{"file1"}) + if err != nil { + t.Errorf("processArgs() unexpected error: %v", err) + return + } + + expectedExpr := ".a.b\n.c.d" // Should convert \r\n to \n + if expr != expectedExpr { + t.Errorf("processArgs() expression = %v, want %v", expr, expectedExpr) + } + + expectedArgs := []string{"file1"} + if !stringsEqual(args, expectedArgs) { + t.Errorf("processArgs() args = %v, want %v", args, expectedArgs) + } +} + +func TestProcessArgsWithNonExistentExpressionFile(t *testing.T) { + // Save original values + originalExpressionFile := expressionFile + defer func() { expressionFile = originalExpressionFile }() + + expressionFile = "/path/that/does/not/exist" + + expr, args, err := processArgs([]string{"file1"}) + if err == nil { + t.Errorf("processArgs() expected error but got none") + } + if expr != "" { + t.Errorf("processArgs() expected empty expression but got %v", expr) + } + if args != nil { + t.Errorf("processArgs() expected nil args but got %v", args) + } +} + +func TestInitCommandWithInvalidOutputFormat(t *testing.T) { + // Create a temporary file for testing + tempFile, err := os.CreateTemp("", "test") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tempFile.Name()) + tempFile.Close() + + // Save original values + originalInputFormat := inputFormat + originalOutputFormat := outputFormat + defer func() { + inputFormat = originalInputFormat + outputFormat = originalOutputFormat + }() + + inputFormat = "auto" + outputFormat = "invalid" + + cmd := &cobra.Command{} + expr, args, err := initCommand(cmd, []string{tempFile.Name()}) + if err == nil { + t.Errorf("initCommand() expected error but got none") + } + if expr != "" { + t.Errorf("initCommand() expected empty expression but got %v", expr) + } + if args != nil { + t.Errorf("initCommand() expected nil args but got %v", args) + } +} + +func TestInitCommandWithUnknownInputFormat(t *testing.T) { + // Create a temporary file with unknown extension + tempFile, err := os.CreateTemp("", "test.unknown") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tempFile.Name()) + tempFile.Close() + + // Save original values + originalInputFormat := inputFormat + originalOutputFormat := outputFormat + defer func() { + inputFormat = originalInputFormat + outputFormat = originalOutputFormat + }() + + inputFormat = "auto" + outputFormat = "auto" + + cmd := &cobra.Command{} + expr, args, err := initCommand(cmd, []string{tempFile.Name()}) + if err != nil { + t.Errorf("initCommand() unexpected error: %v", err) + return + } + + // expr can be empty when no expression is provided + _ = expr + if args == nil { + t.Errorf("initCommand() expected non-nil args") + } +} + +func TestConfigurePrinterWriterWithInvalidSplitExpression(t *testing.T) { + // Save original value + originalSplitFileExp := splitFileExp + defer func() { splitFileExp = originalSplitFileExp }() + + splitFileExp = "[invalid expression" + + writer, err := configurePrinterWriter(&yqlib.Format{}, os.Stdout) + if err == nil { + t.Errorf("configurePrinterWriter() expected error but got none") + } + if writer != nil { + t.Errorf("configurePrinterWriter() expected nil writer but got %v", writer) + } + if err != nil && !strings.Contains(err.Error(), "bad split document expression") { + t.Errorf("configurePrinterWriter() error '%v' does not contain expected message", err.Error()) + } +} + +func TestMaybeFileWithDirectory(t *testing.T) { + // Create a temporary directory + tempDir, err := os.MkdirTemp("", "test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + result := maybeFile(tempDir) + if result { + t.Errorf("maybeFile(%q) = %v, want false", tempDir, result) + } +} + +func TestProcessStdInArgsWithDash(t *testing.T) { + args := []string{"-", "file1"} + result := processStdInArgs(args) + if !stringsEqual(result, args) { + t.Errorf("processStdInArgs() = %v, want %v", result, args) + } +} + +func TestProcessArgsWithYqFileExtension(t *testing.T) { + tempYqFile, err := os.Create("test.yq") + if err != nil { + t.Fatalf("Failed to create temp yq file: %v", err) + } + defer os.Remove(tempYqFile.Name()) + if _, err = tempYqFile.WriteString(".a.b"); err != nil { + t.Fatalf("Failed to write to temp yq file: %v", err) + } + tempYqFile.Close() + + // Save original values + originalExpressionFile := expressionFile + originalForceExpression := forceExpression + defer func() { + expressionFile = originalExpressionFile + forceExpression = originalForceExpression + }() + + // Reset expressionFile to empty to test the auto-detection + expressionFile = "" + forceExpression = "" + + // Debug: check the conditions manually + t.Logf("expressionFile: %q", expressionFile) + t.Logf("forceExpression: %q", forceExpression) + t.Logf("tempYqFile.Name(): %q", tempYqFile.Name()) + t.Logf("strings.HasSuffix(tempYqFile.Name(), '.yq'): %v", strings.HasSuffix(tempYqFile.Name(), ".yq")) + t.Logf("maybeFile(tempYqFile.Name()): %v", maybeFile(tempYqFile.Name())) + + // Test with only the yq file as argument (should be treated as expression file) + expr, args, err := processArgs([]string{tempYqFile.Name()}) + if err != nil { + t.Errorf("processArgs() unexpected error: %v", err) + return + } + + if expr != ".a.b" { + t.Errorf("processArgs() expression = %v, want .a.b", expr) + } + + expectedArgs := []string{} + if !stringsEqual(args, expectedArgs) { + t.Errorf("processArgs() args = %v, want %v", args, expectedArgs) + } +} + +func TestConfigureEncoderWithYamlFormat(t *testing.T) { + // Save original values + originalOutputFormat := outputFormat + originalIndent := indent + originalUnwrapScalar := unwrapScalar + originalColorsEnabled := colorsEnabled + originalNoDocSeparators := noDocSeparators + defer func() { + outputFormat = originalOutputFormat + indent = originalIndent + unwrapScalar = originalUnwrapScalar + colorsEnabled = originalColorsEnabled + noDocSeparators = originalNoDocSeparators + }() + + outputFormat = "yaml" + indent = 4 + unwrapScalar = true + colorsEnabled = true + noDocSeparators = true + + encoder, err := configureEncoder() + if err != nil { + t.Errorf("configureEncoder() unexpected error: %v", err) + return + } + + if encoder == nil { + t.Errorf("configureEncoder() expected encoder but got nil") + } +} + +func TestConfigureEncoderWithPropertiesFormat(t *testing.T) { + // Save original values + originalOutputFormat := outputFormat + originalIndent := indent + originalUnwrapScalar := unwrapScalar + originalColorsEnabled := colorsEnabled + originalNoDocSeparators := noDocSeparators + defer func() { + outputFormat = originalOutputFormat + indent = originalIndent + unwrapScalar = originalUnwrapScalar + colorsEnabled = originalColorsEnabled + noDocSeparators = originalNoDocSeparators + }() + + outputFormat = "properties" + indent = 2 + unwrapScalar = false + colorsEnabled = false + noDocSeparators = false + + encoder, err := configureEncoder() + if err != nil { + t.Errorf("configureEncoder() unexpected error: %v", err) + return + } + + if encoder == nil { + t.Errorf("configureEncoder() expected encoder but got nil") + } +} + +// Mock boolFlag for testing +type mockBoolFlag struct { + explicitlySet bool + value bool +} + +func (f *mockBoolFlag) IsExplicitlySet() bool { + return f.explicitlySet +} + +func (f *mockBoolFlag) IsSet() bool { + return f.value +} + +func (f *mockBoolFlag) String() string { + return "mock" +} + +func (f *mockBoolFlag) Set(_ string) error { + return nil +} + +func (f *mockBoolFlag) Type() string { + return "bool" +} + +// Helper function to compare string slices +func stringsEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestSetupColors(t *testing.T) { + tests := []struct { + name string + forceColor bool + forceNoColor bool + expectColors bool + }{ + { + name: "force color enabled", + forceColor: true, + forceNoColor: false, + expectColors: true, + }, + { + name: "force no color enabled", + forceColor: false, + forceNoColor: true, + expectColors: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original values + originalForceColor := forceColor + originalForceNoColor := forceNoColor + originalColorsEnabled := colorsEnabled + defer func() { + forceColor = originalForceColor + forceNoColor = originalForceNoColor + colorsEnabled = originalColorsEnabled + }() + + forceColor = tt.forceColor + forceNoColor = tt.forceNoColor + colorsEnabled = false // Reset to test the setting + + setupColors() + + if colorsEnabled != tt.expectColors { + t.Errorf("setupColors() colorsEnabled = %v, want %v", colorsEnabled, tt.expectColors) + } + }) + } +} + +func TestLoadSplitFileExpression(t *testing.T) { + // Create a temporary file with expression content + tempFile, err := os.CreateTemp("", "split") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tempFile.Name()) + if _, err = tempFile.WriteString(".a.b"); err != nil { + t.Fatalf("Failed to write to temp file: %v", err) + } + tempFile.Close() + + tests := []struct { + name string + splitFileExpFile string + expectError bool + expectContent string + }{ + { + name: "load from file", + splitFileExpFile: tempFile.Name(), + expectError: false, + expectContent: ".a.b", + }, + { + name: "no file specified", + splitFileExpFile: "", + expectError: false, + expectContent: "", + }, + { + name: "non-existent file", + splitFileExpFile: "/path/that/does/not/exist", + expectError: true, + expectContent: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original value + originalSplitFileExpFile := splitFileExpFile + originalSplitFileExp := splitFileExp + defer func() { + splitFileExpFile = originalSplitFileExpFile + splitFileExp = originalSplitFileExp + }() + + splitFileExpFile = tt.splitFileExpFile + splitFileExp = "" + + err := loadSplitFileExpression() + if tt.expectError { + if err == nil { + t.Errorf("loadSplitFileExpression() expected error but got none") + } + return + } + + if err != nil { + t.Errorf("loadSplitFileExpression() unexpected error: %v", err) + return + } + + if splitFileExp != tt.expectContent { + t.Errorf("loadSplitFileExpression() splitFileExp = %v, want %v", splitFileExp, tt.expectContent) + } + }) + } +} + +func TestHandleBackwardsCompatibility(t *testing.T) { + tests := []struct { + name string + outputToJSON bool + initialFormat string + expectFormat string + }{ + { + name: "outputToJSON true", + outputToJSON: true, + initialFormat: "yaml", + expectFormat: "json", + }, + { + name: "outputToJSON false", + outputToJSON: false, + initialFormat: "yaml", + expectFormat: "yaml", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original value + originalOutputToJSON := outputToJSON + originalOutputFormat := outputFormat + defer func() { + outputToJSON = originalOutputToJSON + outputFormat = originalOutputFormat + }() + + outputToJSON = tt.outputToJSON + outputFormat = tt.initialFormat + + handleBackwardsCompatibility() + + if outputFormat != tt.expectFormat { + t.Errorf("handleBackwardsCompatibility() outputFormat = %v, want %v", outputFormat, tt.expectFormat) + } + }) + } +} + +func TestValidateCommandFlags(t *testing.T) { + tests := []struct { + name string + args []string + writeInplace bool + frontMatter string + splitFileExp string + nullInput bool + expectError bool + errorContains string + }{ + { + name: "valid flags", + args: []string{"file.yaml"}, + writeInplace: false, + frontMatter: "", + splitFileExp: "", + nullInput: false, + expectError: false, + }, + { + name: "write inplace with no args", + args: []string{}, + writeInplace: true, + frontMatter: "", + splitFileExp: "", + nullInput: false, + expectError: true, + errorContains: "write in place flag only applicable when giving an expression and at least one file", + }, + { + name: "write inplace with dash", + args: []string{"-"}, + writeInplace: true, + frontMatter: "", + splitFileExp: "", + nullInput: false, + expectError: true, + errorContains: "write in place flag only applicable when giving an expression and at least one file", + }, + { + name: "front matter with no args", + args: []string{}, + writeInplace: false, + frontMatter: "extract", + splitFileExp: "", + nullInput: false, + expectError: true, + errorContains: "front matter flag only applicable when giving an expression and at least one file", + }, + { + name: "write inplace with split file", + args: []string{"file.yaml"}, + writeInplace: true, + frontMatter: "", + splitFileExp: ".a.b", + nullInput: false, + expectError: true, + errorContains: "write in place cannot be used with split file", + }, + { + name: "null input with args", + args: []string{"file.yaml"}, + writeInplace: false, + frontMatter: "", + splitFileExp: "", + nullInput: true, + expectError: true, + errorContains: "cannot pass files in when using null-input flag", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original values + originalWriteInplace := writeInplace + originalFrontMatter := frontMatter + originalSplitFileExp := splitFileExp + originalNullInput := nullInput + defer func() { + writeInplace = originalWriteInplace + frontMatter = originalFrontMatter + splitFileExp = originalSplitFileExp + nullInput = originalNullInput + }() + + writeInplace = tt.writeInplace + frontMatter = tt.frontMatter + splitFileExp = tt.splitFileExp + nullInput = tt.nullInput + + err := validateCommandFlags(tt.args) + if tt.expectError { + if err == nil { + t.Errorf("validateCommandFlags() expected error but got none") + return + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("validateCommandFlags() error '%v' does not contain '%v'", err.Error(), tt.errorContains) + } + return + } + + if err != nil { + t.Errorf("validateCommandFlags() unexpected error: %v", err) + } + }) + } +} + +func TestConfigureFormats(t *testing.T) { + tests := []struct { + name string + args []string + inputFormat string + outputFormat string + expectError bool + }{ + { + name: "valid formats", + args: []string{"file.yaml"}, + inputFormat: "auto", + outputFormat: "auto", + expectError: false, + }, + { + name: "invalid output format", + args: []string{"file.yaml"}, + inputFormat: "auto", + outputFormat: "invalid", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original values + originalInputFormat := inputFormat + originalOutputFormat := outputFormat + defer func() { + inputFormat = originalInputFormat + outputFormat = originalOutputFormat + }() + + inputFormat = tt.inputFormat + outputFormat = tt.outputFormat + + err := configureFormats(tt.args) + if tt.expectError { + if err == nil { + t.Errorf("configureFormats() expected error but got none") + } + return + } + + if err != nil { + t.Errorf("configureFormats() unexpected error: %v", err) + } + }) + } +} + +func TestConfigureInputFormat(t *testing.T) { + tests := []struct { + name string + inputFilename string + inputFormat string + outputFormat string + expectInput string + expectOutput string + }{ + { + name: "auto format with yaml file", + inputFilename: "file.yaml", + inputFormat: "auto", + outputFormat: "auto", + expectInput: "yaml", + expectOutput: "yaml", + }, + { + name: "auto format with json file", + inputFilename: "file.json", + inputFormat: "auto", + outputFormat: "auto", + expectInput: "json", + expectOutput: "json", + }, + { + name: "auto format with unknown file", + inputFilename: "file.unknown", + inputFormat: "auto", + outputFormat: "auto", + expectInput: "yaml", + expectOutput: "yaml", + }, + { + name: "explicit format", + inputFilename: "file.yaml", + inputFormat: "json", + outputFormat: "auto", + expectInput: "json", + expectOutput: "yaml", // backwards compatibility + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original values + originalInputFormat := inputFormat + originalOutputFormat := outputFormat + defer func() { + inputFormat = originalInputFormat + outputFormat = originalOutputFormat + }() + + inputFormat = tt.inputFormat + outputFormat = tt.outputFormat + + err := configureInputFormat(tt.inputFilename) + if err != nil { + t.Errorf("configureInputFormat() unexpected error: %v", err) + return + } + + if inputFormat != tt.expectInput { + t.Errorf("configureInputFormat() inputFormat = %v, want %v", inputFormat, tt.expectInput) + } + if outputFormat != tt.expectOutput { + t.Errorf("configureInputFormat() outputFormat = %v, want %v", outputFormat, tt.expectOutput) + } + }) + } +} + +func TestConfigureOutputFormat(t *testing.T) { + tests := []struct { + name string + outputFormat string + expectError bool + expectUnwrap bool + }{ + { + name: "yaml format", + outputFormat: "yaml", + expectError: false, + expectUnwrap: true, + }, + { + name: "properties format", + outputFormat: "properties", + expectError: false, + expectUnwrap: true, + }, + { + name: "json format", + outputFormat: "json", + expectError: false, + expectUnwrap: false, + }, + { + name: "invalid format", + outputFormat: "invalid", + expectError: true, + expectUnwrap: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original values + originalOutputFormat := outputFormat + originalUnwrapScalar := unwrapScalar + defer func() { + outputFormat = originalOutputFormat + unwrapScalar = originalUnwrapScalar + }() + + outputFormat = tt.outputFormat + unwrapScalar = false // Reset to test the setting + + err := configureOutputFormat() + if tt.expectError { + if err == nil { + t.Errorf("configureOutputFormat() expected error but got none") + } + return + } + + if err != nil { + t.Errorf("configureOutputFormat() unexpected error: %v", err) + return + } + + if unwrapScalar != tt.expectUnwrap { + t.Errorf("configureOutputFormat() unwrapScalar = %v, want %v", unwrapScalar, tt.expectUnwrap) + } + }) + } +} + +func TestConfigureUnwrapScalar(t *testing.T) { + tests := []struct { + name string + explicitlySet bool + flagValue bool + initialUnwrap bool + expectUnwrap bool + }{ + { + name: "flag not explicitly set", + explicitlySet: false, + flagValue: true, + initialUnwrap: true, + expectUnwrap: true, // Should remain unchanged + }, + { + name: "flag explicitly set to true", + explicitlySet: true, + flagValue: true, + initialUnwrap: false, + expectUnwrap: true, + }, + { + name: "flag explicitly set to false", + explicitlySet: true, + flagValue: false, + initialUnwrap: true, + expectUnwrap: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original value + originalUnwrapScalar := unwrapScalar + originalUnwrapScalarFlag := unwrapScalarFlag + defer func() { + unwrapScalar = originalUnwrapScalar + unwrapScalarFlag = originalUnwrapScalarFlag + }() + + unwrapScalar = tt.initialUnwrap + unwrapScalarFlag = &mockBoolFlag{ + explicitlySet: tt.explicitlySet, + value: tt.flagValue, + } + + configureUnwrapScalar() + + if unwrapScalar != tt.expectUnwrap { + t.Errorf("configureUnwrapScalar() unwrapScalar = %v, want %v", unwrapScalar, tt.expectUnwrap) + } + }) + } +} diff --git a/cmd/version.go b/cmd/version.go index 99356680..b3b0e2da 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -11,7 +11,7 @@ var ( GitDescribe string // Version is main version number that is being run at the moment. - Version = "v4.45.4" + Version = "v4.46.1" // VersionPrerelease is a pre-release marker for the version. If this is "" (empty string) // then it means that it is a final release. Otherwise, this is a pre-release diff --git a/examples/data1.yaml b/examples/data1.yaml index 26c3161c..470fab3d 100644 --- a/examples/data1.yaml +++ b/examples/data1.yaml @@ -1,3 +1,8 @@ -Foo: 3 -apple: 1 -bar: 2 \ No newline at end of file +# 001 +--- +abc: # 001 +- 1 # one +- 2 # two + +--- +def # 002 \ No newline at end of file diff --git a/examples/front-matter.yaml b/examples/front-matter.yaml index 8ce5d4bc..8d513037 100644 --- a/examples/front-matter.yaml +++ b/examples/front-matter.yaml @@ -1,6 +1,6 @@ --- a: apple -b: bannana +b: banana --- hello there apples: great \ No newline at end of file diff --git a/go.mod b/go.mod index be12dcbd..27b4111f 100644 --- a/go.mod +++ b/go.mod @@ -17,10 +17,10 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 github.com/yuin/gopher-lua v1.1.1 + go.yaml.in/yaml/v3 v3.0.4 golang.org/x/net v0.41.0 - golang.org/x/text v0.26.0 + golang.org/x/text v0.27.0 gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 - gopkg.in/yaml.v3 v3.0.1 ) require ( diff --git a/go.sum b/go.sum index 0d2831e4..5ca9bad1 100644 --- a/go.sum +++ b/go.sum @@ -49,13 +49,15 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE= diff --git a/pkg/yqlib/candidate_node.go b/pkg/yqlib/candidate_node.go index 5156ac4b..51485f11 100644 --- a/pkg/yqlib/candidate_node.go +++ b/pkg/yqlib/candidate_node.go @@ -53,6 +53,20 @@ func createScalarNode(value interface{}, stringValue string) *CandidateNode { return node } +type NodeInfo struct { + Kind string `yaml:"kind"` + Style string `yaml:"style,omitempty"` + Anchor string `yaml:"anchor,omitempty"` + Tag string `yaml:"tag,omitempty"` + HeadComment string `yaml:"headComment,omitempty"` + LineComment string `yaml:"lineComment,omitempty"` + FootComment string `yaml:"footComment,omitempty"` + Value string `yaml:"value,omitempty"` + Line int `yaml:"line,omitempty"` + Column int `yaml:"column,omitempty"` + Content []*NodeInfo `yaml:"content,omitempty"` +} + type CandidateNode struct { Kind Kind Style Style @@ -451,3 +465,64 @@ func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignP n.LineComment = other.LineComment } } + +func (n *CandidateNode) ConvertToNodeInfo() *NodeInfo { + info := &NodeInfo{ + Kind: kindToString(n.Kind), + Style: styleToString(n.Style), + Anchor: n.Anchor, + Tag: n.Tag, + HeadComment: n.HeadComment, + LineComment: n.LineComment, + FootComment: n.FootComment, + Value: n.Value, + Line: n.Line, + Column: n.Column, + } + if len(n.Content) > 0 { + info.Content = make([]*NodeInfo, len(n.Content)) + for i, child := range n.Content { + info.Content[i] = child.ConvertToNodeInfo() + } + } + return info +} + +// Helper functions to convert Kind and Style to string for NodeInfo +func kindToString(k Kind) string { + switch k { + case SequenceNode: + return "SequenceNode" + case MappingNode: + return "MappingNode" + case ScalarNode: + return "ScalarNode" + case AliasNode: + return "AliasNode" + default: + return "Unknown" + } +} + +func styleToString(s Style) string { + var styles []string + if s&TaggedStyle != 0 { + styles = append(styles, "TaggedStyle") + } + if s&DoubleQuotedStyle != 0 { + styles = append(styles, "DoubleQuotedStyle") + } + if s&SingleQuotedStyle != 0 { + styles = append(styles, "SingleQuotedStyle") + } + if s&LiteralStyle != 0 { + styles = append(styles, "LiteralStyle") + } + if s&FoldedStyle != 0 { + styles = append(styles, "FoldedStyle") + } + if s&FlowStyle != 0 { + styles = append(styles, "FlowStyle") + } + return strings.Join(styles, ",") +} diff --git a/pkg/yqlib/candidate_node_test.go b/pkg/yqlib/candidate_node_test.go index 2a09b48a..ceaa4052 100644 --- a/pkg/yqlib/candidate_node_test.go +++ b/pkg/yqlib/candidate_node_test.go @@ -160,3 +160,47 @@ func TestCandidateNodeAddKeyValueChild(t *testing.T) { test.AssertResult(t, key.IsMapKey, true) } + +func TestConvertToNodeInfo(t *testing.T) { + child := &CandidateNode{ + Kind: ScalarNode, + Style: DoubleQuotedStyle, + Tag: "!!str", + Value: "childValue", + Line: 2, + Column: 3, + } + parent := &CandidateNode{ + Kind: MappingNode, + Style: TaggedStyle, + Tag: "!!map", + Value: "", + Line: 1, + Column: 1, + Content: []*CandidateNode{child}, + HeadComment: "head", + LineComment: "line", + FootComment: "foot", + Anchor: "anchor", + } + info := parent.ConvertToNodeInfo() + test.AssertResult(t, "MappingNode", info.Kind) + test.AssertResult(t, "TaggedStyle", info.Style) + test.AssertResult(t, "!!map", info.Tag) + test.AssertResult(t, "head", info.HeadComment) + test.AssertResult(t, "line", info.LineComment) + test.AssertResult(t, "foot", info.FootComment) + test.AssertResult(t, "anchor", info.Anchor) + test.AssertResult(t, 1, info.Line) + test.AssertResult(t, 1, info.Column) + if len(info.Content) != 1 { + t.Fatalf("Expected 1 child, got %d", len(info.Content)) + } + childInfo := info.Content[0] + test.AssertResult(t, "ScalarNode", childInfo.Kind) + test.AssertResult(t, "DoubleQuotedStyle", childInfo.Style) + test.AssertResult(t, "!!str", childInfo.Tag) + test.AssertResult(t, "childValue", childInfo.Value) + test.AssertResult(t, 2, childInfo.Line) + test.AssertResult(t, 3, childInfo.Column) +} diff --git a/pkg/yqlib/candidate_node_yaml.go b/pkg/yqlib/candidate_node_yaml.go index c80af2a2..14beb073 100644 --- a/pkg/yqlib/candidate_node_yaml.go +++ b/pkg/yqlib/candidate_node_yaml.go @@ -3,7 +3,7 @@ package yqlib import ( "fmt" - yaml "gopkg.in/yaml.v3" + yaml "go.yaml.in/yaml/v3" ) func MapYamlStyle(original yaml.Style) Style { diff --git a/pkg/yqlib/candidiate_node_json.go b/pkg/yqlib/candidiate_node_json.go index fe7d344e..f1ce277e 100644 --- a/pkg/yqlib/candidiate_node_json.go +++ b/pkg/yqlib/candidiate_node_json.go @@ -1,3 +1,5 @@ +//go:build !yq_nojson + package yqlib import ( diff --git a/pkg/yqlib/context_test.go b/pkg/yqlib/context_test.go index 13ec0e2f..0dadd2fd 100644 --- a/pkg/yqlib/context_test.go +++ b/pkg/yqlib/context_test.go @@ -2,9 +2,11 @@ package yqlib import ( "container/list" + "strings" "testing" "github.com/mikefarah/yq/v4/test" + logging "gopkg.in/op/go-logging.v1" ) func TestChildContext(t *testing.T) { @@ -49,3 +51,211 @@ func TestChildContextNoVariables(t *testing.T) { test.AssertResultComplex(t, make(map[string]*list.List), clone.Variables) } + +func TestSingleReadonlyChildContext(t *testing.T) { + original := Context{ + DontAutoCreate: false, + datetimeLayout: "2006-01-02", + } + + candidate := &CandidateNode{Value: "test"} + clone := original.SingleReadonlyChildContext(candidate) + + // Should have DontAutoCreate set to true + test.AssertResultComplex(t, true, clone.DontAutoCreate) + + // Should have the candidate node in MatchingNodes + test.AssertResultComplex(t, 1, clone.MatchingNodes.Len()) + test.AssertResultComplex(t, candidate, clone.MatchingNodes.Front().Value) +} + +func TestSingleChildContext(t *testing.T) { + original := Context{ + DontAutoCreate: true, + datetimeLayout: "2006-01-02", + } + + candidate := &CandidateNode{Value: "test"} + clone := original.SingleChildContext(candidate) + + // Should preserve DontAutoCreate + test.AssertResultComplex(t, true, clone.DontAutoCreate) + + // Should have the candidate node in MatchingNodes + test.AssertResultComplex(t, 1, clone.MatchingNodes.Len()) + test.AssertResultComplex(t, candidate, clone.MatchingNodes.Front().Value) +} + +func TestSetDateTimeLayout(t *testing.T) { + context := Context{} + + // Test setting datetime layout + context.SetDateTimeLayout("2006-01-02T15:04:05Z07:00") + test.AssertResultComplex(t, "2006-01-02T15:04:05Z07:00", context.datetimeLayout) +} + +func TestGetDateTimeLayout(t *testing.T) { + // Test with custom layout + context := Context{datetimeLayout: "2006-01-02"} + result := context.GetDateTimeLayout() + test.AssertResultComplex(t, "2006-01-02", result) + + // Test with empty layout (should return default) + context = Context{} + result = context.GetDateTimeLayout() + test.AssertResultComplex(t, "2006-01-02T15:04:05Z07:00", result) +} + +func TestGetVariable(t *testing.T) { + // Test with nil Variables + context := Context{} + result := context.GetVariable("nonexistent") + test.AssertResultComplex(t, (*list.List)(nil), result) + + // Test with existing variable + variables := make(map[string]*list.List) + variables["test"] = list.New() + variables["test"].PushBack(&CandidateNode{Value: "value"}) + + context = Context{Variables: variables} + result = context.GetVariable("test") + test.AssertResultComplex(t, variables["test"], result) + + // Test with non-existent variable + result = context.GetVariable("nonexistent") + test.AssertResultComplex(t, (*list.List)(nil), result) +} + +func TestSetVariable(t *testing.T) { + // Test setting variable when Variables is nil + context := Context{} + value := list.New() + value.PushBack(&CandidateNode{Value: "test"}) + + context.SetVariable("key", value) + test.AssertResultComplex(t, value, context.Variables["key"]) + + // Test setting variable when Variables already exists + context.SetVariable("key2", value) + test.AssertResultComplex(t, value, context.Variables["key2"]) +} + +func TestToString(t *testing.T) { + context := Context{ + DontAutoCreate: true, + MatchingNodes: list.New(), + } + + // Add a node to test the full string representation + node := &CandidateNode{Value: "test"} + context.MatchingNodes.PushBack(node) + + // Test with debug logging disabled (default) + result := context.ToString() + test.AssertResultComplex(t, "", result) + + // Test with debug logging enabled + logging.SetLevel(logging.DEBUG, "") + defer logging.SetLevel(logging.INFO, "") // Reset to default + + result2 := context.ToString() + test.AssertResultComplex(t, true, len(result2) > 0) + test.AssertResultComplex(t, true, strings.Contains(result2, "Context")) + test.AssertResultComplex(t, true, strings.Contains(result2, "DontAutoCreate: true")) +} + +func TestDeepClone(t *testing.T) { + // Create original context with variables and matching nodes + originalVariables := make(map[string]*list.List) + originalVariables["test"] = list.New() + originalVariables["test"].PushBack(&CandidateNode{Value: "original"}) + + original := Context{ + DontAutoCreate: true, + datetimeLayout: "2006-01-02", + Variables: originalVariables, + MatchingNodes: list.New(), + } + + // Add a node to MatchingNodes + node := &CandidateNode{Value: "test"} + original.MatchingNodes.PushBack(node) + + clone := original.DeepClone() + + // Should preserve DontAutoCreate and datetimeLayout + test.AssertResultComplex(t, original.DontAutoCreate, clone.DontAutoCreate) + test.AssertResultComplex(t, original.datetimeLayout, clone.datetimeLayout) + + // Should have copied variables + test.AssertResultComplex(t, 1, len(clone.Variables)) + test.AssertResultComplex(t, "original", clone.Variables["test"].Front().Value.(*CandidateNode).Value) + + // Should have deep copied MatchingNodes + test.AssertResultComplex(t, 1, clone.MatchingNodes.Len()) + + // Verify it's a deep copy by modifying the original + original.MatchingNodes.Front().Value.(*CandidateNode).Value = "modified" + test.AssertResultComplex(t, "test", clone.MatchingNodes.Front().Value.(*CandidateNode).Value) +} + +func TestClone(t *testing.T) { + // Create original context + original := Context{ + DontAutoCreate: true, + datetimeLayout: "2006-01-02", + MatchingNodes: list.New(), + } + + node := &CandidateNode{Value: "test"} + original.MatchingNodes.PushBack(node) + + clone := original.Clone() + + // Should preserve DontAutoCreate and datetimeLayout + test.AssertResultComplex(t, original.DontAutoCreate, clone.DontAutoCreate) + test.AssertResultComplex(t, original.datetimeLayout, clone.datetimeLayout) + + // Should have the same MatchingNodes reference + test.AssertResultComplex(t, original.MatchingNodes, clone.MatchingNodes) +} + +func TestReadOnlyClone(t *testing.T) { + original := Context{ + DontAutoCreate: false, + datetimeLayout: "2006-01-02", + MatchingNodes: list.New(), + } + + node := &CandidateNode{Value: "test"} + original.MatchingNodes.PushBack(node) + + clone := original.ReadOnlyClone() + + // Should set DontAutoCreate to true + test.AssertResultComplex(t, true, clone.DontAutoCreate) + + // Should preserve other fields + test.AssertResultComplex(t, original.datetimeLayout, clone.datetimeLayout) + test.AssertResultComplex(t, original.MatchingNodes, clone.MatchingNodes) +} + +func TestWritableClone(t *testing.T) { + original := Context{ + DontAutoCreate: true, + datetimeLayout: "2006-01-02", + MatchingNodes: list.New(), + } + + node := &CandidateNode{Value: "test"} + original.MatchingNodes.PushBack(node) + + clone := original.WritableClone() + + // Should set DontAutoCreate to false + test.AssertResultComplex(t, false, clone.DontAutoCreate) + + // Should preserve other fields + test.AssertResultComplex(t, original.datetimeLayout, clone.datetimeLayout) + test.AssertResultComplex(t, original.MatchingNodes, clone.MatchingNodes) +} diff --git a/pkg/yqlib/data_tree_navigator.go b/pkg/yqlib/data_tree_navigator.go index 7e0c7c72..05e1cba4 100644 --- a/pkg/yqlib/data_tree_navigator.go +++ b/pkg/yqlib/data_tree_navigator.go @@ -64,6 +64,6 @@ func (d *dataTreeNavigator) GetMatchingNodes(context Context, expressionNode *Ex if handler != nil { return handler(d, context, expressionNode) } - return Context{}, fmt.Errorf("unknown operator %v", expressionNode.Operation.OperationType) + return Context{}, fmt.Errorf("unknown operator %v", expressionNode.Operation.OperationType.Type) } diff --git a/pkg/yqlib/data_tree_navigator_test.go b/pkg/yqlib/data_tree_navigator_test.go new file mode 100644 index 00000000..a0e2e59f --- /dev/null +++ b/pkg/yqlib/data_tree_navigator_test.go @@ -0,0 +1,437 @@ +package yqlib + +import ( + "container/list" + "testing" + + "github.com/mikefarah/yq/v4/test" +) + +func TestGetMatchingNodes_NilExpressionNode(t *testing.T) { + navigator := NewDataTreeNavigator() + context := Context{ + MatchingNodes: list.New(), + } + + result, err := navigator.GetMatchingNodes(context, nil) + + test.AssertResult(t, nil, err) + test.AssertResultComplex(t, context, result) +} + +func TestGetMatchingNodes_UnknownOperator(t *testing.T) { + navigator := NewDataTreeNavigator() + context := Context{ + MatchingNodes: list.New(), + } + + // Create an expression node with an unknown operation type + unknownOpType := &operationType{Type: "UNKNOWN", Handler: nil} + expressionNode := &ExpressionNode{ + Operation: &Operation{OperationType: unknownOpType}, + } + + result, err := navigator.GetMatchingNodes(context, expressionNode) + + test.AssertResult(t, "unknown operator UNKNOWN", err.Error()) + test.AssertResultComplex(t, Context{}, result) +} + +func TestGetMatchingNodes_ValidOperator(t *testing.T) { + navigator := NewDataTreeNavigator() + + // Create a simple context with a scalar node + scalarNode := &CandidateNode{ + Kind: ScalarNode, + Tag: "!!str", + Value: "test", + } + context := Context{ + MatchingNodes: list.New(), + } + context.MatchingNodes.PushBack(scalarNode) + + // Create an expression node with a valid operation (self reference) + expressionNode := &ExpressionNode{ + Operation: &Operation{OperationType: selfReferenceOpType}, + } + + result, err := navigator.GetMatchingNodes(context, expressionNode) + + test.AssertResult(t, nil, err) + test.AssertResult(t, 1, result.MatchingNodes.Len()) + + // Verify the result contains the same node + resultNode := result.MatchingNodes.Front().Value.(*CandidateNode) + test.AssertResult(t, scalarNode, resultNode) +} + +func TestDeeplyAssign_ScalarNode(t *testing.T) { + navigator := NewDataTreeNavigator() + + // Create a context with a root mapping node + rootNode := &CandidateNode{ + Kind: MappingNode, + Tag: "!!map", + Content: []*CandidateNode{ + {Kind: ScalarNode, Tag: "!!str", Value: "existing", IsMapKey: true}, + {Kind: ScalarNode, Tag: "!!str", Value: "old_value"}, + }, + } + context := Context{ + MatchingNodes: list.New(), + } + context.MatchingNodes.PushBack(rootNode) + + // Create a scalar node to assign + scalarNode := &CandidateNode{ + Kind: ScalarNode, + Tag: "!!str", + Value: "new_value", + } + + // Assign to path ["new_key"] + path := []interface{}{"new_key"} + err := navigator.DeeplyAssign(context, path, scalarNode) + + test.AssertResult(t, nil, err) + + // Verify the assignment was made + // The root node should now have the new key-value pair + test.AssertResult(t, 4, len(rootNode.Content)) // 2 original + 2 new + + // Find the new key-value pair + found := false + for i := 0; i < len(rootNode.Content)-1; i += 2 { + key := rootNode.Content[i] + value := rootNode.Content[i+1] + if key.Value == "new_key" && value.Value == "new_value" { + found = true + break + } + } + test.AssertResult(t, true, found) +} + +func TestDeeplyAssign_MappingNode(t *testing.T) { + navigator := NewDataTreeNavigator() + + // Create a context with a root mapping node + rootNode := &CandidateNode{ + Kind: MappingNode, + Tag: "!!map", + Content: []*CandidateNode{ + {Kind: ScalarNode, Tag: "!!str", Value: "existing", IsMapKey: true}, + {Kind: ScalarNode, Tag: "!!str", Value: "old_value"}, + }, + } + context := Context{ + MatchingNodes: list.New(), + } + context.MatchingNodes.PushBack(rootNode) + + // Create a mapping node to assign (this should trigger deep merge) + mappingNode := &CandidateNode{ + Kind: MappingNode, + Tag: "!!map", + Content: []*CandidateNode{ + {Kind: ScalarNode, Tag: "!!str", Value: "nested_key", IsMapKey: true}, + {Kind: ScalarNode, Tag: "!!str", Value: "nested_value"}, + }, + } + + // Assign to path ["new_map"] + path := []interface{}{"new_map"} + err := navigator.DeeplyAssign(context, path, mappingNode) + + test.AssertResult(t, nil, err) + + // Verify the assignment was made + // The root node should now have the new mapping + test.AssertResult(t, 4, len(rootNode.Content)) // 2 original + 2 new + + // Find the new mapping + found := false + for i := 0; i < len(rootNode.Content); i += 2 { + if i+1 < len(rootNode.Content) { + key := rootNode.Content[i] + value := rootNode.Content[i+1] + if key.Value == "new_map" && value.Kind == MappingNode { + found = true + // Verify the nested content + test.AssertResult(t, 2, len(value.Content)) + test.AssertResult(t, "nested_key", value.Content[0].Value) + test.AssertResult(t, "nested_value", value.Content[1].Value) + break + } + } + } + test.AssertResult(t, true, found) +} + +func TestDeeplyAssign_DeepPath(t *testing.T) { + navigator := NewDataTreeNavigator() + + // Create a context with a root mapping node + rootNode := &CandidateNode{ + Kind: MappingNode, + Tag: "!!map", + Content: []*CandidateNode{ + {Kind: ScalarNode, Tag: "!!str", Value: "level1", IsMapKey: true}, + {Kind: MappingNode, Tag: "!!map", Content: []*CandidateNode{}}, + }, + } + context := Context{ + MatchingNodes: list.New(), + } + context.MatchingNodes.PushBack(rootNode) + + // Create a scalar node to assign + scalarNode := &CandidateNode{ + Kind: ScalarNode, + Tag: "!!str", + Value: "deep_value", + } + + // Assign to deep path ["level1", "level2", "level3"] + path := []interface{}{"level1", "level2", "level3"} + err := navigator.DeeplyAssign(context, path, scalarNode) + + test.AssertResult(t, nil, err) + + // Verify the deep assignment was made + level1Node := rootNode.Content[1] // The mapping node + test.AssertResult(t, 2, len(level1Node.Content)) // Should have level2 key-value + + level2Key := level1Node.Content[0] + level2Value := level1Node.Content[1] + test.AssertResult(t, "level2", level2Key.Value) + test.AssertResult(t, MappingNode, level2Value.Kind) + + level3Key := level2Value.Content[0] + level3Value := level2Value.Content[1] + test.AssertResult(t, "level3", level3Key.Value) + test.AssertResult(t, "deep_value", level3Value.Value) +} + +func TestDeeplyAssign_ArrayPath(t *testing.T) { + navigator := NewDataTreeNavigator() + + // Create a context with a root mapping node containing an array + rootNode := &CandidateNode{ + Kind: MappingNode, + Tag: "!!map", + Content: []*CandidateNode{ + {Kind: ScalarNode, Tag: "!!str", Value: "array", IsMapKey: true}, + {Kind: SequenceNode, Tag: "!!seq", Content: []*CandidateNode{}}, + }, + } + context := Context{ + MatchingNodes: list.New(), + } + context.MatchingNodes.PushBack(rootNode) + + // Create a scalar node to assign + scalarNode := &CandidateNode{ + Kind: ScalarNode, + Tag: "!!str", + Value: "array_value", + } + + // Assign to array path ["array", 0] + path := []interface{}{"array", 0} + err := navigator.DeeplyAssign(context, path, scalarNode) + + test.AssertResult(t, nil, err) + + // Verify the array assignment was made + arrayNode := rootNode.Content[1] // The sequence node + test.AssertResult(t, 1, len(arrayNode.Content)) // Should have one element + + arrayElement := arrayNode.Content[0] + test.AssertResult(t, "array_value", arrayElement.Value) +} + +func TestDeeplyAssign_OverwriteExisting(t *testing.T) { + navigator := NewDataTreeNavigator() + + // Create a context with a root mapping node + rootNode := &CandidateNode{ + Kind: MappingNode, + Tag: "!!map", + Content: []*CandidateNode{ + {Kind: ScalarNode, Tag: "!!str", Value: "key", IsMapKey: true}, + {Kind: ScalarNode, Tag: "!!str", Value: "old_value"}, + }, + } + context := Context{ + MatchingNodes: list.New(), + } + context.MatchingNodes.PushBack(rootNode) + + // Create a scalar node to assign + scalarNode := &CandidateNode{ + Kind: ScalarNode, + Tag: "!!str", + Value: "new_value", + } + + // Assign to existing path ["key"] + path := []interface{}{"key"} + err := navigator.DeeplyAssign(context, path, scalarNode) + + test.AssertResult(t, nil, err) + + // Verify the value was overwritten + test.AssertResult(t, 2, len(rootNode.Content)) // Should still have 2 elements + + key := rootNode.Content[0] + value := rootNode.Content[1] + test.AssertResult(t, "key", key.Value) + test.AssertResult(t, "new_value", value.Value) // Should be overwritten +} + +func TestDeeplyAssign_ErrorHandling(t *testing.T) { + navigator := NewDataTreeNavigator() + + // Create a context with a scalar node (not a mapping) + scalarNode := &CandidateNode{ + Kind: ScalarNode, + Tag: "!!str", + Value: "not_a_map", + } + context := Context{ + MatchingNodes: list.New(), + } + context.MatchingNodes.PushBack(scalarNode) + + // Create a scalar node to assign + assignNode := &CandidateNode{ + Kind: ScalarNode, + Tag: "!!str", + Value: "value", + } + + // Try to assign to a path on a scalar (should fail) + path := []interface{}{"key"} + err := navigator.DeeplyAssign(context, path, assignNode) + + // Print the actual error for debugging + if err != nil { + t.Logf("Actual error: %v", err) + } + + // This should fail because we can't assign to a scalar + test.AssertResult(t, nil, err) +} + +func TestGetMatchingNodes_WithVariables(t *testing.T) { + navigator := NewDataTreeNavigator() + + // Create a context with variables + variables := make(map[string]*list.List) + varList := list.New() + varList.PushBack(&CandidateNode{Kind: ScalarNode, Tag: "!!str", Value: "var_value"}) + variables["test_var"] = varList + + context := Context{ + MatchingNodes: list.New(), + Variables: variables, + } + + // Create an expression node that gets a variable + expressionNode := &ExpressionNode{ + Operation: &Operation{OperationType: getVariableOpType, StringValue: "test_var"}, + } + + result, err := navigator.GetMatchingNodes(context, expressionNode) + + test.AssertResult(t, nil, err) + test.AssertResult(t, 1, result.MatchingNodes.Len()) + + // Verify the variable was retrieved + resultNode := result.MatchingNodes.Front().Value.(*CandidateNode) + test.AssertResult(t, "var_value", resultNode.Value) +} + +func TestGetMatchingNodes_EmptyContext(t *testing.T) { + navigator := NewDataTreeNavigator() + + // Create an empty context + context := Context{ + MatchingNodes: list.New(), + } + + // Create an expression node with self reference + expressionNode := &ExpressionNode{ + Operation: &Operation{OperationType: selfReferenceOpType}, + } + + result, err := navigator.GetMatchingNodes(context, expressionNode) + + test.AssertResult(t, nil, err) + test.AssertResult(t, 0, result.MatchingNodes.Len()) +} + +func TestDeeplyAssign_ComplexMappingMerge(t *testing.T) { + navigator := NewDataTreeNavigator() + + // Create a context with a root mapping node containing nested data + rootNode := &CandidateNode{ + Kind: MappingNode, + Tag: "!!map", + Content: []*CandidateNode{ + {Kind: ScalarNode, Tag: "!!str", Value: "config", IsMapKey: true}, + {Kind: MappingNode, Tag: "!!map", Content: []*CandidateNode{ + {Kind: ScalarNode, Tag: "!!str", Value: "existing_key", IsMapKey: true}, + {Kind: ScalarNode, Tag: "!!str", Value: "existing_value"}, + }}, + }, + } + context := Context{ + MatchingNodes: list.New(), + } + context.MatchingNodes.PushBack(rootNode) + + // Create a mapping node to merge + mappingNode := &CandidateNode{ + Kind: MappingNode, + Tag: "!!map", + Content: []*CandidateNode{ + {Kind: ScalarNode, Tag: "!!str", Value: "new_key", IsMapKey: true}, + {Kind: ScalarNode, Tag: "!!str", Value: "new_value"}, + {Kind: ScalarNode, Tag: "!!str", Value: "existing_key", IsMapKey: true}, + {Kind: ScalarNode, Tag: "!!str", Value: "updated_value"}, + }, + } + + // Assign to path ["config"] (should merge with existing mapping) + path := []interface{}{"config"} + err := navigator.DeeplyAssign(context, path, mappingNode) + + test.AssertResult(t, nil, err) + + // Verify the merge was successful + configNode := rootNode.Content[1] // The config mapping node + test.AssertResult(t, 4, len(configNode.Content)) // Should have 2 key-value pairs + + // Check that both existing and new keys are present + foundExisting := false + foundNew := false + for i := 0; i < len(configNode.Content); i += 2 { + if i+1 < len(configNode.Content) { + key := configNode.Content[i] + value := configNode.Content[i+1] + switch key.Value { + case "existing_key": + foundExisting = true + test.AssertResult(t, "updated_value", value.Value) // Should be updated + case "new_key": + foundNew = true + test.AssertResult(t, "new_value", value.Value) + } + } + } + test.AssertResult(t, true, foundExisting) + test.AssertResult(t, true, foundNew) +} diff --git a/pkg/yqlib/decoder_base64.go b/pkg/yqlib/decoder_base64.go index b5e9681e..2e45fef6 100644 --- a/pkg/yqlib/decoder_base64.go +++ b/pkg/yqlib/decoder_base64.go @@ -1,3 +1,5 @@ +//go:build !yq_nobase64 + package yqlib import ( diff --git a/pkg/yqlib/decoder_csv_object.go b/pkg/yqlib/decoder_csv_object.go index 21eb1369..6d7c31d4 100644 --- a/pkg/yqlib/decoder_csv_object.go +++ b/pkg/yqlib/decoder_csv_object.go @@ -1,3 +1,5 @@ +//go:build !yq_nocsv + package yqlib import ( diff --git a/pkg/yqlib/decoder_properties.go b/pkg/yqlib/decoder_properties.go index 778ba7ea..4acbdf95 100644 --- a/pkg/yqlib/decoder_properties.go +++ b/pkg/yqlib/decoder_properties.go @@ -1,3 +1,5 @@ +//go:build !yq_noprops + package yqlib import ( diff --git a/pkg/yqlib/decoder_uri.go b/pkg/yqlib/decoder_uri.go index cd08d23c..ad6a52d8 100644 --- a/pkg/yqlib/decoder_uri.go +++ b/pkg/yqlib/decoder_uri.go @@ -1,3 +1,5 @@ +//go:build !yq_nouri + package yqlib import ( diff --git a/pkg/yqlib/decoder_yaml.go b/pkg/yqlib/decoder_yaml.go index c17f9029..070b1658 100644 --- a/pkg/yqlib/decoder_yaml.go +++ b/pkg/yqlib/decoder_yaml.go @@ -8,7 +8,7 @@ import ( "regexp" "strings" - yaml "gopkg.in/yaml.v3" + yaml "go.yaml.in/yaml/v3" ) type yamlDecoder struct { diff --git a/pkg/yqlib/doc/operators/entries.md b/pkg/yqlib/doc/operators/entries.md index 232ff4df..4e2ce2dd 100644 --- a/pkg/yqlib/doc/operators/entries.md +++ b/pkg/yqlib/doc/operators/entries.md @@ -2,7 +2,7 @@ Similar to the same named functions in `jq` these functions convert to/from an object and an array of key-value pairs. This is most useful for performing operations on keys of maps. -Use `with_entries(op)` as a syntatic sugar for doing `to_entries | op | from_entries`. +Use `with_entries(op)` as a syntactic sugar for doing `to_entries | op | from_entries`. ## to_entries Map Given a sample.yml file of: diff --git a/pkg/yqlib/doc/operators/headers/entries.md b/pkg/yqlib/doc/operators/headers/entries.md index 85bdd262..731a8f78 100644 --- a/pkg/yqlib/doc/operators/headers/entries.md +++ b/pkg/yqlib/doc/operators/headers/entries.md @@ -2,4 +2,4 @@ Similar to the same named functions in `jq` these functions convert to/from an object and an array of key-value pairs. This is most useful for performing operations on keys of maps. -Use `with_entries(op)` as a syntatic sugar for doing `to_entries | op | from_entries`. +Use `with_entries(op)` as a syntactic sugar for doing `to_entries | op | from_entries`. diff --git a/pkg/yqlib/encoder_base64.go b/pkg/yqlib/encoder_base64.go index 11ef5a25..cfefbcd0 100644 --- a/pkg/yqlib/encoder_base64.go +++ b/pkg/yqlib/encoder_base64.go @@ -1,3 +1,5 @@ +//go:build !yq_nobase64 + package yqlib import ( diff --git a/pkg/yqlib/encoder_csv.go b/pkg/yqlib/encoder_csv.go index 1fa4354c..332f2699 100644 --- a/pkg/yqlib/encoder_csv.go +++ b/pkg/yqlib/encoder_csv.go @@ -1,3 +1,5 @@ +//go:build !yq_nocsv + package yqlib import ( diff --git a/pkg/yqlib/encoder_properties.go b/pkg/yqlib/encoder_properties.go index 21dedf36..5a60a114 100644 --- a/pkg/yqlib/encoder_properties.go +++ b/pkg/yqlib/encoder_properties.go @@ -1,3 +1,5 @@ +//go:build !yq_noprops + package yqlib import ( diff --git a/pkg/yqlib/encoder_sh.go b/pkg/yqlib/encoder_sh.go index 42b73eca..4cf8e804 100644 --- a/pkg/yqlib/encoder_sh.go +++ b/pkg/yqlib/encoder_sh.go @@ -1,3 +1,5 @@ +//go:build !yq_nosh + package yqlib import ( diff --git a/pkg/yqlib/encoder_shellvariables.go b/pkg/yqlib/encoder_shellvariables.go index 9b97c92a..be54efc6 100644 --- a/pkg/yqlib/encoder_shellvariables.go +++ b/pkg/yqlib/encoder_shellvariables.go @@ -1,3 +1,5 @@ +//go:build !yq_noshell + package yqlib import ( diff --git a/pkg/yqlib/encoder_uri.go b/pkg/yqlib/encoder_uri.go index 86bcc11a..3cc9ad34 100644 --- a/pkg/yqlib/encoder_uri.go +++ b/pkg/yqlib/encoder_uri.go @@ -1,3 +1,5 @@ +//go:build !yq_nouri + package yqlib import ( diff --git a/pkg/yqlib/encoder_yaml.go b/pkg/yqlib/encoder_yaml.go index f198c052..8c65899f 100644 --- a/pkg/yqlib/encoder_yaml.go +++ b/pkg/yqlib/encoder_yaml.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/fatih/color" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) type yamlEncoder struct { diff --git a/pkg/yqlib/format.go b/pkg/yqlib/format.go index a74ce599..67cc1e22 100644 --- a/pkg/yqlib/format.go +++ b/pkg/yqlib/format.go @@ -113,7 +113,7 @@ func FormatStringFromFilename(filename string) string { if filename != "" { GetLogger().Debugf("checking filename '%s' for auto format detection", filename) ext := filepath.Ext(filename) - if ext != "" && ext[0] == '.' { + if len(ext) >= 2 && ext[0] == '.' { format := strings.ToLower(ext[1:]) GetLogger().Debugf("detected format '%s'", format) return format diff --git a/pkg/yqlib/no_base64.go b/pkg/yqlib/no_base64.go new file mode 100644 index 00000000..b6fe32bc --- /dev/null +++ b/pkg/yqlib/no_base64.go @@ -0,0 +1,11 @@ +//go:build yq_nobase64 + +package yqlib + +func NewBase64Decoder() Decoder { + return nil +} + +func NewBase64Encoder() Encoder { + return nil +} diff --git a/pkg/yqlib/no_csv.go b/pkg/yqlib/no_csv.go new file mode 100644 index 00000000..8dbe44ca --- /dev/null +++ b/pkg/yqlib/no_csv.go @@ -0,0 +1,11 @@ +//go:build yq_nocsv + +package yqlib + +func NewCSVObjectDecoder(prefs CsvPreferences) Decoder { + return nil +} + +func NewCsvEncoder(prefs CsvPreferences) Encoder { + return nil +} diff --git a/pkg/yqlib/no_ini.go b/pkg/yqlib/no_ini.go index ef1ae8f8..df89b97c 100644 --- a/pkg/yqlib/no_ini.go +++ b/pkg/yqlib/no_ini.go @@ -6,6 +6,6 @@ func NewINIDecoder() Decoder { return nil } -func NewINIEncoder(indent int) Encoder { +func NewINIEncoder() Encoder { return nil } diff --git a/pkg/yqlib/no_json.go b/pkg/yqlib/no_json.go index ae9d531a..6e115573 100644 --- a/pkg/yqlib/no_json.go +++ b/pkg/yqlib/no_json.go @@ -6,6 +6,6 @@ func NewJSONDecoder() Decoder { return nil } -func NewJSONEncoder(indent int, colorise bool, unwrapScalar bool) Encoder { +func NewJSONEncoder(prefs JsonPreferences) Encoder { return nil } diff --git a/pkg/yqlib/no_props.go b/pkg/yqlib/no_props.go new file mode 100644 index 00000000..955246fc --- /dev/null +++ b/pkg/yqlib/no_props.go @@ -0,0 +1,11 @@ +//go:build yq_noprops + +package yqlib + +func NewPropertiesDecoder() Decoder { + return nil +} + +func NewPropertiesEncoder(prefs PropertiesPreferences) Encoder { + return nil +} diff --git a/pkg/yqlib/no_sh.go b/pkg/yqlib/no_sh.go new file mode 100644 index 00000000..0f4aaee7 --- /dev/null +++ b/pkg/yqlib/no_sh.go @@ -0,0 +1,7 @@ +//go:build yq_nosh + +package yqlib + +func NewShEncoder() Encoder { + return nil +} diff --git a/pkg/yqlib/no_shellvariables.go b/pkg/yqlib/no_shellvariables.go new file mode 100644 index 00000000..c2dfd76e --- /dev/null +++ b/pkg/yqlib/no_shellvariables.go @@ -0,0 +1,7 @@ +//go:build yq_noshell + +package yqlib + +func NewShellVariablesEncoder() Encoder { + return nil +} diff --git a/pkg/yqlib/no_uri.go b/pkg/yqlib/no_uri.go new file mode 100644 index 00000000..6deeb31e --- /dev/null +++ b/pkg/yqlib/no_uri.go @@ -0,0 +1,11 @@ +//go:build yq_nouri + +package yqlib + +func NewUriDecoder() Decoder { + return nil +} + +func NewUriEncoder() Encoder { + return nil +} diff --git a/pkg/yqlib/no_xml.go b/pkg/yqlib/no_xml.go index d3f96bb6..c1a850ad 100644 --- a/pkg/yqlib/no_xml.go +++ b/pkg/yqlib/no_xml.go @@ -6,6 +6,6 @@ func NewXMLDecoder(prefs XmlPreferences) Decoder { return nil } -func NewXMLEncoder(indent int, prefs XmlPreferences) Encoder { +func NewXMLEncoder(prefs XmlPreferences) Encoder { return nil } diff --git a/pkg/yqlib/operator_anchors_aliases.go b/pkg/yqlib/operator_anchors_aliases.go index e25d2497..1fa8ecca 100644 --- a/pkg/yqlib/operator_anchors_aliases.go +++ b/pkg/yqlib/operator_anchors_aliases.go @@ -304,7 +304,10 @@ func overrideEntry(node *CandidateNode, key *CandidateNode, value *CandidateNode log.Debugf("checking new content %v:%v", keyNode.Value, valueEl.Value.(*CandidateNode).Value) if keyNode.Value == key.Value && keyNode.Alias == nil && key.Alias == nil { log.Debugf("overridign new content") - valueEl.Value = value + if !ConfiguredYamlPreferences.FixMergeAnchorToSpec { + log.Warning("--yaml-fix-merge-anchor-to-spec is false; causing the merge anchor to override the existing value at %v which isn't to the yaml spec. This flag will default to true in late 2025.", keyNode.GetNicePath()) + valueEl.Value = value + } return nil } newEl = valueEl // move forward twice diff --git a/pkg/yqlib/operator_anchors_aliases_test.go b/pkg/yqlib/operator_anchors_aliases_test.go index 6a92eb0e..ec54fe6e 100644 --- a/pkg/yqlib/operator_anchors_aliases_test.go +++ b/pkg/yqlib/operator_anchors_aliases_test.go @@ -53,7 +53,54 @@ foobar: thing: foobar_thing ` +var explodeWhenKeysExistDocument = `objects: + - &circle + name: circle + shape: round + - name: ellipse + !!merge <<: *circle + - !!merge <<: *circle + name: egg +` + +var explodeWhenKeysExistLegacy = `D0, P[], (!!map)::objects: + - name: circle + shape: round + - name: circle + shape: round + - shape: round + name: egg +` + +var explodeWhenKeysExistExpected = `D0, P[], (!!map)::objects: + - name: circle + shape: round + - name: ellipse + shape: round + - shape: round + name: egg +` + +var fixedAnchorOperatorScenarios = []expressionScenario{ + { + skipDoc: true, + description: "merge anchor after existing keys", + subdescription: "legacy: overrides existing keys", + document: explodeWhenKeysExistDocument, + expression: "explode(.)", + expected: []string{explodeWhenKeysExistExpected}, + }, +} + var anchorOperatorScenarios = []expressionScenario{ + { + skipDoc: true, + description: "merge anchor after existing keys", + subdescription: "legacy: overrides existing keys", + document: explodeWhenKeysExistDocument, + expression: "explode(.)", + expected: []string{explodeWhenKeysExistLegacy}, + }, { skipDoc: true, description: "merge anchor not map", @@ -358,3 +405,11 @@ func TestAnchorAliasOperatorScenarios(t *testing.T) { } documentOperatorScenarios(t, "anchor-and-alias-operators", anchorOperatorScenarios) } + +func TestAnchorAliasOperatorAlignedToSpecScenarios(t *testing.T) { + ConfiguredYamlPreferences.FixMergeAnchorToSpec = true + for _, tt := range fixedAnchorOperatorScenarios { + testScenario(t, &tt) + } + ConfiguredYamlPreferences.FixMergeAnchorToSpec = false +} diff --git a/pkg/yqlib/operator_collect_object.go b/pkg/yqlib/operator_collect_object.go index 6d44f7b2..2e96b6f1 100644 --- a/pkg/yqlib/operator_collect_object.go +++ b/pkg/yqlib/operator_collect_object.go @@ -2,6 +2,7 @@ package yqlib import ( "container/list" + "fmt" ) /* @@ -34,6 +35,9 @@ func collectObjectOperator(d *dataTreeNavigator, originalContext Context, _ *Exp for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { candidateNode := el.Value.(*CandidateNode) + if len(candidateNode.Content) < len(first.Content) { + return Context{}, fmt.Errorf("CollectObject: mismatching node sizes; are you creating a map with mismatching key value pairs?") + } for i := 0; i < len(first.Content); i++ { log.Debugf("rotate[%v] = %v", i, NodeToString(candidateNode.Content[i])) diff --git a/pkg/yqlib/operator_collect_object_test.go b/pkg/yqlib/operator_collect_object_test.go index 431839e8..8fc09fac 100644 --- a/pkg/yqlib/operator_collect_object_test.go +++ b/pkg/yqlib/operator_collect_object_test.go @@ -12,6 +12,11 @@ var collectObjectOperatorScenarios = []expressionScenario{ "D0, P[name], (!!str)::mike\n", }, }, + { + skipDoc: true, + expression: `{"c": "a", "b", "d"}`, + expectedError: "CollectObject: mismatching node sizes; are you creating a map with mismatching key value pairs?", + }, { skipDoc: true, expression: `{"person": {"names": ["mike"]}} | .person.names[0]`, diff --git a/pkg/yqlib/printer_node_info.go b/pkg/yqlib/printer_node_info.go new file mode 100644 index 00000000..cd047aee --- /dev/null +++ b/pkg/yqlib/printer_node_info.go @@ -0,0 +1,76 @@ +package yqlib + +import ( + "bufio" + "container/list" + "io" + + "go.yaml.in/yaml/v3" +) + +type nodeInfoPrinter struct { + printerWriter PrinterWriter + appendixReader io.Reader + printedMatches bool +} + +func NewNodeInfoPrinter(printerWriter PrinterWriter) Printer { + return &nodeInfoPrinter{ + printerWriter: printerWriter, + } +} + +func (p *nodeInfoPrinter) SetNulSepOutput(_ bool) { +} + +func (p *nodeInfoPrinter) SetAppendix(reader io.Reader) { + p.appendixReader = reader +} + +func (p *nodeInfoPrinter) PrintedAnything() bool { + return p.printedMatches +} + +func (p *nodeInfoPrinter) PrintResults(matchingNodes *list.List) error { + + for el := matchingNodes.Front(); el != nil; el = el.Next() { + mappedDoc := el.Value.(*CandidateNode) + writer, errorWriting := p.printerWriter.GetWriter(mappedDoc) + if errorWriting != nil { + return errorWriting + } + bytes, err := yaml.Marshal(mappedDoc.ConvertToNodeInfo()) + if err != nil { + return err + } + if _, err := writer.Write(bytes); err != nil { + return err + } + if _, err := writer.Write([]byte("\n")); err != nil { + return err + } + p.printedMatches = true + if err := writer.Flush(); err != nil { + return err + } + } + + if p.appendixReader != nil { + writer, err := p.printerWriter.GetWriter(nil) + if err != nil { + return err + } + + log.Debug("Piping appendix reader...") + betterReader := bufio.NewReader(p.appendixReader) + _, err = io.Copy(writer, betterReader) + if err != nil { + return err + } + if err := writer.Flush(); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/yqlib/printer_node_info_test.go b/pkg/yqlib/printer_node_info_test.go new file mode 100644 index 00000000..91b29ccc --- /dev/null +++ b/pkg/yqlib/printer_node_info_test.go @@ -0,0 +1,51 @@ +package yqlib + +import ( + "bufio" + "bytes" + "container/list" + "strings" + "testing" + + "github.com/mikefarah/yq/v4/test" +) + +func TestNodeInfoPrinter_PrintResults(t *testing.T) { + // Create a simple CandidateNode + node := &CandidateNode{ + Kind: ScalarNode, + Style: DoubleQuotedStyle, + Tag: "!!str", + Value: "hello world", + Line: 5, + Column: 7, + HeadComment: "head", + LineComment: "line", + FootComment: "foot", + Anchor: "anchor", + } + listNodes := list.New() + listNodes.PushBack(node) + + var output bytes.Buffer + writer := bufio.NewWriter(&output) + printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer)) + err := printer.PrintResults(listNodes) + writer.Flush() + if err != nil { + t.Fatalf("PrintResults error: %v", err) + } + + outStr := output.String() + // Check for key NodeInfo fields in YAML output using substring checks + test.AssertResult(t, true, strings.Contains(outStr, "kind: ScalarNode")) + test.AssertResult(t, true, strings.Contains(outStr, "style: DoubleQuotedStyle")) + test.AssertResult(t, true, strings.Contains(outStr, "tag: '!!str'")) + test.AssertResult(t, true, strings.Contains(outStr, "value: hello world")) + test.AssertResult(t, true, strings.Contains(outStr, "line: 5")) + test.AssertResult(t, true, strings.Contains(outStr, "column: 7")) + test.AssertResult(t, true, strings.Contains(outStr, "headComment: head")) + test.AssertResult(t, true, strings.Contains(outStr, "lineComment: line")) + test.AssertResult(t, true, strings.Contains(outStr, "footComment: foot")) + test.AssertResult(t, true, strings.Contains(outStr, "anchor: anchor")) +} diff --git a/pkg/yqlib/yaml.go b/pkg/yqlib/yaml.go index 079bc749..1daba35d 100644 --- a/pkg/yqlib/yaml.go +++ b/pkg/yqlib/yaml.go @@ -7,6 +7,7 @@ type YamlPreferences struct { PrintDocSeparators bool UnwrapScalar bool EvaluateTogether bool + FixMergeAnchorToSpec bool } func NewDefaultYamlPreferences() YamlPreferences { @@ -17,6 +18,7 @@ func NewDefaultYamlPreferences() YamlPreferences { PrintDocSeparators: true, UnwrapScalar: true, EvaluateTogether: false, + FixMergeAnchorToSpec: false, } } @@ -28,6 +30,7 @@ func (p *YamlPreferences) Copy() YamlPreferences { PrintDocSeparators: p.PrintDocSeparators, UnwrapScalar: p.UnwrapScalar, EvaluateTogether: p.EvaluateTogether, + FixMergeAnchorToSpec: p.FixMergeAnchorToSpec, } } diff --git a/pkg/yqlib/yaml_test.go b/pkg/yqlib/yaml_test.go index acb978a0..903035a0 100644 --- a/pkg/yqlib/yaml_test.go +++ b/pkg/yqlib/yaml_test.go @@ -84,6 +84,13 @@ var yamlFormatScenarios = []formatScenario{ } var yamlParseScenarios = []expressionScenario{ + // { + // description: "with a unquoted question mark in the string", + // document: "foo: {bar: a?bc}", + // expected: []string{ + // "D0, P[], (!!map)::a: hello # things\n", + // }, + // }, { document: `a: hello # things`, expected: []string{ diff --git a/project-words.txt b/project-words.txt index a030b96c..a00398f2 100644 --- a/project-words.txt +++ b/project-words.txt @@ -257,4 +257,12 @@ noyaml nolint shortfile Unmarshalling -noini \ No newline at end of file +noini +nocsv +nobase64 +nouri +noprops +nosh +noshell +tinygo +nonexistent \ No newline at end of file diff --git a/release_notes.txt b/release_notes.txt index c1d0b431..3828661d 100644 --- a/release_notes.txt +++ b/release_notes.txt @@ -2,6 +2,8 @@ - Added INI support - Fixed 'add' operator when piped in with no data #2378, #2383, #2384 - Fixed delete after slice problem (bad node path) #2387 Thanks @antoinedeschenes + - Fixed yq small build Thanks @imzue + - Switched to YAML org supported go-yaml! - Bumped dependencies diff --git a/scripts/build-small-yq.sh b/scripts/build-small-yq.sh index 9ecfddab..d6845e93 100755 --- a/scripts/build-small-yq.sh +++ b/scripts/build-small-yq.sh @@ -1,2 +1,2 @@ #!/bin/bash -go build -tags "yq_nolua yq_notoml yq_noxml yq_nojson" -ldflags "-s -w" . \ No newline at end of file +go build -tags "yq_nolua yq_noini yq_notoml yq_noxml yq_nojson" -ldflags "-s -w" . \ No newline at end of file diff --git a/scripts/build-tinygo-yq.sh b/scripts/build-tinygo-yq.sh new file mode 100755 index 00000000..043a362d --- /dev/null +++ b/scripts/build-tinygo-yq.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Currently, the `yq_nojson` feature must be enabled when using TinyGo. +tinygo build -no-debug -tags "yq_nolua yq_noini yq_notoml yq_noxml yq_nojson yq_nocsv yq_nobase64 yq_nouri yq_noprops yq_nosh yq_noshell" . \ No newline at end of file diff --git a/scripts/devtools.sh b/scripts/devtools.sh index e162a893..7dac5be4 100755 --- a/scripts/devtools.sh +++ b/scripts/devtools.sh @@ -2,4 +2,4 @@ set -ex go mod download golang.org/x/tools@latest curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.5 -curl -sSfL https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s +curl -sSfL https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s v2.22.5 \ No newline at end of file diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 25846f03..9c6ef19d 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: yq -version: 'v4.45.4' +version: 'v4.46.1' summary: A lightweight and portable command-line data file processor description: | `yq` uses [jq](https://github.com/stedolan/jq) like syntax but works with yaml, json, xml, csv, properties and TOML files. @@ -7,13 +7,16 @@ base: core22 grade: stable # devel|stable. must be 'stable' to release into candidate/stable channels confinement: strict architectures: - - build-on: s390x - - build-on: ppc64el - - build-on: arm64 - - build-on: armhf - - build-on: amd64 - - build-on: i386 - - build-on: riscv64 + - build-on: [amd64] + build-for: [all] +# architectures: +# - build-on: s390x +# - build-on: ppc64el +# - build-on: arm64 +# - build-on: armhf +# - build-on: amd64 +# - build-on: i386 +# - build-on: riscv64 apps: yq: command: bin/yq @@ -24,6 +27,6 @@ parts: build-environment: - CGO_ENABLED: 0 source: https://github.com/mikefarah/yq.git - source-tag: v4.45.4 + source-tag: v4.46.1 build-snaps: - go/latest/stable diff --git a/test/format_test.go b/test/format_test.go new file mode 100644 index 00000000..ffa41ee1 --- /dev/null +++ b/test/format_test.go @@ -0,0 +1,36 @@ +package test + +import ( + "testing" + + "github.com/mikefarah/yq/v4/pkg/yqlib" +) + +// only test format detection based on filename extension +func TestFormatStringFromFilename(t *testing.T) { + cases := []struct { + filename string + expected string + }{ + // filenames that have extensions + {"file.yaml", "yaml"}, + {"FILE.JSON", "json"}, + {"file.properties", "properties"}, + {"file.xml", "xml"}, + {"file.unknown", "unknown"}, + + // filenames without extensions + {"file", "yaml"}, + {"a.dir/file", "yaml"}, + {"file.", "yaml"}, + {".", "yaml"}, + {"", "yaml"}, + } + + for _, c := range cases { + result := yqlib.FormatStringFromFilename(c.filename) + if result != c.expected { + t.Errorf("FormatStringFromFilename(%q) = %q, wanted: %q", c.filename, result, c.expected) + } + } +}