CS 346 (W23)
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Design

Architecture

Command-line applications tend towards performing a single action and then exiting. This lends itself to a pipeline architecture, where processing steps are performed on intput, in order, to produce an output. This is also called a pipes and filters design pattern.

Pipeline Architecture

Examples of how this pattern might be applied:

  • A file rename utility that changes the date on one or more files that are supplied as arguments. The program would (1) process and validate the command-line arguments, (2) apply the rule to produce a set of target output files (likely original:revise filename pairs) and then (3) perform these operations. Any interruption or error would cause the rename to abort.
  • An image processing utility that attempts to filter a photo. Filters are applied in sequence on the input image producing a final output image.

Of course, not every command-line program works like this; there are some that are interactive, where the user provides successive input while the program is running. A program like this could use a mode traditional event-driven architecture:

Event-Driven Programming

Examples of this might include a text editor like Vi. It can certainly process command-line arguments, but it primarily operates in an interactive mode, where it waits for, and acts upon, user input (where keystrokes are represented as events).

Lifecycle

Command-line applications expect a single-entry point: a method with this familiar signature:

fun main(args:Array<String>) {
    // 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.

Fancy console output

As suggested above, we want to use a standard calling convention. Typically, command-line applications should use this format, or something similar:

$ 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]

Common 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")
	}
}
This is a great place to use the command design pattern to abstract commands. See the public repo for samples.

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.

^ C0 Abbr Name Effect
^G 7 BEL Bell Makes an audible noise.
^H 8 BS Backspace Moves the cursor left (but may “backwards wrap” if cursor is at start of line).
^I 9 HT Tab Moves the cursor right to next multiple of 8.
^J 0xA LF Line Feed Moves 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.
^L 0xC FF Form Feed Move a printer to top of next page. Usually does not move horizontally, though programs should not rely on this. Effect on video terminals varies.
^M 0xD CR Carriage Return Moves the cursor to column zero.
^[ 0x1B ESC Escape Starts 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.

Cursor ESC Code Effect
Left \u001B [1D Move the cursor one position left
Right \u001B [1C Move the cursor one position right
Up \u001B [1A Move the cursor one row up
Down \u001B [1B Move the cursor one row down
Startline \u001B [250D Move the cursor to the start of the line
Home \u001B [H Move the cursor to the home position
Clear \u001B [2J Clear the screen
Reset \u001B Reset 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.

Cursor ESC Code Effect
Colour: Black \u001B [30m Set colour Black for anything printed after this
Colour: Yellow \u001B [33m Set colour Yellow for anything printed after this
Colour: White \u001B [37m Set 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

Reference

  • Donald A. Norman. 1981. The truth about Unix: The user interface is horrid. Datamation 27, 12.
  • Chet Ramey & Brian Fox. 2020. GNU Bash Reference Manual 5.1. https://www.gnu.org.