First steps¶
Let's do a hello world like app. We change src/myapplication.cr
to:
require "clip"
module Myapplication
VERSION = "0.1.0"
struct Command
include Clip::Mapper
getter name : String
end
def self.run
begin
command = Command.parse
rescue ex : Clip::Error
puts ex
return
end
if command.is_a?(Clip::Mapper::Help)
puts command.help
else
hello(command.name)
end
end
def self.hello(name)
puts "Hello #{name}"
end
end
Myapplication.run
We can build and run the app, it works as expected:
$ shards build
Dependencies are satisfied
Building: myapplication
$ ./bin/myapplication --help
Usage: ./bin/myapplication [OPTIONS] NAME
Arguments:
NAME [required]
Options:
--help Show this message and exit.
$ ./bin/myapplication Alice
Hello Alice
$ ./bin/myapplication
Error:
argument is required: NAME
We will now look at every block of code.
Mapping a type¶
struct Command
include Clip::Mapper
getter name : String
end
Clip works by including Clip::Mapper
inside a user defined type, a class or a struct.
When the module is included, a macro is executed and generates a constructor, the method #parse
we used, and the help message.
It does that by analysing the type's attributes to find options and arguments.
We say it maps the type definition to the expected CLI parameters, hence the name "Mapper".
The advantage is that we have a classic type definition, which guarantees us type validation at compilation time.
Here Clip detected that we need one argument named NAME
.
Tip
A class is allocated on the heap and passed by reference while a struct is allocated on the stack and passed by value. Hence structs are better suited for read only object.
In this tutorial we will only use structs as we will never need to edit the object, but you can use classes as well.
Parsing the user input¶
begin
command = Command.parse
rescue ex : Clip::Error
puts ex
return
end
As said before, the macro creates a #parse
method.
It accepts an array of strings, and defaults to ARGV
.
This method acts as a constructor. It tries to parse the input, and on success it returns a new object.
But there are two other cases:
- on user input failure, it raises an error
- if the user uses the special flag
--help
, it returns a specialHelp
object
Catching user input error¶
begin
command = Command.parse
rescue ex : Clip::Error
puts ex
return
end
If the user makes a mistake, by example if he uses an option not defined or does not set a required argument, the #parse
method raises an exception.
The exceptions raised by #parse
have two properties:
- they always inherits from
Clip::Error
- their message is a preformatted error message
This means that you can just rescue Clip::Error
and puts the rescued exception to show the user a nice error message, as we just did.
We will see later in this tutorial exactly what exceptions can be raised and how you can use them to format your own error messages.
The help case¶
if command.is_a?(Clip::Mapper::Help)
puts command.help
else
hello(command.name)
end
The help option is a special case that needs a special treatment.
When the user set --help
, he does not expect to get an error because NAME
is required and was not provided.
But NAME
is indeed required, and we cannot return a Command
instance as the Crystal compiler will complain that @name
was not initialized (and we don't want to initialize it with a random or arbitrary value).
The choice made by Clip is to return a instance of the special type Command::Help
.
This type was generated when including the Clip::Mapper
module and has two properties:
- it inherits from
Clip::Mapper::Help
- it provide a method
#help
that returns the generated help message
That why we do this first check using #is_a?
: to print the help if requested by the user.
Accessing the user input¶
if command.is_a?(Clip::Mapper::Help)
puts command.help
else
hello(command.name)
end
Finally we use the user input wich was filled into the object attributes.
As we are in the else
clause of if command.is_a?(Clip::Mapper::Help)
the compiler knows that our object is an instance of Command
, and we can use its attribute name
.
Options, arguments and parameters¶
So far we used those tree words.
We said that NAME
is an argument, that --help
is an option, and we mentioned CLI parameters. But what are they?
Note
All that follows is just conventions.
They are more often than not respected, but some command may have different conventions, like tar
which accepts options without hypens.
CLI Parameters¶
A CLI parameter or just parameter is anything given after the executable's name when executing a command. So in the command ls -lh /tmp
, the parameters are -lh
and /tmp
.
Options¶
An option is a specific type of parameter that must be named. It can have a value or not, it can be long or short, and if short it can be concatenated.
$ somecommand --name=Alice -lg --file somefile
In this fictitious command there are 4 options:
--name
, with a valueAlice
-l
, without value-g
, without value--file
, with a valuesomefile
Arguments¶
An argument is a specific type of parameter that have a value but is not be named.
In our command myapplication
the usage says: ./bin/myapplication [OPTIONS] NAME
. NAME
is an argument, so we don't write ./bin/myapplication --name Alice
but instead ./bin/myapplication Alice
.