From 2946b6ca2e5cb1e31a2488cdc102326d88b2dcee Mon Sep 17 00:00:00 2001 From: Samuel Chan Date: Thu, 7 Aug 2025 14:45:49 +0800 Subject: [PATCH 1/3] fix: Missing trailing newline when using both --multi and --string options --- interpreter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interpreter.go b/interpreter.go index 4bec1f6e9..663978837 100644 --- a/interpreter.go +++ b/interpreter.go @@ -914,7 +914,7 @@ func (i *interpreter) manifestAndSerializeMulti(v value, stringOutputMode bool) if stringOutputMode { switch val := fileJSON.(type) { case string: - r[filename] = val + r[filename] = val + "\n" default: msg := fmt.Sprintf("multi mode: top-level object's key %s has a value of type %T, "+ "should be a string", filename, val) From 2d72bc599a55b0c8ef7ad9679cc99acc10d85879 Mon Sep 17 00:00:00 2001 From: John Bartholomew Date: Tue, 27 Jan 2026 23:37:55 +0000 Subject: [PATCH 2/3] chore: extract repeated interpreter-construction code from eval functions --- interpreter.go | 30 +++--------------------------- vm.go | 32 ++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/interpreter.go b/interpreter.go index 663978837..30049eebb 100644 --- a/interpreter.go +++ b/interpreter.go @@ -1350,15 +1350,7 @@ func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, error) { return result, nil } -// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead -func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction, - maxStack int, ic *importCache, traceOut io.Writer, stringOutputMode bool, evalHook EvalHook) (string, error) { - - i, err := buildInterpreter(ext, nativeFuncs, maxStack, ic, traceOut, evalHook) - if err != nil { - return "", err - } - +func evaluate(i *interpreter, node ast.Node, tla vmExtMap, stringOutputMode bool) (string, error) { result, err := evaluateAux(i, node, tla) if err != nil { return "", err @@ -1379,15 +1371,7 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string] return buf.String(), nil } -// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead -func evaluateMulti(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction, - maxStack int, ic *importCache, traceOut io.Writer, stringOutputMode bool, evalHook EvalHook) (map[string]string, error) { - - i, err := buildInterpreter(ext, nativeFuncs, maxStack, ic, traceOut, evalHook) - if err != nil { - return nil, err - } - +func evaluateMulti(i *interpreter, node ast.Node, tla vmExtMap, stringOutputMode bool) (map[string]string, error) { result, err := evaluateAux(i, node, tla) if err != nil { return nil, err @@ -1399,15 +1383,7 @@ func evaluateMulti(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[st return manifested, err } -// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead -func evaluateStream(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction, - maxStack int, ic *importCache, traceOut io.Writer, evalHook EvalHook) ([]string, error) { - - i, err := buildInterpreter(ext, nativeFuncs, maxStack, ic, traceOut, evalHook) - if err != nil { - return nil, err - } - +func evaluateStream(i *interpreter, node ast.Node, tla vmExtMap) ([]string, error) { result, err := evaluateAux(i, node, tla) if err != nil { return nil, err diff --git a/vm.go b/vm.go index 08013d2c7..0f4f86daa 100644 --- a/vm.go +++ b/vm.go @@ -178,6 +178,10 @@ const ( // version is the current gojsonnet's version const version = "v0.21.0" +func (vm *VM) buildConfiguredInterpreter() (*interpreter, error) { + return buildInterpreter(vm.ext, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook) +} + // Evaluate evaluates a Jsonnet program given by an Abstract Syntax Tree // and returns serialized JSON as string. // TODO(sbarzowski) perhaps is should return JSON in standard Go representation @@ -187,7 +191,11 @@ func (vm *VM) Evaluate(node ast.Node) (val string, err error) { err = fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack()) } }() - return evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput, vm.EvalHook) + i, err := vm.buildConfiguredInterpreter() + if err != nil { + return "", err + } + return evaluate(i, node, vm.tla, vm.StringOutput) } // EvaluateStream evaluates a Jsonnet program given by an Abstract Syntax Tree @@ -198,7 +206,11 @@ func (vm *VM) EvaluateStream(node ast.Node) (output []string, err error) { err = fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack()) } }() - return evaluateStream(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook) + i, err := vm.buildConfiguredInterpreter() + if err != nil { + return nil, err + } + return evaluateStream(i, node, vm.tla) } // EvaluateMulti evaluates a Jsonnet program given by an Abstract Syntax Tree @@ -210,7 +222,11 @@ func (vm *VM) EvaluateMulti(node ast.Node) (output map[string]string, err error) err = fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack()) } }() - return evaluateMulti(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput, vm.EvalHook) + i, err := vm.buildConfiguredInterpreter() + if err != nil { + return nil, err + } + return evaluateMulti(i, node, vm.tla, vm.StringOutput) } func (vm *VM) evaluateSnippet(diagnosticFileName ast.DiagnosticFileName, filename string, snippet string, kind evalKind) (output interface{}, err error) { @@ -223,13 +239,17 @@ func (vm *VM) evaluateSnippet(diagnosticFileName ast.DiagnosticFileName, filenam if err != nil { return "", err } + i, err := vm.buildConfiguredInterpreter() + if err != nil { + return "", err + } switch kind { case evalKindRegular: - output, err = evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput, vm.EvalHook) + output, err = evaluate(i, node, vm.tla, vm.StringOutput) case evalKindMulti: - output, err = evaluateMulti(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.StringOutput, vm.EvalHook) + output, err = evaluateMulti(i, node, vm.tla, vm.StringOutput) case evalKindStream: - output, err = evaluateStream(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook) + output, err = evaluateStream(i, node, vm.tla) } if err != nil { return "", err From 0d99c17d4b895f8b3b850e1e18638397c2895799 Mon Sep 17 00:00:00 2001 From: John Bartholomew Date: Mon, 2 Feb 2026 22:36:00 +0000 Subject: [PATCH 3/3] feat: add flag --no-trailing-newline to prevent adding the trailing newline on outputs For https://github.com/google/go-jsonnet/issues/518 to allow users to keep current behaviour for --string --multi, which on go-jsonnet emitted output without adding an extra newline. --no-trailing-newline works with all modes except --yaml-stream. To avoid confusion, the CLI explicitly rejects the combination of both flags used together. --- cmd/jsonnet/cmd.go | 9 +++ interpreter.go | 20 ++++--- main_test.go | 3 + .../cpp-tests-override/help.golden.stdout | 55 ++++++++++++++++++ .../cpp-tests-override/no_args.golden.stderr | 57 +++++++++++++++++++ testdata/multi_no_newline.golden/bar.json | 7 +++ testdata/multi_no_newline.golden/foo.json | 4 ++ testdata/multi_no_newline.jsonnet | 13 +++++ testdata/multi_no_newline.linter.golden | 0 .../bar.txt | 1 + .../foo.txt | 1 + .../multi_no_newline_string_output.jsonnet | 6 ++ ...lti_no_newline_string_output.linter.golden | 0 testdata/multi_string_output.golden/bar.txt | 2 +- testdata/multi_string_output.jsonnet | 2 +- testdata/object_no_newline.golden | 3 + testdata/object_no_newline.jsonnet | 1 + testdata/object_no_newline.linter.golden | 0 vm.go | 12 ++-- 19 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 testdata/cpp-tests-override/help.golden.stdout create mode 100644 testdata/cpp-tests-override/no_args.golden.stderr create mode 100644 testdata/multi_no_newline.golden/bar.json create mode 100644 testdata/multi_no_newline.golden/foo.json create mode 100644 testdata/multi_no_newline.jsonnet create mode 100644 testdata/multi_no_newline.linter.golden create mode 100644 testdata/multi_no_newline_string_output.golden/bar.txt create mode 100644 testdata/multi_no_newline_string_output.golden/foo.txt create mode 100644 testdata/multi_no_newline_string_output.jsonnet create mode 100644 testdata/multi_no_newline_string_output.linter.golden create mode 100644 testdata/object_no_newline.golden create mode 100644 testdata/object_no_newline.jsonnet create mode 100644 testdata/object_no_newline.linter.golden diff --git a/cmd/jsonnet/cmd.go b/cmd/jsonnet/cmd.go index 464108d1b..9dab7ae78 100644 --- a/cmd/jsonnet/cmd.go +++ b/cmd/jsonnet/cmd.go @@ -51,6 +51,7 @@ func usage(o io.Writer) { fmt.Fprintln(o, " files") fmt.Fprintln(o, " -y / --yaml-stream Write output as a YAML stream of JSON documents") fmt.Fprintln(o, " -S / --string Expect a string, manifest as plain text") + fmt.Fprintln(o, " --no-trailing-newline Do not add a trailing newline to the output") fmt.Fprintln(o, " -s / --max-stack Number of allowed stack frames") fmt.Fprintln(o, " -t / --max-trace Max length of stack trace before cropping") fmt.Fprintln(o, " --version Print version") @@ -256,6 +257,8 @@ func processArgs(givenArgs []string, config *config, vm *jsonnet.VM) (processArg config.evalStream = true } else if arg == "-S" || arg == "--string" { vm.StringOutput = true + } else if arg == "--no-trailing-newline" { + vm.OutputNewline = false } else if len(arg) > 1 && arg[0] == '-' { return processArgsStatusFailure, fmt.Errorf("unrecognized argument: %s", arg) } else { @@ -263,6 +266,12 @@ func processArgs(givenArgs []string, config *config, vm *jsonnet.VM) (processArg } } + // --no-trailing-newline is meaningless when used with --yaml-stream + // so we explicitly reject it to prevent people from relying on it. + if config.evalStream && !vm.OutputNewline { + return processArgsStatusFailure, fmt.Errorf("cannot use --no-trailing-newline with --yaml-stream") + } + want := "filename" if config.filenameIsCode { want = "code" diff --git a/interpreter.go b/interpreter.go index 30049eebb..26df98a2e 100644 --- a/interpreter.go +++ b/interpreter.go @@ -902,7 +902,7 @@ func (i *interpreter) manifestString(buf *bytes.Buffer, v value) error { } } -func (i *interpreter) manifestAndSerializeMulti(v value, stringOutputMode bool) (r map[string]string, err error) { +func (i *interpreter) manifestAndSerializeMulti(v value, stringOutputMode bool, outputNewline bool) (r map[string]string, err error) { r = make(map[string]string) json, err := i.manifestJSON(v) if err != nil { @@ -911,21 +911,23 @@ func (i *interpreter) manifestAndSerializeMulti(v value, stringOutputMode bool) switch json := json.(type) { case map[string]interface{}: for filename, fileJSON := range json { + var buf bytes.Buffer if stringOutputMode { switch val := fileJSON.(type) { case string: - r[filename] = val + "\n" + buf.WriteString(val) default: msg := fmt.Sprintf("multi mode: top-level object's key %s has a value of type %T, "+ "should be a string", filename, val) return r, makeRuntimeError(msg, i.getCurrentStackTrace()) } } else { - var buf bytes.Buffer serializeJSON(fileJSON, true, "", &buf) + } + if outputNewline { buf.WriteString("\n") - r[filename] = buf.String() } + r[filename] = buf.String() } default: msg := fmt.Sprintf("multi mode: top-level object was a %s, "+ @@ -1350,7 +1352,7 @@ func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, error) { return result, nil } -func evaluate(i *interpreter, node ast.Node, tla vmExtMap, stringOutputMode bool) (string, error) { +func evaluate(i *interpreter, node ast.Node, tla vmExtMap, stringOutputMode bool, outputNewline bool) (string, error) { result, err := evaluateAux(i, node, tla) if err != nil { return "", err @@ -1367,18 +1369,20 @@ func evaluate(i *interpreter, node ast.Node, tla vmExtMap, stringOutputMode bool if err != nil { return "", err } - buf.WriteString("\n") + if outputNewline { + buf.WriteString("\n") + } return buf.String(), nil } -func evaluateMulti(i *interpreter, node ast.Node, tla vmExtMap, stringOutputMode bool) (map[string]string, error) { +func evaluateMulti(i *interpreter, node ast.Node, tla vmExtMap, stringOutputMode bool, outputNewline bool) (map[string]string, error) { result, err := evaluateAux(i, node, tla) if err != nil { return nil, err } i.stack.setCurrentTrace(manifestationTrace()) - manifested, err := i.manifestAndSerializeMulti(result, stringOutputMode) + manifested, err := i.manifestAndSerializeMulti(result, stringOutputMode, outputNewline) i.stack.clearCurrentTrace() return manifested, err } diff --git a/main_test.go b/main_test.go index 37fd5193a..b14f07268 100644 --- a/main_test.go +++ b/main_test.go @@ -108,6 +108,7 @@ type jsonnetInput struct { input []byte eKind evalKind stringOutputMode bool + noNewline bool // not nice to have a negative flag, but it gives the more relevant default extVars map[string]string extCode map[string]string } @@ -134,6 +135,7 @@ func runInternalJsonnet(i jsonnetInput) jsonnetResult { errFormatter := termErrorFormatter{pretty: true, maxStackTraceSize: 9} vm.StringOutput = i.stringOutputMode + vm.OutputNewline = !i.noNewline for name, value := range i.extVars { vm.ExtVar(name, value) } @@ -336,6 +338,7 @@ func runTest(t *testing.T, test *mainTest) { input: input, eKind: eKind, stringOutputMode: strings.HasSuffix(test.golden, "_string_output.golden"), + noNewline: strings.Contains(test.golden, "_no_newline"), extVars: test.meta.extVars, extCode: test.meta.extCode, }) diff --git a/testdata/cpp-tests-override/help.golden.stdout b/testdata/cpp-tests-override/help.golden.stdout new file mode 100644 index 000000000..11f96d1f5 --- /dev/null +++ b/testdata/cpp-tests-override/help.golden.stdout @@ -0,0 +1,55 @@ +Jsonnet commandline interpreter (Go implementation) v0.21.0 + +jsonnet {