Skip to content

hshell

A bash competitor that lets you write shell-like scripts in Kotlin, with many other useful features that bash doesn't provide.

Download for your OS

Warning

This site provides docs for an unreleased product! PROCEED WITH CAUTION AND LOW EXPECTATIONS.

Let's get started

demo.shell.kts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env hshell

// Read a file and print it as Markdown to the console.
cat("README.md")

echo()

// Echo markdown to the console.
echomd("## Notices and word wrapping")

// With emoji.
echo(Info("Some information. More text."))
echo(Warning("A warning. More text."))
echo(Error("An error. More text."))

echo()

echo("Roses are ${red("red")}, violets are ${blue("blue")}")

Run hshell demo.shell.kts or chmod +x demo.shell.kts; ./demo.shell.kts.

Example of printing Markdown to the console

List directories and print tables

Print the current directory contents as colored ls style output, and a table:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
val files /* List<Path> */ = ls()

echo(files)
echo()
echo()
echo(table {
    header { row("File name", "Permissions") }
    body {
        for (file in files)
            row(file.name, file.permissionsAsString)
    }
})

Example of files and tables

Find the path to the script, and its containing directory:

1
2
echo(scriptDirPath)
echo(scriptFilePath)

Assertions

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

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

Example of filesystem assertions

Use check for internal assertions and verify for 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" } }
}

asciicast

Other string utilities

1
echo("{one,two-{b,c},three}".braceExpand().bulletedList())

Example of bullets and braces

File ops

See shell API.

Dependencies

Brace expansion in coordinates works:

1
2
3
4
5
6
@file:DependsOn("io.ktor:ktor-client-{core,cio}-jvm:2.0.2")

import io.ktor.client.*
import io.ktor.client.engine.cio.*

val client = HttpClient(CIO)

XML

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
val dom1 = xmlstr("""
    <root>
        <child>text</child>
    </root>
""")

echo(dom.getElementsByTagName("child").item(0).textContent)

val dom2 = xml("foo.xml")
val dom3 = xml("https://www.google.com/sitemap.xml")

Cleaning up

Instead of writing foo.use { foo -> foo.whatever() } to clean things up, you can write foo.closeAtEnd().whatever():

1
val bytes = (dir / "foo").inputStreamWithProgress(progressTracker).closeAtEnd().readBytes()

The closeAtEnd() function uses defer to register a code block to run when the script finishes:

1
2
defer { echo("Good bye!") }
val something = Something().andDefer { methodOnSomething() }

The last block deferred is run first.

Use exit(exitCode) to terminate early. Don't call exitProcess unless you're sure you want immediate termination without any finally or deferred blocks running.

Disk caching

A LocalDiskCache will give you a directory associated with an arbitrary string key, passing it to a handler to fill with content if the key misses in the cache:

1
2
3
4
5
val cache = LocalDiskCache(dir / "cache-dir").closeAtEnd()

val dir = cache.get("Some useful stuff") { cacheEntry: Path ->
    // Fill the cache entry.
}

The cache will automatically keep its size in check, is thread safe and can be configured in various ways. You can also force cache misses and edit content in place.