Kotlin as scripting language

D4be3ad792b57408b3ab6fe98caef08e?s=47 danybony
November 23, 2019

Kotlin as scripting language

Kotlin is gaining more and more popularity, especially after it graduated as official programming language to develop Android Applications. It can be used, however, to write elegant and highly maintainable scripts too and in this this talk I show how.

Presented at Devfest St Petersburg 2019

D4be3ad792b57408b3ab6fe98caef08e?s=128

danybony

November 23, 2019
Tweet

Transcript

  1. Kotlin as scripting language Daniele Bonaldo

  2. None
  3. None
  4. Total pictures: 1536 Covered by 17-40: 839 (54%) Covered by

    24-105: 1413 (91%) Covered by 24-70: 1094 (71%)
  5. Bash Java Python Kotlin

  6. fun main(args: Array<String>) { println("Hello World!") } $ kotlinc hello.kt

    -include-runtime -d hello.jar $ java -jar hello.jar two params Hello World!
  7. println("Hello world!") $ kotlinc -script helloScript.kts Hello world!

  8. println("Called with args:") args.forEach { println("- $it") } $ kotlinc

    -script helloScript.kts hello ‘with spaces’ Called with args: - hello - with spaces
  9. import java.io.File val folders: Array<File>? = File(".").listFiles { file ->

    file.isDirectory } folders?.forEach { folder -> println(folder) } $ kotlinc -script dirsExplore.kts ./dir2 ./dir1
  10. import java.io.File fun printCurrentAndSubdirs(currentDir: File) { println(currentDir.path) currentDir.listFiles { file

    -> file.isDirectory }?.forEach { printCurrentAndSubdirs(it) } } printCurrentAndSubdirs(File(".")) $ kotlinc -script dirsExploreRecursion.kts . ./dir2 ./dir2/dir2.1 ./dir2/dir2.2 ./dir1
  11. import java.io.File fun File.printPathAndSubdirs() { println(path) listFiles { file ->

    file.isDirectory }?.forEach { it.printPathAndSubdirs() } } File(".").printPathAndSubdirs() $ kotlinc -script dirsExploreRecursionExtension.kts . ./dir2 ./dir2/dir2.1 ./dir2/dir2.2 ./dir1
  12. kscript

  13. kscript https://github.com/holgerbrandl/kscript

  14. kscript - install $ brew install holgerbrandl/tap/kscript

  15. println("Hello world!") $ kotlinc -script helloScript.kts Hello world!

  16. println("Hello world!") $ kscript helloScript.kts Hello world!

  17. kscript - input modes $ kscript helloScript.kts $ kscript https://gist.githubusercontent.com/danybony/.../kscriptUrl.kts

    $ kscript ‘println(“Hello there”)’ $ echo ‘ println("Hello Kotlin.") ' | kscript -
  18. #!/usr/bin/env kscript println(“Hello!") for (arg in args) { println("arg: $arg")

    } $ chmod +x interpreter.kts $ ./interpreter.kts world Hello! arg: world
  19. kscript - cache $ time kotlinc -script helloScript.kts $ time

    kscript helloScript.kts 3.140s 3.112s 3.193s 3.087s 3.064s … 3.896s 0.604s 0.607s 0.620s 0.615s … Scripts cache: ~/.kscript/scriptname_hashcode.kts
  20. kscript - dependencies //DEPS com.drewnoakes:metadata-extractor:2.12.0 import com.drew.imaging.ImageMetadataReader import com.drew.metadata.Metadata import

    java.io.File val metadata: Metadata = ImageMetadataReader.readMetadata(File("image.jpg"))
  21. kscript - dependencies @file:MavenRepository("central", "https://repo.maven.apache.org/maven2/") @file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2") @file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.2") import kotlinx.coroutines.* println("Start")

    // Start a coroutine GlobalScope.launch { delay(1000) println("Stop") } println("Hello") Thread.sleep(2000) // wait for 2 seconds
  22. kscript - includes fun String.upperCase() { ... } //INCLUDE utils.kt

    @file:Include("util.kt") val shouted = "hello!".upperCase() script.kts utils.kts
  23. kscript - help $ kscript --help Usage: kscript [options] <script>

    [<script_args>]... kscript --clear-cache The <script> can be a script file (*kts), a script URL, - for stdin, a *.kt source file with a main method, or some kotlin code. Use '--clear-cache' to wipe cached script jars and urls Options: -i --interactive Create interactive shell with dependencies as declared in script -t --text Enable stdin support API for more streamlined text processing --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
  24. Kscript - edit in IntelliJ IDEA $ kscript --idea photoStats.kts

  25. Kscript - edit in IntelliJ IDEA

  26. Kscript - edit in IntelliJ IDEA

  27. kscript - deploy as binaries $ kscript --package helloScript.kts [kscript]

    Packaging script 'helloScript' into standalone executable... $ ./helloScript Hello world!
  28. photoStats

  29. photoStats #!/usr/bin/env kscript @file:DependsOn("com.drewnoakes:metadata-extractor:2.12.0") import PhotoStats.LensRange import com.drew.imaging.ImageMetadataReader import com.drew.metadata.Metadata

    import java.io.File typealias LensRange = Pair<Int, Int> data class Lens( val name: String, val range: LensRange ) val lenses = listOf(LensRange(17, 40), LensRange(24, 105), LensRange(24, 70), LensRange(70, 200)) fun appendFiles(dir: File, filesList: MutableList<File>) { filesList.addAll( dir.listFiles() .filter { !it.isDirectory && it.isSupported() } ) dir.listFiles() .filter { it.isDirectory && it.listFiles().isNotEmpty()} .forEach { appendFiles(it, filesList) } } fun File.focalLength(): Int? { val metadata: Metadata = ImageMetadataReader.readMetadata(this) return metadata.directories .filter { it.name.contains("exif", true) } .firstOrNull { it.tags.find { tag -> tag.tagName == "Focal Length 35" || tag.tagName == "Focal Length" } != null } ?.let { directory -> (directory.tags.firstOrNull { it.tagName == "Focal Length 35" } ?: directory.tags.first { it.tagName == "Focal Length" }) .let { it.description.substringBefore(" ").toInt() } } } val supportedExtensions = listOf("jpg", "jpeg", "cr2", "dng") fun File.isSupported(): Boolean = supportedExtensions.find { it.equals(this.extension, true) } != null val allFiles = ArrayList<File>() val paths = if (args.isNotEmpty()) args.toList() else listOf("./") paths.forEach { val currentDir = File(it) if (currentDir.exists() && currentDir.isDirectory) { println("Scanning ${currentDir.absolutePath}") appendFiles(currentDir, allFiles) } } val focals = allFiles.map { it.focalLength() } .filterNotNull() .sorted() val total = focals.size println("Total pictures: $total") lenses.forEach { lensRange -> val matching = focals.filter { it >= lensRange.first && it <= lensRange.second }.size println("Covered by ${lensRange.first}-${lensRange.second}: $matching (${(matching.toFloat() / total.toFloat() * 100).toInt()}%)") }
  30. photoStats #!/usr/bin/env kscript @file:DependsOn("com.drewnoakes:metadata-extractor:2.12.0") import PhotoStats.LensRange import com.drew.imaging.ImageMetadataReader import com.drew.metadata.Metadata

    import java.io.File typealias LensRange = Pair<Int, Int> data class Lens( val name: String, val range: LensRange ) val lenses = listOf(LensRange(17, 40), LensRange(24, 105), LensRange(24, 70), LensRange(70, 200)) fun appendFiles(dir: File, filesList: MutableList<File>) { filesList.addAll( dir.listFiles() .filter { !it.isDirectory && it.isSupported() } ) dir.listFiles() .filter { it.isDirectory && it.listFiles().isNotEmpty()}
  31. photoStats #!/usr/bin/env kscript @file:DependsOn("com.drewnoakes:metadata-extractor:2.12.0") import PhotoStats.LensRange import com.drew.imaging.ImageMetadataReader import com.drew.metadata.Metadata

    import java.io.File typealias LensRange = Pair<Int, Int> data class Lens( val name: String, val range: LensRange ) val lenses = listOf(LensRange(17, 40), LensRange(24, 105), LensRange(24, 70), LensRange(70, 200)) fun appendFiles(dir: File, filesList: MutableList<File>) { filesList.addAll( dir.listFiles() .filter { !it.isDirectory && it.isSupported() } ) dir.listFiles() .filter { it.isDirectory && it.listFiles().isNotEmpty()}
  32. #!/usr/bin/env kscript @file:DependsOn("com.drewnoakes:metadata-extractor:2.12.0") import PhotoStats.LensRange import com.drew.imaging.ImageMetadataReader import com.drew.metadata.Metadata import

    java.io.File typealias LensRange = Pair<Int, Int> data class Lens( val name: String, val range: LensRange ) val lenses = listOf(LensRange(17, 40), LensRange(24, 105), LensRange(24, 70), LensRange(70, 200)) fun appendFiles(dir: File, filesList: MutableList<File>) { filesList.addAll( dir.listFiles() .filter { !it.isDirectory && it.isSupported() } ) dir.listFiles() .filter { it.isDirectory && it.listFiles().isNotEmpty()} photoStats
  33. dir.listFiles() .filter { !it.isDirectory && it.isSupported() } ) dir.listFiles() .filter

    { it.isDirectory && it.listFiles().isNotEmpty()} .forEach { appendFiles(it, filesList) } } fun File.focalLength(): Int? { val metadata: Metadata = ImageMetadataReader.readMetadata(this) return metadata.directories .filter { it.name.contains("exif", true) } .firstOrNull { it.tags.find { tag -> tag.tagName == "Focal Length 35" || tag.tagName == "Foca Length" } != null } ?.let { directory -> (directory.tags.firstOrNull { it.tagName == "Focal Length 35" } ?: directory.tags.first { it.tagName == "Focal Length" }) .let { it.description.substringBefore(" ").toInt() } } } val supportedExtensions = listOf("jpg", "jpeg", "cr2", "dng") fun File.isSupported(): Boolean = supportedExtensions.find { it.equals(this.extension, true) } != nul val allFiles = ArrayList<File>() val paths = if (args.isNotEmpty()) args.toList() else listOf("./") paths.forEach { val currentDir = File(it) if (currentDir.exists() && currentDir.isDirectory) { println("Scanning ${currentDir.absolutePath}") appendFiles(currentDir, allFiles) photoStats
  34. paths.forEach { val currentDir = File(it) if (currentDir.exists() && currentDir.isDirectory)

    { println("Scanning ${currentDir.absolutePath}") appendFiles(currentDir, allFiles) } } val focals = allFiles.map { it.focalLength() } .filterNotNull() .sorted() val total = focals.size println("Total pictures: $total") lenses.forEach { lensRange -> val matching = focals.filter { it >= lensRange.first && it <= lensRange.second }.size println("Covered by ${lensRange.first}-${lensRange.second}: $matching (${(matching.toFloat() / total.toFloat() * 100).toInt()}%)") } photoStats Total pictures: 1536 Covered by 17-40: 839 (54%) Covered by 24-105: 1413 (91%) Covered by 24-70: 1094 (71%)
  35. Conclusions

  36. Conclusions • Kotlin script support • Good for quick prototyping

    • Kotlin language power • Limitations • kscript • Only for *nix-based OS • Powerful • Efficient
  37. Daniele Bonaldo Thank You! @danybony_ danybony danielebonaldo.com