Skip to content

Commit f92c9e6

Browse files
committed
Final fixes.
1 parent 637ea1e commit f92c9e6

File tree

7 files changed

+94
-51
lines changed

7 files changed

+94
-51
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: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import java.io.File
88
import java.io.FileInputStream
99
import java.io.InputStreamReader
1010
import java.lang.IllegalArgumentException
11+
import java.net.URI
1112
import java.net.URL
1213
import java.net.UnknownHostException
1314
import java.util.*
@@ -92,31 +93,38 @@ fun main(args: Array<String>) {
9293
val loggingEnabled = !docopt.getBoolean("silent")
9394

9495

95-
// create cache dir if it does not yet exist
96-
if (!KSCRIPT_CACHE_DIR.isDirectory) {
97-
KSCRIPT_CACHE_DIR.mkdir()
98-
}
99-
10096
// optionally clear up the jar cache
10197
if (docopt.getBoolean("clear-cache")) {
10298
info("Cleaning up cache...")
103-
KSCRIPT_CACHE_DIR.listFiles().forEach { it.delete() }
99+
KSCRIPT_CACHE_DIR.listFiles()?.forEach { it.delete() }
104100
quit(0)
105101
}
106102

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

110-
val enableSupportApi = docopt.getBoolean("text")
111-
val rawScript = prepareScript(scriptResource)
111+
val rawUri = prepareScript(scriptResource)
112112

113113
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+
114120
errorIf(!rawScript.canWrite()) {
115121
"Script file not writable: $rawScript"
116122
}
123+
117124
errorIf(rawScript.parentFile == SCRIPT_TEMP_DIR) {
118125
"Temporary script file detected: $rawScript, created from $scriptResource"
119126
}
127+
120128
val scriptLines = rawScript.readLines().dropWhile {
121129
it.startsWith("#!/") && it != "#!/bin/bash"
122130
}
@@ -138,23 +146,25 @@ fun main(args: Array<String>) {
138146
quit(0)
139147
}
140148

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

145155
val script = Script(scriptFile)
146156

147157
// Find all //DEPS directives and concatenate their values
148-
val dependencies = (script.collectDependencies() + Script(rawScript).collectDependencies()).distinct()
149-
val customRepos = (script.collectRepos() + Script(rawScript).collectRepos()).distinct()
158+
val dependencies = script.collectDependencies().distinct()
159+
val customRepos = script.collectRepos().distinct()
150160

151161
// Extract kotlin arguments
152162
val kotlinOpts = script.collectRuntimeOptions()
153163
val compilerOpts = script.collectCompilerOptions()
154164

155165
// Create temporary dev environment
156166
if (docopt.getBoolean("idea")) {
157-
println(launchIdeaWithKscriptlet(rawScript, userArgs, dependencies, customRepos, includeURLs, compilerOpts))
167+
println(launchIdeaWithKscriptlet(scriptFile, userArgs, dependencies, customRepos, includeURLs, compilerOpts))
158168
exitProcess(0)
159169
}
160170

@@ -319,63 +329,64 @@ private fun versionCheck() {
319329
}
320330

321331

322-
fun prepareScript(scriptResource: String):File {
323-
var scriptFile: File?
332+
fun prepareScript(scriptResource: String): URI {
333+
if (isUrl(scriptResource)) {
334+
return URI(scriptResource)
335+
}
336+
337+
var scriptUri: URI?
324338

325339
// map script argument to script file
326-
scriptFile = with(File(scriptResource)) {
340+
scriptUri = with(File(scriptResource)) {
327341
if (!canRead()) {
328342
// not a file so let's keep the script-file undefined here
329343
null
330344
} else if (listOf("kts", "kt").contains(extension)) {
331-
this
345+
this.toURI()
332346
} else {
333347
// if we can "just" read from script resource create tmp file
334348
// i.e. script input is process substitution file handle
335349
// not FileInputStream(this).bufferedReader().use{ readText()} does not work nor does this.readText
336-
createTmpScript(FileInputStream(this).bufferedReader().readText())
350+
createTmpScript(FileInputStream(this).bufferedReader().readText()).toURI()
337351
}
338352
}
339353

340354
// support stdin
341355
if (scriptResource == "-" || scriptResource == "/dev/stdin") {
342356
val scriptText = generateSequence() { readLine() }.joinToString("\n").trim()
343-
scriptFile = createTmpScript(scriptText)
357+
scriptUri = createTmpScript(scriptText).toURI()
344358
}
345359

346-
347-
// Support URLs as script files
348-
if (scriptResource.startsWith("http://") || scriptResource.startsWith("https://")) {
349-
scriptFile = fetchFromURL(scriptResource)
350-
}
351-
352-
353360
// Support for support process substitution and direct script arguments
354-
if (scriptFile == null && !scriptResource.endsWith(".kts") && !scriptResource.endsWith(".kt")) {
361+
if (scriptUri == null && !scriptResource.endsWith(".kts") && !scriptResource.endsWith(".kt")) {
355362
val scriptText = if (File(scriptResource).canRead()) {
356363
File(scriptResource).readText().trim()
357364
} else {
358365
// the last resort is to assume the input to be a kotlin program
359366
scriptResource.trim()
360367
}
361368

362-
scriptFile = createTmpScript(scriptText)
369+
scriptUri = createTmpScript(scriptText).toURI()
363370
}
364371

365372
// just proceed if the script file is a regular file at this point
366-
errorIf(scriptFile == null || !scriptFile.canRead()) {
373+
errorIf(scriptUri == null) {
367374
"Could not read script argument '$scriptResource'"
368375
}
369376

370377
// note script file must be not null at this point
371378

372-
return scriptFile!!
379+
return scriptUri!!
373380
}
374381

375382

376-
private fun resolvePreambles(rawScript: File, enableSupportApi: Boolean): File {
383+
private fun resolvePreambles(rawUri: URI, enableSupportApi: Boolean): URI {
384+
if (!isFile(rawUri)) {
385+
return rawUri
386+
}
387+
377388
// include preamble for custom interpreters (see https://github.com/holgerbrandl/kscript/issues/67)
378-
var scriptFile = rawScript
389+
var scriptFile = File(rawUri)
379390

380391
System.getenv("CUSTOM_KSCRIPT_PREAMBLE")?.let { interpPreamble ->
381392
// rawScript = Script(rawScript!!).prependWith(interpPreamble).createTmpScript()
@@ -404,5 +415,5 @@ private fun resolvePreambles(rawScript: File, enableSupportApi: Boolean): File {
404415
scriptFile = Script(scriptFile).prependWith("//INCLUDE ${preambleFile.absolutePath}").createTmpScript()
405416
}
406417

407-
return scriptFile
418+
return scriptFile.toURI()
408419
}

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

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,27 @@ 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(file: File): IncludeResult {
19+
fun resolveIncludes(uri: URI): 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+
}
30+
}
31+
2032
val includes = mutableListOf<URI>()
21-
//TODO: in case initial file is a remote file, we shouldn't allow local references, so initial 'true' here is a wrong assumption
22-
//TODO: first resolve redirects: https://stackoverflow.com/questions/2659000/java-how-to-find-the-redirected-url-of-a-url
23-
val lines = resolve(true, file.toURI(), includes)
24-
val script = Script(lines, file.extension)
33+
val lines = resolve(isFile(uri), uri, includes)
34+
val script = Script(lines, urlExtension)
2535

2636
return IncludeResult(script.consolidateStructure().createTmpScript(), includes.map { it.toURL() })
2737
}
2838

29-
private fun resolve(allowLocalReferences: Boolean, scriptUri: URI, includes: MutableList<URI>): List<String> {
39+
private fun resolve(allowFileReferences: Boolean, scriptUri: URI, includes: MutableList<URI>): List<String> {
3040
val lines = readLines(scriptUri)
3141
val scriptPath = scriptUri.resolve(".")
3242
val result = mutableListOf<String>()
@@ -36,7 +46,7 @@ private fun resolve(allowLocalReferences: Boolean, scriptUri: URI, includes: Mut
3646
val include = extractTarget(line)
3747
val includeUri = resolveUri(scriptPath, include)
3848

39-
if (!allowLocalReferences && isLocal(includeUri)) {
49+
if (!allowFileReferences && isFile(includeUri)) {
4050
errorMsg("References to local filesystem from remote scripts are not allowed.\nIn script: $scriptUri; Reference: $includeUri")
4151
quit(1)
4252
}
@@ -49,7 +59,7 @@ private fun resolve(allowLocalReferences: Boolean, scriptUri: URI, includes: Mut
4959

5060
includes.add(includeUri)
5161

52-
val resolvedLines = resolve(allowLocalReferences && isLocal(includeUri), includeUri, includes)
62+
val resolvedLines = resolve(allowFileReferences && isFile(includeUri), includeUri, includes)
5363
result.addAll(resolvedLines)
5464
continue
5565
}
@@ -62,7 +72,7 @@ private fun resolve(allowLocalReferences: Boolean, scriptUri: URI, includes: Mut
6272

6373
private fun readLines(uri: URI): List<String> {
6474
try {
65-
return uri.toURL().readText().lines()
75+
return fetchFromURI(uri)
6676
} catch (e: Exception) {
6777
errorMsg("Failed to resolve include with URI: '${uri}'")
6878
System.err.println(e.message?.lines()!!.map { it.prependIndent("[kscript] [ERROR] ") })
@@ -80,8 +90,6 @@ private fun resolveUri(scriptPath: URI, include: String): URI {
8090
return result.normalize()
8191
}
8292

83-
internal fun isLocal(uri: URI) = uri.scheme.startsWith("file")
84-
8593
internal fun isIncludeDirective(line: String) = line.startsWith("//INCLUDE") || line.startsWith(INCLUDE_ANNOT_PREFIX)
8694

8795
internal fun extractTarget(incDirective: String) = when {
@@ -108,6 +116,6 @@ private const val INCLUDE_ANNOT_PREFIX = "@file:Include("
108116
object ResolveIncludes {
109117
@JvmStatic
110118
fun main(args: Array<String>) {
111-
System.err.println(resolveIncludes(File(args[0])).scriptFile.readText())
119+
System.err.println(resolveIncludes(File(args[0]).toURI()).scriptFile.readText())
112120
}
113121
}

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: 4 additions & 4 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
}
@@ -207,22 +207,22 @@ class Tests {
207207
val file = File("test/resources/includes/include_variations.kts")
208208
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

219219
result.includes.filter { it.protocol == "file" }.map { File(it.toURI()).name } shouldBe List(7) { "include_${it + 1}.kt" }
220220
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",

test/test_suite.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ echo
4141
echo "Starting $SUITE test suite... Compiling... Please wait..."
4242

4343
# exit code of `true` is expected to be 0 (see https://github.com/lehmannro/assert.sh)
44-
cd "$PROJECT_DIR" || exit
44+
cd "$PROJECT_DIR"
4545
assert_raises "./gradlew build"
46-
cd - || exit
46+
cd -
4747

4848
assert_end "$SUITE"
4949

0 commit comments

Comments
 (0)