Console development

A console application (aka “command-line application”) is an application that is intended to run from within a shell, such as bash, zsh, or PowerShell, and uses text and block characters for input and output. This style of application dominated through the early days of computer science, until the 1980s when graphical user interfaces became much more common.

Console applications use text for both input and output: this can be as simple as plan text displayed in a window (e.g. bash), to systems that use text-based graphics and color to enhance their usability (e.g. Midnight Commander). This application style was really driven by the technical constraints of the time. Software was written for text-based terminals, often with very limited resources, and working over slow network connections. Text was faster to process, transmit and display than sophisticated graphics.

Midnight Commander
Midnight Commander pushes text graphics pretty far; it's a great example of what can be done in a console.

Some console applications remain popular, often due to powerful capabilities or features that are difficult to replicate in a graphical environment. Vim, for instance, is a text editor that originated as a console application, and is still used by many developers. Despite various attempts to build a “graphical vim”, the console version is often seen as more desireable due to the benefits of console applications i.e. faster, reduced latency, small memory footprint and ability to easily leverage other command-line tools.

Vim (text editor)
Vim is a very popular console application. There have been many attempts to create a "graphical Vi" but they've never been as popular as the console version.

Design

Typically, command-line applications should use this format, or something similar, when executed from the command-line:

$ program_name --option=value parameter

  • program_name is the name of your program. It should be meaningful, and reflect what your program does.
  • options represent a value that tells your program how to operate on the data. Typically options are prefixed with a dash (”-”) to distinguish them from parameters. If an option also requires a value (e.g. ”-bg=red”) then separate the option and value with a colon (”:”) or an equals sign (”=”).
  • parameter represents input, or data that your program would act upon (e.g. the name of a file containing data). If multiple parameters are required, separate them with whitespace (e.g. ”program_name parameter1 parameter2”).

The order of options and parameters should not matter.

Running a program with insufficient arguments should display information on how to successfully execute it.

 $ rename
 Usage: rename [source] [dest]

Typical command-line interaction is shown below:

% exa --help
Usage:
  exa [options] [files...]

META OPTIONS
  -?, --help         show list of command-line options
  -v, --version      show version of exa

DISPLAY OPTIONS
  -1, --oneline      display one entry per line
  -l, --long         display extended file metadata as a table
  -G, --grid         display entries as a grid (default)
  -x, --across       sort the grid across, rather than downwards
  -R, --recurse      recurse into directories
  -T, --tree         recurse into directories as a tree
  -F, --classify     display type indicator by file names
  ...

Although we tend to run graphical operating systems with graphical applications, console applications are still not uncommon. For expert users in particular, this style has some advantages.

Advantages:

  • They can easily be scripted or automated to run without user intervention.
  • Use standard I/O streams, to allow interaction with other console applications.
  • Tend to be lightweight and performant, due to their relatively low-overhead (i.e. no graphics, sound).

Disadvantages:

  • Lack of interactions standards,
  • A steep learning curve (man pages and trial-and-error), and
  • Lack of feature discoverability (i.e., you need to memorize commands/how to use the app).

Project Setup

Create a new project

In IntelliJ, use File > New > Project > Kotlin > Kotlin/JVM to create a new project. Standard Kotlin projects are JVM projects. (Note that Android Studio is NOT recommended for console projects).

Create a main method

Command-line applications require a single entry point, typically called main. This method is the first method that is executed when your program is launched.

fun main(args:Array<String>) {
    // insert code here
    System.exit(0)
}

When your program is launched it executes this method. When it reaches the end of the method, the program stops executing. It’s considered “good form” to call System.exit(0) where the 0 is an error code returned to the OS. In this case, 0 means no errors i.e. a normal execution.

  • Input should be handled through either (a) arguments supplied on the command-line, or (b) values supplied through stdin. Batch-style applications should be able to run to completion without prompting the user for more input.
  • Output should be directed to stdout. You’re typically limited to textual output, or limited graphics (typically through the use of Unicode characters).
  • Errors should typically be directed to stderr.

Compile and package

Use the Gradle menu (View > Tool Windows > Gradle).

CommandWhat does it do?
Tasks > build > cleanRemoves temp files (deletes the /build directory)
Tasks > build > buildCompiles your application
Tasks > application > runExecutes your application (builds it first if necessary)
Tasks > application > installDistCreates a distribution package
Tasks > application > distZipCreates a distribution package

Features

Processing arguments

The main() method can optionally accept an array of Strings containing the command-line arguments for your program. To process them, you can simply iterate over the array.

This is effectively the same approach that you would take in C/C++.

fun main(args: Array<String>) {
	// args.size returns number of items
	for (s in args) {
	  println("Argument: $s")
	}
}

Tip

If you are writing this code yourself, this is a great place to use the command design pattern to abstract command-line options. I’ve also used the Clikt library, which is great for collecting and handling arguments. Clikt also provides built in support for common options like --help and --version.

Clikt handles command-line arguments

Reading/writing text

The Kotlin Standard Library (“kotlin-stdlib“) includes the standard IO functions for interacting with the console.

  • readLine() reads a single value from “stdin“
  • println() directs output to “stdout“

code/ucase.kts

// read single value from stdin
val str:String ?= readLine()
if (str != null) {
  println(str.toUpperCase())
}

It also includes basic methods for reading from existing files.

import java.io.*
var filename = "transactions.txt"

// read up to 2GB as a string (incl. CRLF)
val contents = File(filename).readText(Charsets.UTF_8)
println(contents)

2020-06-06T14:35:44, 1001, 78.22, CDN
2020-06-06T14:38:18, 1002, 12.10, CDN
2020-06-06T14:42:51, 1003, 44.50, CDN

Example: rename utility

Let’s combine these ideas into a larger example.

Requirements: Write an interactive application that renames one or more files using options that the user provides. Options should support the following operations: add a prefix, add a suffix, or capitalize it.

We need to write a script that does the following:

  1. Extracts options and target filenames from the arguments.
  2. Checks that we have (a) valid arguments and (b) enough arguments to execute program properly (i.e. at least one filename and one rename option).
  3. For each file in the list, use the options to determine the new filename, and then rename the file.

Usage: rename.kts [option list] [filename]

For this example, we need to manipulate a file on the local file system. The Kotlin standard library offers a File class that supports this. (e.g. changing permissions, renaming the file).

Construct a ‘File‘ object by providing a filename, and it returns a reference to that file on disk.

fun main(args: Array<String>) {
  val files = getFiles(args)
  val options = getOptions(args)

  // check minimum required arguments
  if (files.size == 0 || options.size == 0) {
	  println("Usage: [options] [file]")
  } else {
  	applyOptions(files, options)
}

fun getOptions(args:Array<String>): HashMap<String, String> {
	var options = HashMap<String, String>()
	for (arg in args) {
		if (arg.contains(":")) {
			val (key, value) = arg.split(":")
			options.put(key, value)
		}
	}
	return options
}

fun getFiles(args:Array<String>) : List<String> {
  var files:MutableList<String> = mutableListOf()
    for (arg in args) {
	    if (!arg.contains(":")) {
        files.add(arg)
      }
    }
  return files
}

fun applyOptions(files:List<String>,options:HashMap<String, String>) {
	for (file in files) {
		var rFile = file
		// capitalize before adding prefix or suffix
    if (options.contains("prefix")) rFile = options.get("prefix") + rFile
    if (options.contains("suffix")) rFile = rFile + options.get("suffix")

		File(file).renameTo(rFile)
		println(file + " renamed to " + rFile)
	}
}

Reading/writing binary data

These examples have all talked about reading/writing text data. What if I want to process binary data? Many binary data formats (e.g. JPEG) are defined by a standard, and will have library support for reading and writing them directly.

Kotlin also includes object-streams that support reading and writing binary data, including entire objects. You can, for instance, save an object and it’s state (serializing it) and then later load and restore it into memory (deserializing it).

class Emp(var name: String, var id:Int) : Serializable {}
var file = FileOutputStream("datafile")
var stream = ObjectOutputStream(file)
var ann = Emp(1001, "Anne Hathaway", "New York")
stream.writeObject(ann)

Cursor positioning

ANSI escape codes are specific codes that can be “printed” by the shell but will be interpreted as commands rather than output. Note that these are not standard across consoles, so you are encouraged to test with a specific console that you wish to support.

^C0AbbrNameEffect
^G7BELBellMakes an audible noise.
^H8BSBackspaceMoves the cursor left (but may “backwards wrap” if cursor is at start of line).
^I9HTTabMoves the cursor right to next multiple of 8.
^J0xALFLine FeedMoves to next line, scrolls the display up if at bottom of the screen. Usually does not move horizontally, though programs should not rely on this.
^L0xCFFForm FeedMove a printer to top of next page. Usually does not move horizontally, though programs should not rely on this. Effect on video terminals varies.
^M0xDCRCarriage ReturnMoves the cursor to column zero.
^[0x1BESCEscapeStarts all the escape sequences

These escape codes can be used to move the cursor around the console. All of these start with ESC plus a suffix.

CursorESCCodeEffect
Left\u001B[1DMove the cursor one position left
Right\u001B[1CMove the cursor one position right
Up\u001B[1AMove the cursor one row up
Down\u001B[1BMove the cursor one row down
Startline\u001B[250DMove the cursor to the start of the line
Home\u001B[HMove the cursor to the home position
Clear\u001B[2JClear the screen
Reset\u001BReset to default settings

For instance, this progress bar example draws a line and updates the starting percentage in-place.

val ESC  = "\u001B";
val STARTLINE = ESC + "[250D";

for (i in 1..100) {
  print(STARTLINE);
  print(i + "% " + BLOCK.repeat(i));
}

// output
100% ▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋▋

Printing in colour

We can also use escape sequences to change the color that is displayed.

{.compact}

CursorESCCodeEffect
Colour: Black\u001B[30mSet colour Black for anything printed after this
Colour: Yellow\u001B[33mSet colour Yellow for anything printed after this
Colour: White\u001B[37mSet colour White for anything printed after this

Below, we use ANSI colour codes to display colored output:

val ANSI_BLACK = "\u001B[30m"
val ANSI_YELLOW = "\u001B[33m"
val ANSI_WHITE = "\u001B[37m"

println(ANSI_WHITE + "  ;)(;  ");
println(ANSI_YELLOW + " :----: " + ANSI_BLACK + " Vendor:   " + System.getProperty("java.vendor"));
println(ANSI_YELLOW + "C|" + ANSI_WHITE + "====" + ANSI_YELLOW + "| " + ANSI_BLACK + " JDK Name: " + System.getProperty("java.vm.name"));
println(ANSI_YELLOW + " |    | " + ANSI_BLACK + " Version:  " + System.getProperty("java.version"));
println(ANSI_YELLOW + " `----' ");
println();

Output using ANSI escape codes

Useful Libraries

There are some advanced toolkits that make working with console applications easier. They’re highly recommended for handling user-input, parsing command-line arguments, and building interactive console applications.

  • Clickt: Multiplatform command-line interface framework.
  • kotlin-inquirer: Building interactive console apps.
  • Kotter: Declarative API for dynamic console applications.

Packaging & Installers

A console or command-line application is typically deployed as a JAR file, containing all classes and dependencies. The Gradle application plugin has the ability to generate a JAR file, and an associated script that can be used to launch your application.

In Gradle:

Tasks > distribution > distZip or distTar

This will produce a ZIP or TAR file in the build/distribution folder. If you uncompress it, you will find a lib directory of JAR file dependencies, and a bin directory with a script that will execute the program. To install this, you could place these contents into a folder, and add the bin to your $PATH.

Example

Here’s an example of Kotlin programs that are “installed” on my computer1. For each program, I unzipped the distribution ZIP file into a directory named bin that I’ve placed in my PATH, and created symlinks from the program’s script to a top-level alias in the bin directory i.e. the symlinks are in the bin directory, which also places them in my PATH so that I can easily run them.

$ tree bin
bin
├── courses -> /Users/jaffe/bin/courses-1.1/bin/courses
├── courses-1.1
│   ├── bin
│   └── lib
├── roll -> /Users/jaffe/bin/roll-1.0/bin/roll
├── roll-1.0
│   ├── bin
│   └── lib
1

If you’re curious, courses prints out course calendar information, and roll is a dice roller. No, I don’t use it for grading.