Command Line Interface Application

    There are two main topics when building a CLI application:

    This topic covers all things related to:

    It is a very common practice to pass options to the application. For example, we may run crystal -v and Crystal will display:

    and if we run: crystal -h, then Crystal will show all the accepted options and how to use them.

    So now the question would be: do we need to implement an options parser? No need to, Crystal got us covered with the class OptionParser. Let’s build an application using this parser!

    At the start our CLI application has two options:

    • -v / --version: it will display the application version.
    • -h / --help: it will display the application help.
    1. require "option_parser"
    2. OptionParser.parse do |parser|
    3. parser.banner = "Welcome to The Beatles App!"
    4. parser.on "-v", "--version", "Show version" do
    5. puts "version 1.0"
    6. exit
    7. end
    8. parser.on "-h", "--help", "Show help" do
    9. puts parser
    10. exit
    11. end
    12. end

    So, how does all this work? Well … magic! No, it’s not really magic! Just Crystal making our life easy. When our application starts, the block passed to OptionParser#parse gets executed. In that block we define all the options. After the block is executed, the parser will start consuming the arguments passed to the application, trying to match each one with the options defined by us. If an option matches then the block passed to parser#on gets executed!

    We can read all about OptionParser in the official API documentation. And from there we are one click away from the source code … the actual proof that it is not magic!

    Now, let’s run our application. We have two ways :

    1. Build the application and then run it.
    2. Compile and , all in one command.

    We are going to use the second way:

    1. $ crystal run ./help.cr -- -h
    2. Welcome to The Beatles App!
    3. -v, --version Show version
    4. -h, --help Show help

    Let’s build another fabulous application with the following feature:

    By default (i.e. no options given) the application will display the names of the Fab Four. But, if we pass the option -t / --twist it will display the names in uppercase:

    1. require "option_parser"
    2. the_beatles = [
    3. "John Lennon",
    4. "Paul McCartney",
    5. "George Harrison",
    6. "Ringo Starr",
    7. ]
    8. shout = false
    9. option_parser = OptionParser.parse do |parser|
    10. parser.banner = "Welcome to The Beatles App!"
    11. parser.on "-v", "--version", "Show version" do
    12. puts "version 1.0"
    13. exit
    14. end
    15. parser.on "-h", "--help", "Show help" do
    16. puts parser
    17. exit
    18. end
    19. parser.on "-t", "--twist", "Twist and SHOUT" do
    20. shout = true
    21. end
    22. members = the_beatles
    23. members = the_beatles.map &.upcase if shout
    24. puts ""
    25. puts "Group members:"
    26. puts "=============="
    27. members.each do |member|
    28. puts member
    29. end
    1. $ crystal run ./twist_and_shout.cr -- -t
    2. Group members:
    3. ==============
    4. JOHN LENNON
    5. PAUL MCCARTNEY
    6. GEORGE HARRISON
    7. RINGO STARR

    Parameterized options

    Let’s create another application: when passing the option -g / --goodbye_hello, the application will say hello to a given name passed as a parameter to the option.

    In this case, the block receives a parameter that represents the parameter passed to the option.

    Let’s try it!

    1. $ crystal run ./hello_goodbye.cr -- -g "Penny Lane"
    2. You say goodbye, and Ringo Starr says hello to Penny Lane!

    Great! These applications look awesome! But, what happens when we pass an option that is not declared? For example -n

    1. $ crystal run ./hello_goodbye.cr -- -n
    2. Unhandled exception: Invalid option: -n (OptionParser::InvalidOption)
    3. from ...

    Oh no! It’s broken: we need to handle invalid options and invalid parameters given to an option! For these two situations, the OptionParser class has two methods: #invalid_option and #missing_option

    So, let’s add this option handler and merge all these CLI applications into one fabulous CLI application!

    All My CLI: The complete application!

    Here’s the final result, with invalid/missing options handling, plus other new options:

    1. require "option_parser"
    2. the_beatles = [
    3. "John Lennon",
    4. "Paul McCartney",
    5. "George Harrison",
    6. "Ringo Starr",
    7. ]
    8. shout = false
    9. say_hi_to = ""
    10. strawberry = false
    11. option_parser = OptionParser.parse do |parser|
    12. parser.banner = "Welcome to The Beatles App!"
    13. parser.on "-v", "--version", "Show version" do
    14. puts "version 1.0"
    15. exit
    16. end
    17. parser.on "-h", "--help", "Show help" do
    18. puts parser
    19. exit
    20. end
    21. parser.on "-t", "--twist", "Twist and SHOUT" do
    22. shout = true
    23. end
    24. parser.on "-g NAME", "--goodbye_hello=NAME", "Say hello to whoever you want" do |name|
    25. say_hi_to = name
    26. end
    27. parser.on "-r", "--random_goodbye_hello", "Say hello to one random member" do
    28. say_hi_to = the_beatles.sample
    29. end
    30. parser.on "-s", "--strawberry", "Strawberry fields forever mode ON" do
    31. strawberry = true
    32. end
    33. STDERR.puts "ERROR: #{option_flag} is missing something."
    34. STDERR.puts ""
    35. STDERR.puts parser
    36. exit(1)
    37. end
    38. parser.invalid_option do |option_flag|
    39. STDERR.puts "ERROR: #{option_flag} is not a valid option."
    40. STDERR.puts parser
    41. exit(1)
    42. end
    43. end
    44. members = the_beatles.map &.upcase if shout
    45. puts "Strawberry fields forever mode ON" if strawberry
    46. puts ""
    47. puts "Group members:"
    48. puts "=============="
    49. members.each do |member|
    50. puts "#{strawberry ? "🍓" : "-"} #{member}"
    51. end
    52. unless say_hi_to.empty?
    53. puts ""
    54. puts "You say goodbye, and I say hello to #{say_hi_to}!"
    55. end

    Request for user input

    Sometimes, we may need the user to input a value. How do we read that value? Easy, peasy! Let’s create a new application: the Fab Four will sing with us any phrase we want. When running the application, it will request a phrase to the user and the magic will happen!

    1. puts "Welcome to The Beatles Sing-Along version 1.0!"
    2. puts "Enter a phrase you want The Beatles to sing"
    3. print "> "
    4. user_input = gets
    5. puts "The Beatles are singing: 🎵#{user_input}🎶🎸🥁"

    The method will pause the execution of the application until the user finishes entering the input (pressing the Enter key). When the user presses Enter, then the execution will continue and user_input will have the user value.

    But what happens if the user doesn’t enter any value? In that case, we would get an empty string (if the user only presses Enter) or maybe a Nil value (if the input stream is closed, e.g. by pressing Ctrl+D). To illustrate the problem let’s try the following: we want the input entered by the user to be sung loudly:

    When running the example, Crystal will reply:

    1. $ crystal run ./let_it_cli.cr
    2. Showing last frame. Use --error-trace for full trace.
    3. In let_it_cli.cr:5:46
    4. 5 | puts "The Beatles are singing: 🎵#{user_input.upper_case}
    5. ^---------
    6. Error: undefined method 'upper_case' for Nil (compile-time type is (String | Nil))

    Ah! We should have known better: the type of the user input is the union type String | Nil. So, we have to test for Nil and for empty and act naturally for each case:

    1. puts "Welcome to The Beatles Sing-Along version 1.0!"
    2. puts "Enter a phrase you want The Beatles to sing"
    3. print "> "
    4. user_input = gets
    5. exit if user_input.nil? # Ctrl+D
    6. default_lyrics = "Na, na, na, na-na-na na" \
    7. " / " \
    8. "Na-na-na na, hey Jude"
    9. lyrics = user_input.presence || default_lyrics
    10. puts "The Beatles are singing: 🎵#{lyrics.upcase}🎶🎸🥁"

    And to accomplish this, we will be using the module.

    Let’s build a really simple application that shows a string with colors! We will use a yellow font on a black background:

    1. require "colorize"
    2. puts "#{"The Beatles".colorize(:yellow).on(:black)} App"

    Great! That was easy! Now imagine using this string as the banner for our All My CLI application, it’s easy if you try:

    1. parser.banner = "#{"The Beatles".colorize(:yellow).on(:black)} App"

    For our second application, we will add a text decoration (blinkin this case):

    Let’s try the renewed application … and hear the difference!! Now we have two fabulous apps!!

    You may find a list of available colors and text decorations in the API documentation.

    As with any other application, at some point, we would like to for the different features.

    Right now the code containing the logic of each of the applications always gets executed with the OptionParser, i.e. there is no way to include that file without running the whole application. So first we would need to refactor the code, separating the code necessary for parsing options from the logic. Once the refactoring is done, we could start testing the logic and including the file with the logic in the testing files we need. We leave this as an exercise for the reader.

    In case we want to build richer CLI applications, there are libraries that can help us. Here we will name two well-known libraries: Readline and NCurses.

    As stated in the documentation for the GNU Readline Library, Readline is a library that provides a set of functions for use by applications that allow users to edit command lines as they are typed in. Readline has some great features: filename autocompletion out of the box; custom auto-completion method; keybinding, just to mention a few. If we want to try it then the shard will give us an easy API to use Readline.

    On the other hand, we have NCurses(New Curses). This library allows developers to create graphical user interfaces in the terminal. As its name implies, it is an improved version of the library named Curses, which was developed to support a text-based dungeon-crawling adventure game called Rogue! As you can imagine, there are already a couple of shards in the ecosystem that will allow us to use NCurses in Crystal!

    And so we have reached The End 😎🎶