Skip to content

Non CLI applications

In this whole documentation we used a CLI application to demonstrate the capabilities of Clip. But one goal of Clip is to work as well for a CLI application than for an application which interacts with users in a text way and want to use options, arguments or commands.

Example of such applications are REPL or text bots (like IRC bots).

Let's build a very simple REPL:

module Myapplication
  VERSION = "0.1.0"

  def self.run
    loop do
      print "> "
      case gets
      when "hello"
        self.hello
      when "exit"
        return
      else
        puts "Unknown command."
      end
    end
  end

  def self.hello
    puts "Hello, world!"
  end
end

Myapplication.run

It does not do much, but it does something:

$ shards build
Dependencies are satisfied
Building: myapplication
$ ./bin/myapplication
> help
Unknown command.
> hello
Hello, world!
> exit

Now we would like to let the user specify the person to greet. We could try to parse the input when it starts with "hello", and accept one argument. Then we may want to add another feature that could be enabled but would be disabled by default, something like… an option. And then we will probably want to add some others commands, and we will need an help command so the user could known which commands are available and how to use them.

You have probably guessed it yet: we want to interact with our REPL like we would do with a CLI application. So why writing all this mechanism by ourself? Unlike most of the CLI application libraries, Clip was built from the begining with this use case in mind. That's why Clip never prints anything by itself. You are in control, because your output may not be STDOUT but instead an IRC channel or anything else.

Let's use Clip to build a nicer REPL with all that we learned:

require "process"

require "clip"

module Myapplication
  VERSION = "0.1.0"

  @[Clip::Doc("Greet a person.")]
  abstract struct Command
    include Clip::Mapper

    Clip.add_commands({
      "hello"   => HelloCommand,
      "goodbye" => GoodbyeCommand,
      "exit"    => ExitCommand,
    })
  end

  abstract struct GreetCommand < Command
    @[Clip::Doc("The name of the person to greet.")]
    getter name : String
  end

  @[Clip::Doc("Say hello to someone.")]
  struct HelloCommand < GreetCommand
    include Clip::Mapper

    @[Clip::Doc("Repeat the message many times.")]
    getter repeat = 1
  end

  @[Clip::Doc("Say goodbye to someone.")]
  struct GoodbyeCommand < GreetCommand
    include Clip::Mapper

    @[Clip::Option("-y", "--yell")]
    @[Clip::Doc("Activate YELLING.")]
    getter yell : Bool? = nil
  end

  @[Clip::Doc("Exit the program.")]
  struct ExitCommand < Command
    include Clip::Mapper
  end

  def self.run
    loop do
      print "> "
      if input = gets
        begin
          command = Command.parse(input)
        rescue ex : Clip::Error
          puts ex
          next
        end

        case command
        when Clip::Mapper::Help
          puts command.help(nil)
        when HelloCommand
          hello(command.name, command.repeat)
        when GoodbyeCommand
          goodbye(command.name, command.yell)
        when ExitCommand
          return
        end
      else
        # we get here if the user types ^D
        puts
        return
      end
    end
  end

  def self.hello(name, repeat)
    repeat.times { puts "Hello, #{name}!" }
  end

  def self.goodbye(name, yell)
    msg = "Goodbye, #{name}!"

    if yell
      puts msg.upcase
    else
      puts msg
    end
  end
end

Myapplication.run

Tip

We could parse the input string using Process.parse_arguments, but the #parse method provides a shortcut and accepts a string. Internally it uses the exact same method from the Process class.

Now we have options and arguments support, beautiful help messages, and error handling!

$ shards build
Dependencies are satisfied
Building: myapplication
$ ./bin/myapplication
> help
Usage: COMMAND [ARGS]...

Greet a person.

Commands:
  hello    Say hello to someone.
  goodbye  Say goodbye to someone.
  exit     Exit the program.
  help     Show this message and exit.
> hello --help
Usage: hello [OPTIONS] NAME

Say hello to someone.

Arguments:
  NAME  The name of the person to greet.  [required]

Options:
  --repeat INTEGER  Repeat the message many times.  [default: 1]
  --help            Show this message and exit.
> goodbye --help
Usage: goodbye [OPTIONS] NAME

Say goodbye to someone.

Arguments:
  NAME  The name of the person to greet.  [required]

Options:
  -y, --yell  Activate YELLING.
  --help      Show this message and exit.
> hello --repeat 2 Alice
Hello, Alice!
Hello, Alice!
> goodbye -y Alice
GOODBYE, ALICE!
> hello -y Alice
Error:
  no such option: -y
> goodbye
Error:
  argument is required: NAME
> exit
$