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
#!/usr/bin/env hshell

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

echo()
echomd("## Notices and word wrapping")
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").assertIsDirectory()
val file: Path = path("subdir/file.txt").assertIsFile()

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

SSH

ssh gives a subshell pointed at the remote host. Path objects keep track of what machine they were collected on:

1
2
3
4
5
6
7
val localFile = scriptDirPath / "data-file.txt"
ssh("mike@hq.hydraulic.software") {
    // From local to remote.
    cp(localFile, "file-on-remote-host.txt")
    // And back.
    cp("file-on-remote-host.txt", localFile)
}

You can omit the username and execute commands remotely:

1
2
3
ssh("hq.hydraulic.software") {
    check("hostname".get<String>() == "hq.hydraulic.software")
}

Explicitly managing the lifetime:

1
2
3
4
5
val remoteMachine = ssh("hq.hydraulic.software")
val remoteFiles = remoteMachine.ls().toSortedSet()
val localFiles = ls().toSortedSet()
val onBoth: Set<Path> = remoteFiles.intersect(localFiles)
remoteMachine.close()

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")