From a4645d242fbd6ac8e0b0ab35d41864bd5b8ec7dc Mon Sep 17 00:00:00 2001 From: Eugene Susla Date: Mon, 3 Dec 2018 18:44:20 -0800 Subject: [PATCH] Implement --add-bootstrap-header See the added section in README.md for details Test Plan: - test/test_suite.sh - rm -rf ~/.sdkman && echo 'println("Hello world")' > /tmp/s && kscript --add-bootstrap-header /tmp/s && /tmp/s --- README.md | 17 +++++++++++ src/main/kotlin/kscript/app/Kscript.kt | 41 ++++++++++++++++++++++++++ test/resources/echo_stdin_args.kts | 6 ++++ test/test_suite.sh | 27 +++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100755 test/resources/echo_stdin_args.kts diff --git a/README.md b/README.md index 3f032088..f024e64a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Taken all these features together, `kscript` provides an easy-to-use, very flexi - [Treat yourself a REPL with `--interactive`](#treat-yourself-a-repl-with---interactive) - [Boostrap IDEA from a `kscript`let](#boostrap-idea-from-a-kscriptlet) - [Deploy scripts as standalone binaries](#deploy-scripts-as-standalone-binaries) +- [Embed kscript installer within a script](#embed-kscript-installer-within-a-script) - [FAQ](#faq) - [Support](#support) - [How to contribute?](#how-to-contribute) @@ -427,6 +428,22 @@ The created binary will contain a compiled copy of the script, as well as all de Just `java` is required to run these binaries. + +Embed kscript installer within your script +-------------------------------------- + + To make a script automatically [install kscript](#installation) and its dependencies on first run if necessary, run: + + ```bash +kscript --add-bootstrap-header some_script.kts +``` + +Now `some_script.kts` can be shared and run directly on any other machine that has `bash`, without having to go through the [Installation](#installation) steps first. + +Note that unlike the [`--package` option](#deploy-scripts-as-standalone-binaries) this doesn't produce a separate file, allowing the distributed script to be read and modified(including with [`kscript --idea`](#boostrap-idea-from-a-kscriptlet)) similar to what you might expect with bash/python/ruby scripts. +On the other hand this doesn't embed dependencies within the script("fat jar"), so internet connection may be required on its first run. + + FAQ --- diff --git a/src/main/kotlin/kscript/app/Kscript.kt b/src/main/kotlin/kscript/app/Kscript.kt index e1d67900..b6542255 100644 --- a/src/main/kotlin/kscript/app/Kscript.kt +++ b/src/main/kotlin/kscript/app/Kscript.kt @@ -2,6 +2,7 @@ package kscript.app import kscript.app.ShellUtils.requireInPath import org.docopt.DocOptWrapper +import org.intellij.lang.annotations.Language import java.io.BufferedReader import java.io.File import java.io.FileInputStream @@ -43,6 +44,7 @@ Options: --idea Open script in temporary Intellij session -s --silent Suppress status logging to stderr --package Package script and dependencies into self-dependent binary + --add-bootstrap-header Prepend bash header that installs kscript if necessary Copyright : 2017 Holger Brandl @@ -59,6 +61,18 @@ val KSCRIPT_CACHE_DIR = System.getenv("KSCRIPT_CACHE_DIR")?.let { File(it) } // use lazy here prevent empty dirs for regular scripts https://github.com/holgerbrandl/kscript/issues/130 val SCRIPT_TEMP_DIR by lazy { createTempDir() } +@Language("sh") +private val BOOTSTRAP_HEADER = """ + #!/bin/bash + + //usr/bin/env echo ' + /**** BOOTSTRAP kscript ****\'>/dev/null + command -v kscript >/dev/null 2>&1 || curl -L "https://git.io/fpVLG" | bash 1>&2 + exec kscript $0 "$@" + \*** IMPORTANT: Any code including imports and annotations must come after this line ***/ + + """.trimIndent().lines() + fun main(args: Array) { // skip org.docopt for version and help to allow for lazy version-check @@ -124,6 +138,33 @@ fun main(args: Array) { val enableSupportApi = docopt.getBoolean("text") val (rawScript, includeContext) = prepareScript(scriptResource) + if (docopt.getBoolean("add-bootstrap-header")) { + errorIf(!rawScript.canWrite()) { + "Script file not writable: $rawScript" + } + errorIf(rawScript.parentFile == SCRIPT_TEMP_DIR) { + "Temporary script file detected: $rawScript, created from $scriptResource" + } + val scriptLines = rawScript.readLines().dropWhile { + it.startsWith("#!/") && it != "#!/bin/bash" + } + + errorIf(scriptLines.getOrNull(0) == BOOTSTRAP_HEADER[0] + && scriptLines.any { "command -v kscript >/dev/null 2>&1 || " in it }) { + val lastHeaderLine = BOOTSTRAP_HEADER.findLast { it.isNotBlank() }!! + val preexistingHeader = + scriptLines.dropLastWhile { it != lastHeaderLine }.joinToString("\n") + "Bootstrap header already detected:\n\n$preexistingHeader\n\n" + + "You can remove it to force the re-generation" + } + + rawScript.writeText((BOOTSTRAP_HEADER + scriptLines).joinToString("\n")) + + if (loggingEnabled) { + info("$rawScript updated") + } + quit(0) + } // post process script (text-processing mode, custom dsl preamble, resolve includes) // and finally resolve all includes (see https://github.com/holgerbrandl/kscript/issues/34) diff --git a/test/resources/echo_stdin_args.kts b/test/resources/echo_stdin_args.kts new file mode 100755 index 00000000..c8a01360 --- /dev/null +++ b/test/resources/echo_stdin_args.kts @@ -0,0 +1,6 @@ +#!/usr/bin/env kscript + +if (System.console() == null) { + print(generateSequence(::readLine).joinToString("\n") + " | ") +} +println("script ${args.toList().joinToString(" ")}") \ No newline at end of file diff --git a/test/test_suite.sh b/test/test_suite.sh index c72a4d2b..1e8657f4 100755 --- a/test/test_suite.sh +++ b/test/test_suite.sh @@ -278,3 +278,30 @@ assert_end misc assert_raises "./gradlew test" assert_end junit_tests + + +######################################################################################################################## +## bootstrap header + +f=/tmp/echo_stdin_args.kts +cp ${KSCRIPT_HOME}/test/resources/echo_stdin_args.kts $f + +# ensure script works as is +assert 'echo stdin | '$f' --foo bar' "stdin | script --foo bar" + +# add bootstrap header +assert 'kscript --add-bootstrap-header '$f '' + +# ensure adding it again raises an error +assert_raises 'kscript --add-bootstrap-header '$f 1 + +# ensure scripts works with header, including stdin +assert 'echo stdin | '$f' --foo bar' "stdin | script --foo bar" + +# ensure scripts works with header invoked with explicit `kscript` +assert 'echo stdin | kscript '$f' --foo bar' "stdin | script --foo bar" + + +rm $f + +assert_end bootstrap_header