Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ usage: sjsonnet [sjsonnet-options] script-file
--tla-str-file <str> <var>=<file> Provide top-level arguments variable as string from
the file
-y --yaml-stream Write output as a YAML stream of JSON documents
--no-trailing-newline Do not add a trailing newline to the output
--yaml-debug Generate source line comments in the output YAML doc to make it
easier to figure out where values come from.
--yaml-out Write output as a YAML document
Expand Down
5 changes: 5 additions & 0 deletions sjsonnet/src-jvm-native/sjsonnet/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ final case class Config(
"Set maximum parser recursion depth to prevent stack overflow from deeply nested structures"
)
maxParserRecursionDepth: Int = 1000,
@arg(
name = "no-trailing-newline",
doc = "Do not add a trailing newline to the output"
)
noTrailingNewline: Flag = Flag(),
@arg(
name = "broken-assertion-logic",
doc =
Expand Down
33 changes: 27 additions & 6 deletions sjsonnet/src-jvm-native/sjsonnet/SjsonnetMainBase.scala
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ object SjsonnetMainBase {
customDoc = doc,
autoPrintHelpAndExit = None
)
_ <- {
if (config.noTrailingNewline.value && config.yamlStream.value)
Left("error: cannot use --no-trailing-newline with --yaml-stream")
else Right(())
}
file <- Right(config.file)
outputStr <- mainConfigured(
file,
Expand Down Expand Up @@ -131,7 +136,12 @@ object SjsonnetMainBase {
case Right((config, str)) =>
if (str.nonEmpty) {
config.outputFile match {
case None => stdout.println(str)
case None =>
// In multi mode, the file list on stdout always ends with a newline,
// matching go-jsonnet/C++ jsonnet behavior. --no-trailing-newline only
// affects the content written to the output files, not the file list.
if (config.multi.isDefined || !config.noTrailingNewline.value) stdout.println(str)
else stdout.print(str)
case Some(f) => os.write.over(os.Path(f, wd), str)
}
}
Expand Down Expand Up @@ -162,8 +172,18 @@ object SjsonnetMainBase {
case e => e.toString
}

private def writeFile(config: Config, f: os.Path, contents: String): Either[String, Unit] =
handleWriteFile(os.write.over(f, contents, createFolders = config.createDirs.value))
private def writeFile(
config: Config,
f: os.Path,
contents: String,
trailingNewline: Boolean): Either[String, Unit] =
handleWriteFile(
os.write.over(
f,
if (trailingNewline) contents + "\n" else contents,
createFolders = config.createDirs.value
)
)

private def writeToFile(config: Config, wd: os.Path)(
materialize: Writer => Either[String, ?]): Either[String, String] = {
Expand Down Expand Up @@ -196,7 +216,7 @@ object SjsonnetMainBase {
writeToFile(config, wd) { writer =>
val renderer = rendererForConfig(writer, config, getCurrentPosition)
val res = interp.interpret0(jsonnetCode, OsPath(path), renderer)
if (config.yamlOut.value) writer.write('\n')
if (config.yamlOut.value && !config.noTrailingNewline.value) writer.write('\n')
res
}
}
Expand Down Expand Up @@ -301,6 +321,7 @@ object SjsonnetMainBase {

(config.multi, config.yamlStream.value) match {
case (Some(multiPath), _) =>
val trailingNewline = !config.noTrailingNewline.value
interp.interpret(jsonnetCode, OsPath(path)).flatMap {
case obj: ujson.Obj =>
val renderedFiles: Seq[Either[String, os.FilePath]] =
Expand All @@ -313,7 +334,7 @@ object SjsonnetMainBase {
Right(writer.toString)
}
relPath = (os.FilePath(multiPath) / os.RelPath(f)).asInstanceOf[os.FilePath]
_ <- writeFile(config, relPath.resolveFrom(wd), rendered)
_ <- writeFile(config, relPath.resolveFrom(wd), rendered, trailingNewline)
} yield relPath
}

Expand All @@ -333,7 +354,7 @@ object SjsonnetMainBase {
)
}
case (None, true) =>
// YAML stream
// YAML stream (--no-trailing-newline is already rejected above for yaml-stream)

interp.interpret(jsonnetCode, OsPath(path)).flatMap {
case arr: ujson.Arr =>
Expand Down
4 changes: 4 additions & 0 deletions sjsonnet/test/resources/db/multi_string.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"hello.txt": "hello world",
"bar.txt": "bar"
}
163 changes: 157 additions & 6 deletions sjsonnet/test/src-jvm/sjsonnet/MainTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ object MainTests extends TestSuite {
assert((res, out, err) == ((0, expectedOut, "")))

val helloDestStr = os.read(multiDest / "hello")
assert(helloDestStr == "1")
assert(helloDestStr == "1\n")

val worldDestStr = os.read(multiDest / "world")
assert(worldDestStr == expectedWorldDestStr)
assert(worldDestStr == expectedWorldDestStr + "\n")
}

test("multiOutputFile") {
Expand All @@ -132,10 +132,10 @@ object MainTests extends TestSuite {
assert(destStr == expectedOut)

val helloDestStr = os.read(multiDest / "hello")
assert(helloDestStr == "1")
assert(helloDestStr == "1\n")

val worldDestStr = os.read(multiDest / "world")
assert(worldDestStr == expectedWorldDestStr)
assert(worldDestStr == expectedWorldDestStr + "\n")
}

test("multiYamlOut") {
Expand All @@ -146,16 +146,167 @@ object MainTests extends TestSuite {
assert((res, out, err) == ((0, expectedOut, "")))

val helloDestStr = os.read(multiDest / "hello")
assert(helloDestStr == "1")
assert(helloDestStr == "1\n")

val worldDestStr = os.read(multiDest / "world")
assert(
worldDestStr ==
"""- 2
|- three
|- true""".stripMargin
|- true
|""".stripMargin
)
}

// -- Default trailing newline behavior (with newline) --

test("execString") {
val source = """"hello""""
val (res, out, err) = runMain(source, "--exec", "--string")
assert((res, out, err) == ((0, "hello\n", "")))
}

test("multiStringOutput") {
val source = testSuiteRoot / "db" / "multi_string.jsonnet"
val multiDest = os.temp.dir()
val (res, out, err) = runMain(source, "--multi", multiDest, "--string")
assert(res == 0)
assert(err.isEmpty)

val helloDestStr = os.read(multiDest / "hello.txt")
assert(helloDestStr == "hello world\n")

val barDestStr = os.read(multiDest / "bar.txt")
assert(barDestStr == "bar\n")
}

// -- No trailing newline behavior --

test("noTrailingNewline") {
// Simple scalar output — default has trailing newline
val (resDefault, outDefault, _) = runMain("42", "--exec")
assert((resDefault, outDefault) == ((0, "42\n")))

// Simple scalar output — no trailing newline
val (res1, out1, err1) = runMain("42", "--exec", "--no-trailing-newline")
assert((res1, out1, err1) == ((0, "42", "")))

// Object output — default has trailing newline
val (resObj, outObj, _) = runMain("""{"a": 1, "b": 2}""", "--exec")
val expectedJsonWithNewline =
"""{
| "a": 1,
| "b": 2
|}
|""".stripMargin
assert((resObj, outObj) == ((0, expectedJsonWithNewline)))

// Object output — no trailing newline
val (res2, out2, err2) =
runMain("""{"a": 1, "b": 2}""", "--exec", "--no-trailing-newline")
val expectedJson =
"""{
| "a": 1,
| "b": 2
|}""".stripMargin
assert((res2, out2, err2) == ((0, expectedJson, "")))

// String output — default has trailing newline
val (resStr, outStr, _) = runMain(""""hello"""", "--exec", "--string")
assert((resStr, outStr) == ((0, "hello\n")))

// String output — no trailing newline
val (res3, out3, err3) =
runMain(""""hello"""", "--exec", "--string", "--no-trailing-newline")
assert((res3, out3, err3) == ((0, "hello", "")))
}

test("noTrailingNewlineMulti") {
// Default multi — files have trailing newline
val source = testSuiteRoot / "db" / "multi.jsonnet"
val multiDestDefault = os.temp.dir()
val (resDefault, _, _) = runMain(source, "--multi", multiDestDefault)
assert(resDefault == 0)
assert(os.read(multiDestDefault / "hello") == "1\n")
assert(os.read(multiDestDefault / "world") == expectedWorldDestStr + "\n")

// No trailing newline multi — files have no trailing newline,
// but the file list on stdout still ends with \n (matching go-jsonnet behavior)
val multiDest = os.temp.dir()
val (res, out, err) =
runMain(source, "--multi", multiDest, "--no-trailing-newline")
val expectedOut = s"$multiDest/hello\n$multiDest/world\n"
assert((res, out, err) == ((0, expectedOut, "")))
assert(os.read(multiDest / "hello") == "1")
assert(os.read(multiDest / "world") == expectedWorldDestStr)
}

test("noTrailingNewlineMultiString") {
val source = testSuiteRoot / "db" / "multi_string.jsonnet"

// Default multi+string — files have trailing newline
val multiDestDefault = os.temp.dir()
val (resDefault, _, _) = runMain(source, "--multi", multiDestDefault, "--string")
assert(resDefault == 0)
assert(os.read(multiDestDefault / "hello.txt") == "hello world\n")
assert(os.read(multiDestDefault / "bar.txt") == "bar\n")

// No trailing newline multi+string — files have no trailing newline,
// but the file list on stdout still ends with \n (matching go-jsonnet behavior)
val multiDest = os.temp.dir()
val (res, out, err) =
runMain(source, "--multi", multiDest, "--string", "--no-trailing-newline")
val expectedOut = s"$multiDest/bar.txt\n$multiDest/hello.txt\n"
assert((res, out, err) == ((0, expectedOut, "")))
assert(os.read(multiDest / "hello.txt") == "hello world")
assert(os.read(multiDest / "bar.txt") == "bar")
}

test("noTrailingNewlineOutputFile") {
// Default output-file — file has content without trailing newline (file mode)
val source = "42"
val destDefault = os.temp()
val (resDefault, _, _) = runMain(source, "--exec", "--output-file", destDefault)
assert(resDefault == 0)
val defaultContent = os.read(destDefault)
assert(defaultContent == "42")

// No trailing newline output-file — same behavior
val dest = os.temp()
val (res, out, err) =
runMain(source, "--exec", "--no-trailing-newline", "--output-file", dest)
assert((res, out, err) == ((0, "", "")))
assert(os.read(dest) == "42")
}

test("noTrailingNewlineYamlStreamError") {
val source = testSuiteRoot / "db" / "stream.jsonnet"
val (res, out, err) =
runMain(source, "--yaml-stream", "--no-trailing-newline")
assert(res == 1)
assert(out.isEmpty)
assert(err.contains("cannot use --no-trailing-newline with --yaml-stream"))
}

test("noTrailingNewlineYamlOut") {
// Default yaml-out — has trailing newline
val source = "local x = [1]; local y = [2]; x + y"
val (resDefault, outDefault, _) = runMain(source, "--exec", "--yaml-out")
val expectedYamlWithNewline =
"""- 1
|- 2
|
|""".stripMargin
assert((resDefault, outDefault) == ((0, expectedYamlWithNewline)))

// No trailing newline yaml-out — no trailing newline
val (res, out, err) =
runMain(source, "--exec", "--yaml-out", "--no-trailing-newline")
val expectedYaml =
"""- 1
|- 2""".stripMargin
assert((res, out, err) == ((0, expectedYaml, "")))
}
}

def runMain(args: os.Shellable*): (Int, String, String) = {
Expand Down