How to prompt users for input from Scala shell scripts

This is an excerpt from the 1st Edition of the Scala Cookbook (partially modified for the internet). This is Recipe 14.12, “How to prompt users for input from Scala shell scripts.”

Problem

You want to prompt a user for input from a Scala shell script and read their responses.

Solution

In Scala 2, use the readLine, print, printf, and Console.read* methods to read user input, as demonstrated in the following script. Comments in the script describe each method:

#!/bin/sh
exec scala "$0" "$@"
!#

// write some text out to the user with Console.println
Console.println("Hello")

// Console is imported by default, so it's not really needed, just use println
println("World")

// readLine lets you prompt the user and read their input as a String
val name = readLine("What's your name? ")

// readInt lets you read an Int, but you have to prompt the user manually
print("How old are you? ")
val age = readInt()

// you can also print output with printf
println(s"Your name is $name and you are $age years old.")

Update

As of Scala 2.11.0, readLine is now available as scala.io.StdIn.readLine. The read* methods that I mention below are also in this package (such as scala.io.StdIn.readInt, etc.).

Discussion

The readLine method lets you prompt a user for input, but the other read* methods don’t, so you need to prompt the user manually with print, println, or printf.

You can list the Console.read* methods in the Scala REPL:

scala> Console.read
readBoolean   readByte   readChar   readDouble   readFloat
readInt       readLine   readLong   readShort    readf
readf1        readf2     readf3

Be careful with the methods that read numeric values; as you might expect, they can all throw a NumberFormatException.

Although these methods are thorough, if you prefer, you can also fall back and read input with the Java Scanner class:

// you can also use the Java Scanner class, if desired
val scanner = new java.util.Scanner(System.in)
print("Where do you live? ")

val input = scanner.nextLine()
print(s"I see that you live in $input")

Reading multiple values from one line

If you want to read multiple values from one line of user input (such as a person’s name, age, and weight), there are several approaches to the problem.

To my surprise, I prefer to use the Java Scanner class. The following code demonstrates the Scanner approach:

import java.util.Scanner

// simulated input
val input = "Joe 33 200.0"
val line = new Scanner(input)
val name = line.next
val age = line.nextInt
val weight = line.nextDouble

To use this approach in a shell script, replace the input line with a readLine() call, like this:

val input = readLine()

Of course if the input doesn’t match what you expect, an error should be thrown. The Scanner class next* methods throw a java.util.InputMismatchException when the data doesn’t match what you expect, so you’ll want to wrap this code in a try/catch block.

I initially assumed that one of the readf methods on the Console object would be the best solution to this problem, but unfortunately they return their types as Any, and then you have to cast them to the desired type. For instance, suppose you want to read the same name, age, and weight information as the previous example. After prompting the user, you read three values with the readf3 method like this:

val(a,b,c) = readf3("{0} {1,number} {2,number}")

If the user enters a string followed by two numbers, a result is returned, but if he enters an improperly formatted string, such as 1 a b, the expression fails with a ParseException:

java.text.ParseException: MessageFormat parse error!
    at java.text.MessageFormat.parse(MessageFormat.java:1010)
    at scala.Console$.readf(Console.scala:413)
    at scala.Console$.readf3(Console.scala:445)

Unfortunately, even if the user enters the text as desired, you still need to cast the values to the correct type, because the variables a, b, and c are of type Any. You can try to cast them with this approach:

val name = a
val age = b.asInstanceOf[Long]
val weight = c.asInstanceOf[Double]

Or convert them like this:

val name = a.toString
val age = b.toString.toInt
val weight = c.toString.toDouble

But for me, the Scanner is cleaner and easier.

A third approach is to read the values in as a String, and then split them into tokens. Here’s what this looks like in the REPL:

scala> val input = "Michael 54 250.0"
input: String = Michael 54 250.0

scala> val tokens = input.split(" ")
tokens: Array[String] = Array(Michael, 54, 250.0)

The split method creates an Array[String], so access the array elements and cast them to the desired types to create your variables:

val name = tokens(0)
val age = tokens(1).toInt
val weight = tokens(2).toDouble

Note that the age and weight fields in this example can throw a NumberFormatException.

A fourth way to read the user’s input is by specifying a regular expression to match the input you expect to receive. Using this technique, you again receive each variable as a String, and then cast it to the desired type. The process looks like this in the REPL:

scala> val ExpectedPattern = "(.*) (\\d+) (\\d*\\.?\\d*)".r
ExpectedPattern: scala.util.matching.Regex = (.*) (\d+) (\d*\.?\d*)

// you would use readLine() here
scala> val input = "Paul 36 180.0"
input: String = Paul 36 180.0

scala> val ExpectedPattern(a, b, c) = input
a: String = Paul
b: String = 36
c: String = 180.0

Now that you have the variables as strings, cast them to the desired types, as before:

val name = a
val age = b.toInt
val weight = c.toDouble

The ExpectedPattern line in this example will fail with a scala.MatchError if the input doesn’t match what’s expected.

Hopefully with all of these examples you’ll find your own preferred way to read in multiple values at one time.

Fun with output

Use print, printf, or println to write output. As shown in the Solution, the readLine method also lets you prompt a user for input.

The Console object contains a number of fields that you can use with the print methods to control the display. For instance, if you want your entire line of output to be underlined, change the last lines of the script to look like this:

val qty = 2
val pizzaType = "Cheese"
val total = 20.10

print(Console.UNDERLINED)
println(f"$qty%d $pizzaType pizzas coming up, $$$total%.2f.")
print(Console.RESET)

This prints the following string, underlined:

2 Cheese pizzas coming up, $20.10.

Other displayable attributes include colors and attributes such as BLINK, BOLD, INVISIBLE, RESET, REVERSED, and UNDERLINED. See the Console object Scaladoc page for more options.

See Also