Skip to content

Commit

Permalink
added support for script arguments (fixes #18)
Browse files Browse the repository at this point in the history
  • Loading branch information
holgerbrandl committed May 2, 2017
1 parent 0ccce39 commit 95ffef0
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 39 deletions.
54 changes: 40 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@

Enhanced scripting support for [Kotlin](https://kotlinlang.org/) on *nix-based systems.

Kotlin has a limited support for scripting already but it's not (yet) feature-rich enough to be fun.
Kotlin has some built-in support for scripting already but it is not yet feature-rich enough to be a viable alternative in the shell.

In particular this wrapper around `kotlinc-jvm -script` adds
* Compiled script caching (using md5 checksums)
* Automatic dependency resolution via gradle-style resource locators
* Dependency declarations using gradle-style resource locators and automatic dependency resolution with maven
* More options to provide scripts including interpreter mode, reading from stdin, local files or URLs
* Embedded configuration for Kotlin runtime options
* Support library to ease the writing of Kotlin scriptlets

Taken all these features together, `kscript` provides an easy-to-use, very flexible, and almost zero-overhead solution to write self-contained mini-applications with Kotlin.


## Installation

Expand All @@ -27,9 +33,9 @@ curl -L -o ~/bin/kscript https://git.io/vaoNi && chmod u+x ~/bin/kscript



## Usage
## Interpreter Usage

To use `kscript` just specify it in the shebang line of your Kotlin scripts:
To use `kscript` for a script just point to it in the shebang line of your Kotlin scripts:

```kotlin
#!/usr/bin/env kscript
Expand Down Expand Up @@ -64,32 +70,38 @@ val doArgs = Docopt(usage).parse(args.toList())
println("Hello from Kotlin!")
println("Parsed script arguments are: \n" + doArgs.joinToString())
```

`kscript` will read dependencies from all lines in a script that start with `//DEPS` (if any). Multiple dependencies can be split by comma, space or semicolon.

Note: It might feel more intuitive to provide dependencies as an argument to `kscript`, however because of the way the shebang line works on Linux this is not possible.

Inlined Usage
=============

To use `kscript` in a workflow without creating an additional script file, you can also use one its supported modes for _inlined useage_. The following modes are supported:

Inline Usage
============
* Directly provide a Kotlin scriptlet as argument
```{bash}
kscript 'println("hello world")'
```

You can even inline `kscript` solutions into larger scripts, because `kscript` can read from stdin as well. So, depending on your preference you could simply pipe a kotlin snippet into `kscript`

* Pipe a Kotlin snippet into `kscript` and instruct it to read from `stdin` by using `-` as script argument

```{bash}
echo '
println("hello kotlin")
println("Hello Kotlin.")
' | kscript -
```


or do the same using `heredoc` (preferred solution) which gives you some more flexibility to also use single quotes in your script:
* Using `heredoc` (preferred solution for inlining) which gives you some more flexibility to also use single quotes in your script:
```{bash}
kscript - <<"EOF"
println("hello kotlin and heredoc")
println("It's a beautiful day!")
EOF
```

Since the piped content is considered as a regular script it can also have dependencies
* Since the piped content is considered as a regular script it can also have dependencies
```{bash}
kscript - <<"EOF"
//DEPS com.offbytwo:docopt:0.6.0.20150202 log4j:log4j:1.2.14
Expand All @@ -101,14 +113,28 @@ println("hello again")
EOF
```

Finally (for sake of completeness) it also works with process substitution and for sure you can always provide additional arguments (exposed as `args : Array<String>` within the script)
* Finally (for sake of completeness), it also works with process substitution and for sure you can always provide additional arguments (exposed as `args : Array<String>` within the script)
```{bash}
kscript - arg1 arg2 arg3 <(echo 'println("k-onliner")')
kscript <(echo 'println("k-onliner")') arg1 arg2 arg3
```

Inlined _kscripts_ are also cached based on `md5` checksum, so running the same snippet again will use a cached jar (sitting in `$TMPDIR`).


Support API
===========


`kscript` is complemented by a support library to ease the writing of Kotlin scriptlets. The latter includes solutions to common use-cases like argument parsing, data streaming, IO utilities, and various iterators to streamline the development of kscript applications.

When using the direct script arguments the methods in the the `kscript.*` namespace are automatically imported by convention. This allows sed.for constructs like

```bash
cat some_big_file | kscript 'stdin.filter { "^de0[-0]*".toRegex().matches(it) }.forEach{it::println}'
```



Tool repositories
=================

Expand Down
2 changes: 1 addition & 1 deletion expandcp.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Inspired by mvncp created by Andrew O'Malley


// Use cached classpath from previous run if present
val DEP_LOOKUP_CACHE_FILE = File("/tmp/.kscript_deps_cache.txt")
val DEP_LOOKUP_CACHE_FILE = File("/tmp/kscript_deps_cache.txt")


if (args.size == 1 && args[0] == "--clear-cache") {
Expand Down
65 changes: 45 additions & 20 deletions kscript
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ assertInPath(){ if [ -z "$(which $1)" ]; then echo "[ERROR] Could not locate '$1

assertInPath kotlinc
assertInPath mvn
assertInPath curl

## prefer JAVA_HOME over PATH for javac and jar (to stay in sync with kotlinc; see #6)
if [ -n "$JAVA_HOME" -a -x "$JAVA_HOME/bin/java" ]; then
Expand Down Expand Up @@ -46,7 +47,7 @@ fi
## optionally clear up the jar cache
if [ "$1" == "--clear-cache" ]; then
echo "Cleaning up cache..."
rm -f ${TMPDIR=/tmp}/.kscript_*
rm -f ${TMPDIR=/tmp}/kscript_*
exit 0
fi

Expand Down Expand Up @@ -78,44 +79,65 @@ shift
if [ "${OSTYPE//[0-9.]/}" == "darwin" ]; then md5sum(){ md5 -r $1; }; fi


## If script is provided from stdin create a temporary file
## note we cannot support empty args list here because this would confuse with actual script args
if [ "$scriptFile" == "-" ]; then scriptFile="/dev/stdin"; fi

#if [ "$scriptFile"=="/dev/stdin" ]; then

## Support URLs as script files
## http://stackoverflow.com/questions/2172352/in-bash-how-can-i-check-if-a-string-begins-with-some-value
if [[ "$scriptFile" == "http://"* ]] || [[ "$scriptFile" == "https://"* ]]; then
urlHash=$(echo "$scriptFile" | md5sum)

# http://unix.stackexchange.com/questions/174817/finding-the-correct-tmp-dir-on-multiple-platforms
tmpScript=${TMPDIR=/tmp}/.kscript_urlkts_cache_${urlHash}.kts
urlCache=${TMPDIR=/tmp}/kscript_urlkts_cache_${urlHash}.kts

if [ ! -f "$tmpScript" ]; then
if [ ! -f "$urlCache" ]; then
# echo "fetching kscript from url $scriptFile into ${tmpScript}..."
curl -L ${scriptFile} 2>/dev/null > ${tmpScript}
curl -L ${scriptFile} 2>/dev/null > ${urlCache}
fi

scriptFile=${tmpScript}
scriptFile=${urlCache}
fi

## Rather Test if script ends with kts to also support process substitution here. Wrap stdin



## remap - to the standard input file-descriptor
if [ "$scriptFile" == "-" ]; then scriptFile="/dev/stdin"; fi


## if scriptFile is still not a file we assume the input to be a kotlin program directly
## note: -e also supports non-regular files in contrast to -f
if [[ ! -e ${scriptFile} ]] && [[ "$scriptFile" != *kts ]]; then
# echo "processing direct script argument"
urlHash=$(echo "$scriptFile" | md5sum)

# http://unix.stackexchange.com/questions/174817/finding-the-correct-tmp-dir-on-multiple-platforms
urlCache=${TMPDIR=/tmp}/kscript_scriptarg_cache_${urlHash}.kts

echo "$scriptFile" > $urlCache

scriptFile=${urlCache}
fi


## Support for support process substitution and stdin.
# http://serverfault.com/questions/52034/what-is-the-difference-between-double-and-single-square-brackets-in-bash
# https://viewsby.wordpress.com/2013/09/06/bash-string-ends-with/

if [[ "$scriptFile" != *kts ]]; then
tmpScript=${TMPDIR=/tmp}/.kscript_stdin_${RANDOM}${RANDOM}.kts # odd but works on macos as well
cat "${scriptFile}" > ${tmpScript}
urlCache=${TMPDIR=/tmp}/kscript_stdinbuffer_${RANDOM}${RANDOM}.kts # odd but works on macos as well
cat "${scriptFile}" > ${urlCache}

## rename to use checksum as name to allow for jar-caching also when using stdin
stdinMD5=$(md5sum ${tmpScript} | cut -c1-6)
stdinMD5=$(md5sum ${urlCache} | cut -c1-6)

## replace script file with md5 hash file copy of stdin
scriptFile=$(dirname ${tmpScript})/kscript_stdin_${stdinMD5}.kts
mv ${tmpScript} $scriptFile
scriptFile=$(dirname ${urlCache})/kscript_stdin_${stdinMD5}.kts
mv ${urlCache} $scriptFile
fi

## just proceed if the script file is a regular file at this point
if [[ ! -f $scriptFile ]]; then
echo "[ERROR] Could not open script file '$scriptFile'" 1>&2
exit 1
fi


### auto-install expandcp.kts into same dir as kscript for automatic dependency resolution if not yet in PATH
Expand All @@ -125,6 +147,7 @@ if ! which expandcp.kts &> /dev/null; then
chmod u+x ${installDir}/expandcp.kts
fi


## Extract dependencies from script
#scriptFile=/Users/brandl/projects/kotlin/kscript/test/resources/multi_line_deps.kts
if [ $(grep "^// DEPS" ${scriptFile} | wc -l) -gt 0 ]; then
Expand All @@ -135,9 +158,10 @@ dependencies=$(grep "^//DEPS" ${scriptFile} | cut -f2- -d' ' | trim | tr ',;\n'


## First try dependency cache directly to avoid jvm launch for expandcp.kts
dependency_cache="/tmp/.kscript_deps_cache.txt"
# fix me more consistent use of ${TMPDIR}
dependency_cache="/tmp/kscript_deps_cache.txt"
if [ -n "$dependencies" ] && [ -f "$dependency_cache" ]; then
classpath=$(grep -F $(echo ${dependencies} | tr ' ' ';')" " /tmp/.kscript_deps_cache.txt | cut -d' ' -f2)
classpath=$(grep -F $(echo ${dependencies} | tr ' ' ';')" " /tmp/kscript_deps_cache.txt | cut -d' ' -f2)
fi

## If there are dependencies but cache-lookup failed we run expandcp.kts
Expand Down Expand Up @@ -178,6 +202,7 @@ className=$(basename ${scriptFile} .kts)
className=$(echo ${className:0:1} | tr '[a-z]' '[A-Z]')${className:1}


#ls -la ${jarFile}
# build cache-jar if it does not yet exist
if [ ! -f "${jarFile}" ]; then
## remove previous (now outdated) cache jars
Expand All @@ -191,7 +216,7 @@ if [ ! -f "${jarFile}" ]; then
fi


# fixme xxxx placeholder does not work on macos
# note xxxx placeholder does not work on macos but mktemp creates unique path alreade
mainJava=$(mktemp -dt kscript.XXXXXX)/Main_${className}.java

echo '
Expand Down
8 changes: 7 additions & 1 deletion notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,10 @@ echo kotlinc -script -classpath "$classpath" "$@"

## how does process substitution work?

http://unix.stackexchange.com/questions/156084/why-does-process-substitution-result-in-a-file-called-dev-fd-63-which-is-a-pipe
http://unix.stackexchange.com/questions/156084/why-does-process-substitution-result-in-a-file-called-dev-fd-63-which-is-a-pipe

When you do `<(some_command)`, your shell executes the command inside the parentheses and replaces the whole thing with a file descriptor, that is connected to the command's stdout. So `/dev/fd/63` is a pipe containing the output of your ls call.



https://unix.stackexchange.com/questions/153896/bash-process-substitution-and-stdin
1 change: 1 addition & 0 deletions test/resources/cmd_subst_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kscript <(echo 'println("command substitution works as well")') arg u ments
1 change: 1 addition & 0 deletions test/resources/direct_script_arg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kscript 'println("kotlin rocks")'
14 changes: 11 additions & 3 deletions test/test_suite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export DEBUG="--verbose"


## make sure that scripts can be piped into kscript
assert 'kscript "println(\\'kotlin rocks\\')"' "kotlin rocks"
assert "source ${KSCRIPT_HOME}/test/resources/direct_script_arg.sh" "kotlin rocks"


## provide script via stidin
Expand All @@ -15,14 +15,21 @@ assert "echo 'println(1+1)' | kscript -" "2"
## make sure that heredoc is accepted as argument
assert "source ${KSCRIPT_HOME}/test/resources/here_doc_test.sh" "hello kotlin"

## make sure that command substitution works as expected
assert "source ${KSCRIPT_HOME}/test/resources/cmd_subst_test.sh" "command substitution works as well"

## make sure that it runs with local script files
assert "source ${KSCRIPT_HOME}/test/resources/local_script_file.sh" "kscript rocks!"

## make sure that it runs with local script files
assert "kscript ${KSCRIPT_HOME}/test/resources/multi_line_deps.kts" "kscript is cool!"

## missing script
assert_raises "kscript i_do_not_exist.kts" 1
assert "kscript i_do_not_exist.kts 2>&1" "[ERROR] Could not open script file 'i_do_not_exist.kts'"

## make sure that it runs with remote URLs
assert "kscript https://github.com/holgerbrandl/kscript/blob/master/test/resources/url_test.kts.kt" "I came from the internet"
assert "kscript https://raw.githubusercontent.com/holgerbrandl/kscript/master/test/resources/url_test.kts" "I came from the internet"


assert_end script_input_modes
Expand All @@ -41,6 +48,8 @@ assert_end cli_helper_tests
## make sure that KOTLIN_HOME can be guessed from kotlinc correctly
assert "unset KOTLIN_HOME; echo 'println(99)' | kscript -" "99"

## todo test what happens if kotlin/kotlinc/java/maven is not in PATH


assert_end environment_tests

Expand All @@ -65,5 +74,4 @@ assert_raises "expandcp.kts org.docopt:docopt:0.9.0-SNAPSHOT log4j:log4j:1.2.14"

assert_end dependency_lookup

## todo test what happens if kotlin/kotlinc/java/maven is not in PATH

0 comments on commit 95ffef0

Please sign in to comment.