Skip to content

Commit 95d167c

Browse files
committed
Merge branch 'PR303'
2 parents de35754 + 306dcf1 commit 95d167c

File tree

16 files changed

+324
-196
lines changed

16 files changed

+324
-196
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies {
1818
compile("com.offbytwo:docopt:0.6.0.20150202")
1919

2020
implementation("org.jetbrains.kotlin:kotlin-scripting-common:$kotlinVersion")
21+
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
2122
implementation("org.jetbrains.kotlin:kotlin-scripting-jvm:$kotlinVersion")
2223
implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies:$kotlinVersion")
2324
implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies-maven:$kotlinVersion")

src/main/kotlin/kscript/app/AppHelpers.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package kscript.app
22

33
import kscript.app.ShellUtils.requireInPath
44
import java.io.*
5+
import java.net.URI
56
import java.net.URL
67
import java.nio.file.Files
78
import java.nio.file.Paths
@@ -135,6 +136,10 @@ fun createTmpScript(scriptText: String, extension: String = "kts"): File {
135136
}
136137
}
137138

139+
fun isFile(uri: URI) = uri.scheme.startsWith("file")
140+
fun isUrl(uri: URI) = uri.scheme.startsWith("http") || uri.scheme.startsWith("https")
141+
142+
fun isUrl(scriptResource: String) = scriptResource.startsWith("http://") || scriptResource.startsWith("https://")
138143

139144
fun fetchFromURL(scriptURL: String): File {
140145
val urlHash = md5(scriptURL)
@@ -157,6 +162,24 @@ fun fetchFromURL(scriptURL: String): File {
157162
return urlCache
158163
}
159164

165+
fun fetchFromURI(scriptURI: URI): List<String> {
166+
if (isFile(scriptURI)) {
167+
return scriptURI.toURL().readText().lines()
168+
}
169+
170+
val urlHash = md5(scriptURI.toString())
171+
val urlCache = File(KSCRIPT_CACHE_DIR, "/url_cache_${urlHash}")
172+
173+
if (urlCache.exists()) {
174+
return urlCache.readText().lines()
175+
}
176+
177+
val urlContent = scriptURI.toURL().readText()
178+
urlCache.writeText(urlContent)
179+
180+
return urlContent.lines()
181+
}
182+
160183

161184
fun md5(byteProvider: () -> ByteArray): String {
162185
// from https://stackoverflow.com/questions/304268/getting-a-files-md5-checksum-in-java

src/main/kotlin/kscript/app/Kscript.kt

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -85,40 +85,46 @@ fun main(args: Array<String>) {
8585
quit(0)
8686
}
8787

88-
// note: with current impt we still don't support `kscript -1` where "-1" is a valid kotlin expression
88+
// note: with current implementation we still don't support `kscript -1` where "-1" is a valid kotlin expression
8989
val userArgs = args.dropWhile { it.startsWith("-") && it != "-" }.drop(1)
9090
val kscriptArgs = args.take(args.size - userArgs.size)
9191

9292
val docopt = DocOptWrapper(kscriptArgs, USAGE)
9393
val loggingEnabled = !docopt.getBoolean("silent")
9494

9595

96-
// create cache dir if it does not yet exist
97-
if (!KSCRIPT_CACHE_DIR.isDirectory) {
98-
KSCRIPT_CACHE_DIR.mkdir()
99-
}
100-
10196
// optionally clear up the jar cache
10297
if (docopt.getBoolean("clear-cache")) {
10398
info("Cleaning up cache...")
104-
KSCRIPT_CACHE_DIR.listFiles().forEach { it.delete() }
105-
// evalBash("rm -f ${KSCRIPT_CACHE_DIR}/*")
99+
KSCRIPT_CACHE_DIR.listFiles()?.forEach { it.delete() }
106100
quit(0)
107101
}
108102

103+
// create cache dir if it does not yet exist
104+
if (!KSCRIPT_CACHE_DIR.isDirectory) {
105+
KSCRIPT_CACHE_DIR.mkdir()
106+
}
107+
109108
// Resolve the script resource argument into an actual file
110109
val scriptResource = docopt.getString("script")
111110

112-
val enableSupportApi = docopt.getBoolean("text")
113-
val (rawScript, includeContext) = prepareScript(scriptResource)
111+
val (rawUri, includeContext) = prepareScript(scriptResource)
114112

115113
if (docopt.getBoolean("add-bootstrap-header")) {
114+
errorIf(!isFile(rawUri)) {
115+
"Can not add bootstrap header to URL resources: $rawUri"
116+
}
117+
118+
val rawScript = File(rawUri)
119+
116120
errorIf(!rawScript.canWrite()) {
117121
"Script file not writable: $rawScript"
118122
}
123+
119124
errorIf(rawScript.parentFile == SCRIPT_TEMP_DIR) {
120125
"Temporary script file detected: $rawScript, created from $scriptResource"
121126
}
127+
122128
val scriptLines = rawScript.readLines().dropWhile {
123129
it.startsWith("#!/") && it != "#!/bin/bash"
124130
}
@@ -140,25 +146,25 @@ fun main(args: Array<String>) {
140146
quit(0)
141147
}
142148

149+
val enableSupportApi = docopt.getBoolean("text")
150+
143151
// post process script (text-processing mode, custom dsl preamble, resolve includes)
144152
// and finally resolve all includes (see https://github.com/holgerbrandl/kscript/issues/34)
145-
val (scriptFile, includeURLs) = resolveIncludes(resolvePreambles(rawScript, enableSupportApi), includeContext)
146-
153+
val (scriptFile, includeURLs) = resolveIncludes(resolvePreambles(rawUri, enableSupportApi), includeContext)
147154

148155
val script = Script(scriptFile)
149156

150-
151157
// Find all //DEPS directives and concatenate their values
152-
val dependencies = (script.collectDependencies() + Script(rawScript).collectDependencies()).distinct()
153-
val customRepos = (script.collectRepos() + Script(rawScript).collectRepos()).distinct()
158+
val dependencies = script.collectDependencies().distinct()
159+
val customRepos = script.collectRepos().distinct()
154160

155161
// Extract kotlin arguments
156162
val kotlinOpts = script.collectRuntimeOptions()
157163
val compilerOpts = script.collectCompilerOptions()
158164

159165
// Create temporary dev environment
160166
if (docopt.getBoolean("idea")) {
161-
println(launchIdeaWithKscriptlet(rawScript, userArgs, dependencies, customRepos, includeURLs, compilerOpts))
167+
println(launchIdeaWithKscriptlet(scriptFile, userArgs, dependencies, customRepos, includeURLs, compilerOpts))
162168
exitProcess(0)
163169
}
164170

@@ -283,6 +289,7 @@ fun main(args: Array<String>) {
283289
}
284290

285291
var extClassPath = "${jarFile}${CP_SEPARATOR_CHAR}${KOTLIN_HOME}${File.separatorChar}lib${File.separatorChar}kotlin-script-runtime.jar"
292+
286293
if (classpath.isNotEmpty())
287294
extClassPath += CP_SEPARATOR_CHAR + classpath
288295

@@ -321,8 +328,7 @@ private fun versionCheck() {
321328
}
322329
}
323330

324-
325-
fun prepareScript(scriptResource: String): Pair<File, URI> {
331+
fun prepareScript(scriptResource: String): Pair<URI, URI> {
326332
var scriptFile: File?
327333

328334
// we need to keep track of the scripts dir or the working dir in case of stdin script to correctly resolve includes
@@ -382,13 +388,16 @@ fun prepareScript(scriptResource: String): Pair<File, URI> {
382388

383389
// note script file must be not null at this point
384390

385-
return Pair(scriptFile!!, includeContext)
391+
return Pair(scriptFile!!.toURI(), includeContext)
386392
}
387393

394+
private fun resolvePreambles(rawUri: URI, enableSupportApi: Boolean): URI {
395+
if (!isFile(rawUri)) {
396+
return rawUri
397+
}
388398

389-
private fun resolvePreambles(rawScript: File, enableSupportApi: Boolean): File {
390399
// include preamble for custom interpreters (see https://github.com/holgerbrandl/kscript/issues/67)
391-
var scriptFile = rawScript
400+
var scriptFile = File(rawUri)
392401

393402
System.getenv("CUSTOM_KSCRIPT_PREAMBLE")?.let { interpPreamble ->
394403
// rawScript = Script(rawScript!!).prependWith(interpPreamble).createTmpScript()
@@ -416,9 +425,6 @@ private fun resolvePreambles(rawScript: File, enableSupportApi: Boolean): File {
416425

417426
scriptFile = Script(scriptFile).prependWith("//INCLUDE ${preambleFile.absolutePath}").createTmpScript()
418427
}
419-
return scriptFile
420-
}
421-
422428

423-
private fun postProcessScript(inputFile: File?, includeContext: URI): IncludeResult =
424-
resolveIncludes(inputFile!!, includeContext)
429+
return scriptFile.toURI()
430+
}
Lines changed: 69 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package kscript.app
22

33
import java.io.File
4-
import java.io.FileNotFoundException
54
import java.net.URI
65
import java.net.URL
76

87
/**
98
* @author Holger Brandl
109
* @author Ilan Pillemer
10+
* @author Marcin Kuszczak
1111
*/
1212

1313
const val PACKAGE_STATEMENT_PREFIX = "package "
@@ -16,67 +16,88 @@ const val IMPORT_STATEMENT_PREFIX = "import " // todo make more solid by using o
1616
data class IncludeResult(val scriptFile: File, val includes: List<URL> = emptyList())
1717

1818
/** Resolve include declarations in a script file. Resolved script will be put into another temporary script */
19-
fun resolveIncludes(template: File, includeContext: URI = template.parentFile.toURI()): IncludeResult {
20-
var script = Script(template)
21-
22-
// just rewrite user scripts if includes a
23-
if (!script.any { isIncludeDirective(it) }) {
24-
return IncludeResult(template)
19+
fun resolveIncludes(uri: URI, includeContext: URI = uri.resolve(".")): IncludeResult {
20+
val scriptText = uri.toURL().readText()
21+
22+
val urlExtension = when {
23+
uri.toString().endsWith(".kt") -> "kt"
24+
uri.toString().endsWith(".kts") -> "kts"
25+
else -> if (scriptText.contains("fun main")) {
26+
"kt"
27+
} else {
28+
"kts"
29+
}
2530
}
2631

27-
val includes = emptyList<URL>().toMutableList()
28-
29-
// resolve as long as it takes. YAGNI but we do because we can!
30-
while (script.any { isIncludeDirective(it) }) {
31-
script = script.flatMap { line ->
32-
if (isIncludeDirective(line)) {
33-
val include = extractIncludeTarget(line)
34-
35-
val includeURL = when {
36-
isUrl(include) -> URL(include)
37-
include.startsWith("/") -> File(include).toURI().toURL()
38-
include.startsWith("~/") -> File(System.getenv("HOME")!! + include.substring(1)).toURI().toURL()
39-
else -> includeContext.resolve(URI(include.removePrefix("./"))).toURL()
40-
}
41-
42-
// test if include was processed already (aka include duplication, see #151)
43-
if (includes.map { it.path }.contains(includeURL.path)) {
44-
// include was already resolved, so we return an emtpy result here to avoid duplication errors
45-
emptyList()
46-
} else {
47-
includes.add(includeURL)
48-
49-
try {
50-
includeURL.readText().lines()
51-
} catch (e: FileNotFoundException) {
52-
errorMsg("Failed to resolve //INCLUDE '${include}'")
53-
System.err.println(e.message?.lines()!!.map { it.prependIndent("[kscript] [ERROR] ") })
54-
quit(1)
55-
}
56-
}
57-
} else {
58-
listOf(line)
32+
val includes = mutableListOf<URI>()
33+
val lines = resolve(isFile(uri), uri, includeContext, includes)
34+
val script = Script(lines, urlExtension)
35+
36+
return IncludeResult(script.consolidateStructure().createTmpScript(), includes.map { it.toURL() })
37+
}
38+
39+
private fun resolve(allowFileReferences: Boolean, scriptUri: URI, includeContext: URI, includes: MutableList<URI>): List<String> {
40+
val lines = readLines(scriptUri)
41+
val result = mutableListOf<String>()
42+
43+
for (line in lines) {
44+
if (isIncludeDirective(line)) {
45+
val include = extractTarget(line)
46+
val includeUri = resolveUri(includeContext, include)
47+
48+
if (!allowFileReferences && isFile(includeUri)) {
49+
errorMsg("References to local filesystem from remote scripts are not allowed.\nIn script: $scriptUri; Reference: $includeUri")
50+
quit(1)
51+
}
52+
53+
// test if include was processed already (aka include duplication, see #151)
54+
if (includes.map { it.path }.contains(includeUri.path)) {
55+
// include was already resolved, so we just continue
56+
continue
5957
}
60-
}.let { script.copy(it) }
58+
59+
includes.add(includeUri)
60+
61+
val resolvedLines = resolve(allowFileReferences && isFile(includeUri), includeUri, includeUri.resolve("."), includes)
62+
result.addAll(resolvedLines)
63+
continue
64+
}
65+
66+
result.add(line)
6167
}
6268

63-
return IncludeResult(script.consolidateStructure().createTmpScript(), includes)
69+
return result
6470
}
6571

66-
internal fun isUrl(s: String) = s.startsWith("http://") || s.startsWith("https://")
72+
private fun readLines(uri: URI): List<String> {
73+
try {
74+
return fetchFromURI(uri)
75+
} catch (e: Exception) {
76+
errorMsg("Failed to resolve include with URI: '${uri}'")
77+
System.err.println(e.message?.lines()!!.map { it.prependIndent("[kscript] [ERROR] ") })
78+
quit(1)
79+
}
80+
}
6781

68-
private const val INCLUDE_ANNOT_PREFIX = "@file:Include("
82+
private fun resolveUri(scriptPath: URI, include: String): URI {
83+
val result = when {
84+
include.startsWith("/") -> File(include).toURI()
85+
include.startsWith("~/") -> File(System.getenv("HOME")!! + include.substring(1)).toURI()
86+
else -> scriptPath.resolve(URI(include.removePrefix("./")))
87+
}
6988

70-
internal fun isIncludeDirective(line: String) = line.startsWith("//INCLUDE") || line.startsWith(INCLUDE_ANNOT_PREFIX)
89+
return result.normalize()
90+
}
7191

92+
internal fun isIncludeDirective(line: String) = line.startsWith("//INCLUDE") || line.startsWith(INCLUDE_ANNOT_PREFIX)
7293

73-
internal fun extractIncludeTarget(incDirective: String) = when {
74-
incDirective.startsWith(INCLUDE_ANNOT_PREFIX) -> incDirective
75-
.replaceFirst(INCLUDE_ANNOT_PREFIX, "")
94+
internal fun extractTarget(incDirective: String) = when {
95+
incDirective.startsWith(INCLUDE_ANNOT_PREFIX) -> incDirective.replaceFirst(INCLUDE_ANNOT_PREFIX, "")
7696
.split(")")[0].trim(' ', '"')
7797
else -> incDirective.split("[ ]+".toRegex()).last()
7898
}
7999

100+
private const val INCLUDE_ANNOT_PREFIX = "@file:Include("
80101

81102
/**
82103
* Basic launcher used for testing
@@ -94,6 +115,6 @@ internal fun extractIncludeTarget(incDirective: String) = when {
94115
object ResolveIncludes {
95116
@JvmStatic
96117
fun main(args: Array<String>) {
97-
System.err.println(resolveIncludes(File(args[0])).scriptFile.readText())
118+
System.err.println(resolveIncludes(File(args[0]).toURI()).scriptFile.readText())
98119
}
99120
}

src/main/kotlin/kscript/app/Script.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ data class Script(val lines: List<String>, val extension: String = "kts") : Iter
77

88
constructor(scriptFile: File) : this(scriptFile.readLines(), scriptFile.extension)
99

10-
/** Returns a the namespace/package of the script (if declared). */
10+
/** Returns a namespace/package of the script (if declared). */
1111
val pckg by lazy {
1212
lines.find { it.startsWith("package ") }?.split("[ ]+".toRegex())?.get(1)?.run { this + "." }
1313
}

src/test/kotlin/Tests.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ class Tests {
196196
val file = File("test/resources/consolidate_includes/template.kts")
197197
val expected = File("test/resources/consolidate_includes/expected.kts")
198198

199-
val result = resolveIncludes(file)
199+
val result = resolveIncludes(file.toURI())
200200

201201
result.scriptFile.readText() shouldBe (expected.readText())
202202
}
@@ -205,24 +205,24 @@ class Tests {
205205
@Test
206206
fun test_include_annotations() {
207207
val file = File("test/resources/includes/include_variations.kts")
208-
val expected = File("test/resources/includes/expexcted_variations.kts")
208+
val expected = File("test/resources/includes/expected_variations.kts")
209209

210-
val result = resolveIncludes(file)
210+
val result = resolveIncludes(file.toURI())
211211

212212
result.scriptFile.readText() shouldBe (expected.readText())
213213
}
214214

215215
@Test
216216
fun test_include_detection() {
217-
val result = resolveIncludes(File("test/resources/includes/include_variations.kts"))
217+
val result = resolveIncludes(File("test/resources/includes/include_variations.kts").toURI())
218218

219-
result.includes.filter { it.protocol == "file" }.map { File(it.toURI()).name } shouldBe List(4) { "include_${it + 1}.kt" }
220-
result.includes.filter { it.protocol != "file" }.size shouldBe 1
219+
result.includes.filter { it.protocol == "file" }.map { File(it.toURI()).name } shouldBe List(7) { "include_${it + 1}.kt" }
220+
result.includes.filter { it.protocol != "file" }.size shouldBe 2
221221
}
222222

223223
@Test
224224
fun `test include detection - should not include dependency twice`() {
225-
val result = resolveIncludes(File("test/resources/includes/dup_include/dup_include.kts"))
225+
val result = resolveIncludes(File("test/resources/includes/dup_include/dup_include.kts").toURI())
226226

227227
result.includes.map { File(it.toURI()).name } shouldBe listOf(
228228
"dup_include_1.kt",

0 commit comments

Comments
 (0)