Skip to content

User interaction

Progress tracking

Shell operations yield progress bars by default when they take too long. Progress operations are represented as ProgressReport objects and passed to a ProgressReport.Tracker. The global tracker is accessed via progressTracker.

There are utilities to help you work with and generate these events.

Tracking progress through a list

1
2
3
4
for (file in ls("/").withProgress(progressTracker)) {
    echo(file)
    Thread.sleep(1000)
}

asciicast

Customize the output

1
2
3
4
for (file in ls("/").withProgress(progressTracker, ProgressReport.create("Iterating over files"))) {
    echo(file)
    Thread.sleep(1000)
}

Send progress events manually

1
2
3
4
5
6
7
8
// Expect 500 units of work
var report = ProgressReport.create("My operation", 500).immutableReport()

while (!report.complete) {
    progressTracker.accept(report)
    Thread.sleep(100)
    report = report.withIncremented(1)
}

Spinner

For indefinite tasks where no progress can be measured:

1
2
3
progressTracker.indeterminate("My message") {
  Thread.sleep(5000)
}

Process a file with progress tracking

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Write a million lines of text.
write("bigfile.txt", buildString { for (i in 1..1_000_000) appendLine("Line $i") })

// Process lines whilst tracking progress.
path("bigfile.txt").inputStreamWithProgress(progressTracker).bufferedReader().use { reader ->
    reader.lineSequence().forEachIndexed { lineNumber, line ->
        if ("Line 12345" in line)
            echo(line)

        // Slow down a bit so the progress tracker is visible.
        if (lineNumber.mod(1000) == 0)
            Thread.sleep(10)
    }
}

asciicast

Prompting

1
val answer: String = prompt("Password?", hideInput = true)

There are also promptPrefix, promptSuffix, default parameters, and validateResponse which lets you quickly check if the user input is OK, repeating the question if not.

Warning the user

Automatically deduplicated warnings:

1
warnOnce("A warning!")

User errors

throw UserError("A message") to end the script in a way that indicates a user error. The exit code is set appropriately. Other kinds of exceptions will print a message indicating that the script has crashed.

Use check to assert internal invariants and verify to assert things that are potentially user errors:

1
2
3
4
5
6
7
8
@Option(names = ["--foo-path"])
var foo: Path? = null

// If --foo-path is specified, verify it's a .txt file and then assert (crash) if it's empty.
val textLines: List<String>? = foo?.let { foo ->
    verify(foo.name.endsWith(".txt"), "If --foo-path is specified it must be a .txt file")
    readLines(foo).also { check(it.isNotEmpty()) { "File $foo should not be empty" } }
}

Path assertions

1
2
val dir: Path = path("subdir").verifyIsExistingDirectory()
val file: Path = path("subdir/file.txt").verifyIsExistingFile()

If a path assertion fails the user is shown the contents of the nearest directory and a spelling error suggestion.

Example of filesystem assertions