One of the biggest challenges with a terminal/command line driven interface (CLI) is parsing out the relavent pieces of the input. For a lot of web and touch based interfaces this kind of input is constrained by input widgets.
We recently completed an ‘Event Reporter’ project, and it allows the user to interact with a library of data about attendees to an event. Commands would include:
find last_name Smith,
save to results.csv, or
queue count and
Ruby provides a capable set of methods for breaking input into strings and arrays, which then allows you to manipulate and extract the parts that are meaningful so they can be passed as messages to the other classes in the application.
The basic CLI requires a REPL loop that Reads input, Evaluates it, Prints a response, and repeats until some condition is met that terminates the loop.
These are examples of how we tackled some of the problems we came up against:
1 2 3 4 5 6 7 8
In the code snippet above, the two methods on lines 4 & 5 handle the request for input and then pass that input off for processing.
process_command method is a
case statement that has boolean conditions based on the input received, and for each type of command request, breaks the command down to extract the required content. For example, the command
load has two conditions: as a single term that will load a default file, and with a filename that will load a given file. One strategy looks like this:
1 2 3 4 5 6 7 8
Here we already know that the second (or last) part of the command is going to be the filename, so we use
length to determine that condition, and then pass the corresponding string out of the
@input array to the
load() method. The
load method has it’s own logic to check if a file exists, and handle malformed file names, since that is not a command parsing responsiblity.
When evaluating the
find method there are three components to the command,
criteria, i.e. state, city, etc., and the
term to search against, ‘CO’ or ‘Denver’. However the
term may be consist of multiple words, such as ‘New Orleans’. Because we decompose the command into an array, split on spaces, the search term needs to be reconsituted. One way to approach that might look like this:
1 2 3 4 5 6 7 8 9 10
In a similar strategy to the
load method, we evaluate how long the input request is. Then we go after the
criteria and the
term. Criteria is the second element in the array after the command, so that is handled with
input (remembering that Ruby is zero-indexed).
The search term or criteria is all the remaining string elements of the array, so we get that value on line 3 by re-combining all the array elements from
[input..input.length] and then
.join(" ") them with a space between. The resulting terms are then handled off to the
repository on line 4 to the
find(criteria, search_term) method.
As part of the evaluation of our project, all of this parsing logic should eventually be refactored into a separate class of it’s own, but that’s a topic for another post.