Skip to content

Commit d9e3939

Browse files
authored
feat: Add missing newlines and add flag --no-trailing-newline port google/go-jsonnet#843 (#613)
refs: #592
1 parent 8c3a8cd commit d9e3939

5 files changed

Lines changed: 194 additions & 12 deletions

File tree

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ usage: sjsonnet [sjsonnet-options] script-file
4444
--tla-str-file <str> <var>=<file> Provide top-level arguments variable as string from
4545
the file
4646
-y --yaml-stream Write output as a YAML stream of JSON documents
47+
--no-trailing-newline Do not add a trailing newline to the output
4748
--yaml-debug Generate source line comments in the output YAML doc to make it
4849
easier to figure out where values come from.
4950
--yaml-out Write output as a YAML document

sjsonnet/src-jvm-native/sjsonnet/Config.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ final case class Config(
145145
"Set maximum parser recursion depth to prevent stack overflow from deeply nested structures"
146146
)
147147
maxParserRecursionDepth: Int = 1000,
148+
@arg(
149+
name = "no-trailing-newline",
150+
doc = "Do not add a trailing newline to the output"
151+
)
152+
noTrailingNewline: Flag = Flag(),
148153
@arg(
149154
name = "broken-assertion-logic",
150155
doc =

sjsonnet/src-jvm-native/sjsonnet/SjsonnetMainBase.scala

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ object SjsonnetMainBase {
9595
customDoc = doc,
9696
autoPrintHelpAndExit = None
9797
)
98+
_ <- {
99+
if (config.noTrailingNewline.value && config.yamlStream.value)
100+
Left("error: cannot use --no-trailing-newline with --yaml-stream")
101+
else Right(())
102+
}
98103
file <- Right(config.file)
99104
outputStr <- mainConfigured(
100105
file,
@@ -131,7 +136,12 @@ object SjsonnetMainBase {
131136
case Right((config, str)) =>
132137
if (str.nonEmpty) {
133138
config.outputFile match {
134-
case None => stdout.println(str)
139+
case None =>
140+
// In multi mode, the file list on stdout always ends with a newline,
141+
// matching go-jsonnet/C++ jsonnet behavior. --no-trailing-newline only
142+
// affects the content written to the output files, not the file list.
143+
if (config.multi.isDefined || !config.noTrailingNewline.value) stdout.println(str)
144+
else stdout.print(str)
135145
case Some(f) => os.write.over(os.Path(f, wd), str)
136146
}
137147
}
@@ -162,8 +172,18 @@ object SjsonnetMainBase {
162172
case e => e.toString
163173
}
164174

165-
private def writeFile(config: Config, f: os.Path, contents: String): Either[String, Unit] =
166-
handleWriteFile(os.write.over(f, contents, createFolders = config.createDirs.value))
175+
private def writeFile(
176+
config: Config,
177+
f: os.Path,
178+
contents: String,
179+
trailingNewline: Boolean): Either[String, Unit] =
180+
handleWriteFile(
181+
os.write.over(
182+
f,
183+
if (trailingNewline) contents + "\n" else contents,
184+
createFolders = config.createDirs.value
185+
)
186+
)
167187

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

302322
(config.multi, config.yamlStream.value) match {
303323
case (Some(multiPath), _) =>
324+
val trailingNewline = !config.noTrailingNewline.value
304325
interp.interpret(jsonnetCode, OsPath(path)).flatMap {
305326
case obj: ujson.Obj =>
306327
val renderedFiles: Seq[Either[String, os.FilePath]] =
@@ -313,7 +334,7 @@ object SjsonnetMainBase {
313334
Right(writer.toString)
314335
}
315336
relPath = (os.FilePath(multiPath) / os.RelPath(f)).asInstanceOf[os.FilePath]
316-
_ <- writeFile(config, relPath.resolveFrom(wd), rendered)
337+
_ <- writeFile(config, relPath.resolveFrom(wd), rendered, trailingNewline)
317338
} yield relPath
318339
}
319340

@@ -333,7 +354,7 @@ object SjsonnetMainBase {
333354
)
334355
}
335356
case (None, true) =>
336-
// YAML stream
357+
// YAML stream (--no-trailing-newline is already rejected above for yaml-stream)
337358

338359
interp.interpret(jsonnetCode, OsPath(path)).flatMap {
339360
case arr: ujson.Arr =>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"hello.txt": "hello world",
3+
"bar.txt": "bar"
4+
}

sjsonnet/test/src-jvm/sjsonnet/MainTests.scala

Lines changed: 157 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ object MainTests extends TestSuite {
114114
assert((res, out, err) == ((0, expectedOut, "")))
115115

116116
val helloDestStr = os.read(multiDest / "hello")
117-
assert(helloDestStr == "1")
117+
assert(helloDestStr == "1\n")
118118

119119
val worldDestStr = os.read(multiDest / "world")
120-
assert(worldDestStr == expectedWorldDestStr)
120+
assert(worldDestStr == expectedWorldDestStr + "\n")
121121
}
122122

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

134134
val helloDestStr = os.read(multiDest / "hello")
135-
assert(helloDestStr == "1")
135+
assert(helloDestStr == "1\n")
136136

137137
val worldDestStr = os.read(multiDest / "world")
138-
assert(worldDestStr == expectedWorldDestStr)
138+
assert(worldDestStr == expectedWorldDestStr + "\n")
139139
}
140140

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

148148
val helloDestStr = os.read(multiDest / "hello")
149-
assert(helloDestStr == "1")
149+
assert(helloDestStr == "1\n")
150150

151151
val worldDestStr = os.read(multiDest / "world")
152152
assert(
153153
worldDestStr ==
154154
"""- 2
155155
|- three
156-
|- true""".stripMargin
156+
|- true
157+
|""".stripMargin
157158
)
158159
}
160+
161+
// -- Default trailing newline behavior (with newline) --
162+
163+
test("execString") {
164+
val source = """"hello""""
165+
val (res, out, err) = runMain(source, "--exec", "--string")
166+
assert((res, out, err) == ((0, "hello\n", "")))
167+
}
168+
169+
test("multiStringOutput") {
170+
val source = testSuiteRoot / "db" / "multi_string.jsonnet"
171+
val multiDest = os.temp.dir()
172+
val (res, out, err) = runMain(source, "--multi", multiDest, "--string")
173+
assert(res == 0)
174+
assert(err.isEmpty)
175+
176+
val helloDestStr = os.read(multiDest / "hello.txt")
177+
assert(helloDestStr == "hello world\n")
178+
179+
val barDestStr = os.read(multiDest / "bar.txt")
180+
assert(barDestStr == "bar\n")
181+
}
182+
183+
// -- No trailing newline behavior --
184+
185+
test("noTrailingNewline") {
186+
// Simple scalar output — default has trailing newline
187+
val (resDefault, outDefault, _) = runMain("42", "--exec")
188+
assert((resDefault, outDefault) == ((0, "42\n")))
189+
190+
// Simple scalar output — no trailing newline
191+
val (res1, out1, err1) = runMain("42", "--exec", "--no-trailing-newline")
192+
assert((res1, out1, err1) == ((0, "42", "")))
193+
194+
// Object output — default has trailing newline
195+
val (resObj, outObj, _) = runMain("""{"a": 1, "b": 2}""", "--exec")
196+
val expectedJsonWithNewline =
197+
"""{
198+
| "a": 1,
199+
| "b": 2
200+
|}
201+
|""".stripMargin
202+
assert((resObj, outObj) == ((0, expectedJsonWithNewline)))
203+
204+
// Object output — no trailing newline
205+
val (res2, out2, err2) =
206+
runMain("""{"a": 1, "b": 2}""", "--exec", "--no-trailing-newline")
207+
val expectedJson =
208+
"""{
209+
| "a": 1,
210+
| "b": 2
211+
|}""".stripMargin
212+
assert((res2, out2, err2) == ((0, expectedJson, "")))
213+
214+
// String output — default has trailing newline
215+
val (resStr, outStr, _) = runMain(""""hello"""", "--exec", "--string")
216+
assert((resStr, outStr) == ((0, "hello\n")))
217+
218+
// String output — no trailing newline
219+
val (res3, out3, err3) =
220+
runMain(""""hello"""", "--exec", "--string", "--no-trailing-newline")
221+
assert((res3, out3, err3) == ((0, "hello", "")))
222+
}
223+
224+
test("noTrailingNewlineMulti") {
225+
// Default multi — files have trailing newline
226+
val source = testSuiteRoot / "db" / "multi.jsonnet"
227+
val multiDestDefault = os.temp.dir()
228+
val (resDefault, _, _) = runMain(source, "--multi", multiDestDefault)
229+
assert(resDefault == 0)
230+
assert(os.read(multiDestDefault / "hello") == "1\n")
231+
assert(os.read(multiDestDefault / "world") == expectedWorldDestStr + "\n")
232+
233+
// No trailing newline multi — files have no trailing newline,
234+
// but the file list on stdout still ends with \n (matching go-jsonnet behavior)
235+
val multiDest = os.temp.dir()
236+
val (res, out, err) =
237+
runMain(source, "--multi", multiDest, "--no-trailing-newline")
238+
val expectedOut = s"$multiDest/hello\n$multiDest/world\n"
239+
assert((res, out, err) == ((0, expectedOut, "")))
240+
assert(os.read(multiDest / "hello") == "1")
241+
assert(os.read(multiDest / "world") == expectedWorldDestStr)
242+
}
243+
244+
test("noTrailingNewlineMultiString") {
245+
val source = testSuiteRoot / "db" / "multi_string.jsonnet"
246+
247+
// Default multi+string — files have trailing newline
248+
val multiDestDefault = os.temp.dir()
249+
val (resDefault, _, _) = runMain(source, "--multi", multiDestDefault, "--string")
250+
assert(resDefault == 0)
251+
assert(os.read(multiDestDefault / "hello.txt") == "hello world\n")
252+
assert(os.read(multiDestDefault / "bar.txt") == "bar\n")
253+
254+
// No trailing newline multi+string — files have no trailing newline,
255+
// but the file list on stdout still ends with \n (matching go-jsonnet behavior)
256+
val multiDest = os.temp.dir()
257+
val (res, out, err) =
258+
runMain(source, "--multi", multiDest, "--string", "--no-trailing-newline")
259+
val expectedOut = s"$multiDest/bar.txt\n$multiDest/hello.txt\n"
260+
assert((res, out, err) == ((0, expectedOut, "")))
261+
assert(os.read(multiDest / "hello.txt") == "hello world")
262+
assert(os.read(multiDest / "bar.txt") == "bar")
263+
}
264+
265+
test("noTrailingNewlineOutputFile") {
266+
// Default output-file — file has content without trailing newline (file mode)
267+
val source = "42"
268+
val destDefault = os.temp()
269+
val (resDefault, _, _) = runMain(source, "--exec", "--output-file", destDefault)
270+
assert(resDefault == 0)
271+
val defaultContent = os.read(destDefault)
272+
assert(defaultContent == "42")
273+
274+
// No trailing newline output-file — same behavior
275+
val dest = os.temp()
276+
val (res, out, err) =
277+
runMain(source, "--exec", "--no-trailing-newline", "--output-file", dest)
278+
assert((res, out, err) == ((0, "", "")))
279+
assert(os.read(dest) == "42")
280+
}
281+
282+
test("noTrailingNewlineYamlStreamError") {
283+
val source = testSuiteRoot / "db" / "stream.jsonnet"
284+
val (res, out, err) =
285+
runMain(source, "--yaml-stream", "--no-trailing-newline")
286+
assert(res == 1)
287+
assert(out.isEmpty)
288+
assert(err.contains("cannot use --no-trailing-newline with --yaml-stream"))
289+
}
290+
291+
test("noTrailingNewlineYamlOut") {
292+
// Default yaml-out — has trailing newline
293+
val source = "local x = [1]; local y = [2]; x + y"
294+
val (resDefault, outDefault, _) = runMain(source, "--exec", "--yaml-out")
295+
val expectedYamlWithNewline =
296+
"""- 1
297+
|- 2
298+
|
299+
|""".stripMargin
300+
assert((resDefault, outDefault) == ((0, expectedYamlWithNewline)))
301+
302+
// No trailing newline yaml-out — no trailing newline
303+
val (res, out, err) =
304+
runMain(source, "--exec", "--yaml-out", "--no-trailing-newline")
305+
val expectedYaml =
306+
"""- 1
307+
|- 2""".stripMargin
308+
assert((res, out, err) == ((0, expectedYaml, "")))
309+
}
159310
}
160311

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

0 commit comments

Comments
 (0)