Repository navigation
Feature Suggestion: Self-bootstrapping scripts #195
Description
I was thinking if there was a way to make the scripts I write more standalone in a sense that I could share it with someone without either having to explain how to install kotlin&kscript or producing a binary they can't read/edit. Much like you can do today with shell/python scripts.
I believe I have found a fairly practical way to achieve this: by wrapping an existing script with a specific header and footer it can be made into a valid shell script and kotlin script at the same time. No unpacking step needed! See example below.
That means original script can be rewritten once, replacing it, and further iteration can happen with the bootsrapping footer in place.
IntelliJ support is available for both kotlin and bash parts of the script with kscript --idea script.kts thanks to the intellij's language injection feature.
Having the modifying happen only once also has the benefit that the shell code despite being auto-generated is ok to modify, as it won't be overwritten. See example below where I choose to propagate the script file name to kotlin.
So I was thinking, that it would be great to have this built into kscript on an "opt-in" basis, e.g.
kscript --bootstrapify script.kts
^ prepends/appends things to script.kts making it shareable
P.S.: Happy to work on a PR if this seems ok to add
Example of self-bootstrapping script.kts: (try opening with kscript --idea to see how IDE experience is uncompromised; installing "BashSupport" pluging may be required for bash support)
#!/bin/bash
//usr/bin/env cat << '__EOF_KOTLIN' >/dev/null
import org.intellij.lang.annotations.Language
println("Hello from kotlin!")
if (System.console() == null) {
print(generateSequence(::readLine).joinToString("\n") + " | ")
}
println("${System.getenv("KSCRIPT_FILE")} ${args.toList().joinToString(" ")}")
//DEPS org.jetbrains:annotations-java5:16.0.2
@Language("sh")
private val __SHELL_BOOTSTRAP = """
__EOF_KOTLIN
function in_path() { command -v "$1" >/dev/null 2>&1; }
in_path kscript || {
function echo_and_eval() { echo "$ $@" 1>&2; eval "$@" 1>&2; }
in_path sdk || {
curl "https://get.sdkman.io" | bash 1>&2 && \
echo_and_eval source "$"SDKMAN_DIR/bin/sdkman-init.sh
}
in_path kotlin || echo_and_eval sdk install kotlin
in_path gradle || echo_and_eval sdk install gradle
echo_and_eval sdk install kscript
}
export KSCRIPT_FILE="$0";
kscript $0 "$@";
exit $?
"""
Activity
holgerbrandl commentedon Nov 28, 2018
That's a very clever way of doing it. :-)
Just works for python, because Linux OS vendors often choose to bundle the python runtime because it is popular. Fingers crossed that kotlin will be honored in the same way some day. :-)
Just in case you missed it: There is
--packagealready, which allows to share scripts in a similar way. After--packageyou do not even need gradle or kotlin anymore. Clearly being able to edit the shared solution like in your suggestion is a big plus.Although the rewritten scripts are legit kscripts, the kscripty nature is feels a bit hidden to me when looking at your example. That is it took me almost a minute to figure the used interpreter.
On a technical level, I think your suggestion does not do process substitution, whereas
kscriptdoes so. But maybe by doingexec kscript $0 $*at the we could fix this.I also wonder about stdin handling. Can we still pipe into these rewritten script? Like in
cat foo.txt | kscript bar.kts >> result.txtAnyway, the suggestion is great and I think we should add it.
For sake of conciseness I'd think that we may want to leave out
since it should work without.
Also, the bootstrapping block feels a bit long and is not really interesting to the user, so we could potentially try to shorten it to something like:
So essentially we could/would move the bootstrap part to a script in the repo. What do you think? Maybe we could even inline
in_pathIt would be great if you provide a PR including
--bootstrapifying twice creates a corrupted scriptkscript script.ktsand./script.ktsare supported (which seems to be the case already) and that stream redirection still works with bootstrappified scriptsI know this is a lot, but I'm also happy to help if needed.
I'm not sure about Windows support. A strappified script would not longer run under windows shell (see #194) whereas a old-school kscripts will after #194 has beed merged. But since
kscriptoriginal and main intention is to support bash, I think we can live with this limitation.Final question, could there be a more descriptive name for the process and the option instead of
--bootstrapify? I have no sound idea yet, but bootstrapify may be to obscure for some/most users.EugeneSusla commentedon Nov 29, 2018
Yep, knew about it. Btw, just tried it and got an error, will post separate bug with output.
Yes! Works with the original example too (you'll see it echoed back), just need to save it to a file first
I have a previous iteration of this that happens to trade language injection for having all shell in header (no footer). Sounds like it may be better then - see example below
Great idea!
Hmm, will it not? I thought windows will just look at the extension(and ignore shebang) and send it directly to kscript, which will just treat is as valid kotlin script.
Looking forward to #194 to actually try it out!
Awesome! I'll get started on a PR with these then! (thanks for enumerating the corner cases/potential regressions by the way!)
--embed-bootstrapperhaps? I was struggling with the name too as you can tell :)One caveat worth mentioning with any approach that doesn't add any imports, is that intellij will try to put first import in-between shebang and second line, breaking the validity of shell script. Easiest workaround I could think of is to add dummy
import kotlin.Unitas part of the header - if there's already at least one import, IntelliJ will place new imports next to it instead.I guess in a refined version I could inspect if script already has imports and only add dummy one if it doesn't
Example 2: (note that intellij allows folding bootstrap lines, which looks nicer. I wish there was a way to programmatically trigger that particular fold...)
holgerbrandl commentedon Nov 29, 2018
afaik there's neither support for shebang nor bash in the windows shell. So, no it won't.
Indeed, but it's imho a minor limitation which we could just document in the boostrap section (see my revised version below).
IJ will optimize it away quickly, so I don't think that this would help.
There's no need for
echo $?in the header because exec substitutes the process. Just try the example with smthg likecat text.txt | ./script2.kts -followed byecho $?I also ditched
KSCRIPT_FILEbecause we do no support it elsewhere inkscript. If at all it could/should be a different ticket/PR.or maybe
--add-boostrap-header? Should it write the modified script to stdout or do an inplace file replacement (similar tosed -i)? The former approach would be easier to grasp, but the latter more convenient I guess.IMHO there is no need to save stdin to a file first, just checkout my example from above. Which is good, because this allows to stream data also through a boostrappified kscript.
You can fold block comments in general when using IDEA but I guess that's not intended in most situations.
EugeneSusla commentedon Nov 30, 2018
It's nice to see it that short!
Will use the latest one as the template when implementing then
I was thinking it should maybe say the opposite (or just don't say either way), as we'll never overwrite custom changes there. There are some things that can be only done in bash, capturing script name being one example.
I think in-place replacement is the only right way - we should encourage people to not duplicate their script to a new file as much as we can. Without clear guidance I can imagine thinking this is basically just another flavor of
--packageand redirecting output to a new file is expected of me.Seems pretty clear, if a bit long. I'll go with it then
yeah, meant to say the script itself, not stdin :) we're on the same page here
Expose script file name to script itself
Expose script file name to script itself
Expose script file name to script (#200)
EugeneSusla commentedon Dec 6, 2018
Closing as the implementation is now merged