Skip to main content

Getting started with NSScript

NSScript serves to replace script engines/commandline interpreters such as Bash, Cmd or PowerShell. Creating an NSScript script is therefore also very similar.

A script is defined as a file with a .nss extension. Like any text file that can be executed on a Unix system, it is possible to use a shebang sequence to define the interpreter for the script. On Windows this line will simply be ignored when executing the script.

Scripts are written in the Kotlin language which runs in a Java Virtual Machine, so a large portion of the Kotlin API and the Java API are available to build scripts if needed.

Example

In this basic hello world example, the Kotlin println() function is called to write to the console.

hello_world.nss
#!/usr/bin/env nss

println("Hello World!")
Output
$ nss hello_world.nss
Hello World!

Imports

API imports

It is possible to import Kotlin/Java APIs with regular import statements. The standard libraries are available on the classpath, so these are always accessible.

Example

Importing the Java UUID class from the standard Java API is as simple as adding an import to access its full functionality.

uuid.nss
#!/usr/bin/env nss

import java.util.UUID

println(UUID.randomUUID())
Output
$ nss uuid.nss
2f95518a-578b-43b0-9a94-9543a41de58e

Script imports

It is possible to import code and functionality from other scripts. These imports are not transitive, but do allow for scripts to centralize some functionality. The contents of the global scope of a script import will be available in the global scope of the importing script.

All script imports should be defined only once and at the top of the script file. The path of the import is relative to the importing script, but could also be defined as an absolute path, though not recommended.

Example
common.nss
#!/usr/bin/env nss

fun sayHello(target: String) {
println("Hello $target!")
}
hello_again.nss
#!/usr/bin/env nss
@file:Import("common.nss")

sayHello("Mars")
Output
$ nss hello_again.nss
Hello Mars!

NS DSL/API design

The NS specific API uses higher-order functions to make scripting for common tasks clean and simple. There are two categories of functions:

  • Scope functions: These change the context of the scope by either providing different/changed fields or different functionality.
  • Execution functions: These are essentially builders that can be used to execute some task by providing the necessary arguments within the context of an execution template.

Scope functions

Both the script itself (the global scope) as several functions in the API are "scoping functions". Every scoping function can provide some functionality and/or modify the scope context. All functions will adapt their behavior depending on the context within which they execute.

Attention

Since the scripting language itself allows access from withing the scope of a function body to any parent scope, any functionality introduced by a parent scope function can be accessed if it was not redeclared in the scope itself or another parent in the chain. This applies to things explicitly defined in the bodies of functions, as well as things provided implicitly by the API.

scope_example.nss
#!/usr/bin/env nss

class MyScope

fun example(body: MyScope.() -> Unit) {
MyScope().body()
}

example {
val hiStr = "Hello"
val targetStr = "World"
example {
val targetStr = "Mars"
println("$hiStr $targetStr!")
}
}
Output
$ nss scope_example.nss
Hello Mars!

Scope context

The scope context is a field that is provided by every scope function. The name of this field is ctx and this contains data that is relevant within the current scope. Additionally, the scope context also contains a field script, which contains the script context with data that is fixed for the entire script.

Example
script_scope.nss
#!/usr/bin/env nss

println("Current directory: ${ctx.path}")
println("Script directory: ${ctx.script.path}")

dir("../new_dir") {
println("Scoped directory: ${ctx.path}")
println("Still original script directory: ${ctx.script.path}")
}

dir(ctx.script.path) {
println("Script directory as scope: ${ctx.path}")
}
Output
$ nss Desktop/script_scope.nss
Current directory: /home/username
Script directory: /home/username/Desktop
Scoped directory: /home/new_dir
Still original script directory: /home/username/Desktop
Script directory as scope: /home/username/Desktop

Execution functions

Though execution functions looks similar to scope functions, they serve a vastly different purpose. They are builders which allow you to construct the parameters to execute some functionality. Functions are used to supply arguments to the builder and once the function has executed logic defined in the function body, it will use the constructed data to execute the command.

Because execution functions are builders, but defined as a function body that executed on an execution template, it is version transparent and possible to use logic in the function body to determine how the arguments should be populated.

Unlike scope functions, execution functions do not redefine the ctx scope context field, but they do inherit it from the parent scope and take it into account when executing. To avoid confusion, it is not allowed to use scope functions inside of execution functions, as this would also serve no purpose.

Example
execute_echo.nss
#!/usr/bin/env nss

val currentPlanet = "Earth"
exec {
arg("echo", "Hello")
if (currentPlanet == "Earth") {
arg("World!")
} else {
arg("Mars!")
}
}
Output
$ nss execute_echo.nss
Hello World!

JSON serialization

JSON serialization is supported through kotlinx serialization.

Example
serialize_json.nss
#!/usr/bin/env nss

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString

@Serializable
data class DataClass(val hello: String, val number: Int)

val data = DataClass("world", 42)

val json = Json.encodeToString(data)
println(json)
Output
$ nss serialize_json.nss
{"hello":"world","number":42}