I just wrote my first Reason project that compiled to native, and you can too! Luckily for you, my first native project was a cli tool to help people get started with native Reason development 🙌.
If you’re more interested in targeting JavaScript, I wrote two tutorials about that to get you started.
What will we be doing?
- project setup
- building & running
- parsing cli arguments
Project Setup
First, you’ll need to install OCaml on your machine if you don’t have it already. brew install ocaml
on macos, apt-get install ocaml
on debian/ubuntu, with instructions for more platforms here.
Then we’ll install ohai, the tool I made for configuring a new native reason project. You can think of it as the npm init
or cargo new
for Reason/OCaml.
opam pin add ohai git+https://github.com/jaredly/ohai#1.0.1
Finally, let’s setup the project!
ohai init --bin my_cli
You’ll see that a bunch of files have been created for you - go ahead and take a look in each to see what’s there.
~$ tree
.
├── Makefile
├── bin
│ ├── jbuild
│ └── my_cli.re
├── jbuild-workspace.dev
├── lib
│ ├── Main.re
│ └── jbuild
├── test
│ ├── jbuild
│ └── test.re
└── my_cli.opam
So we’ve got 3 directories - lib
is where our functionality lives, bin
just has a single top-level call into lib/Main.re
, and test
will have any tests we write.
Building & Running
If you run make run
it will build your project and run the built executable.
~$ make run
jbuilder build @install
refmt bin/my_cli.re.ml
ocamldep bin/my_cli.depends.ocamldep-output
ocamlc lib/lib.{cmi,cmo,cmt}
refmt lib/Main.re.ml
ocamldep lib/lib.depends.ocamldep-output
ocamlc lib/lib__Main.{cmi,cmo,cmt}
ocamlc bin/my_cli.{cmi,cmo,cmt}
ocamlopt lib/lib.{cmx,o}
ocamlopt lib/lib__Main.{cmx,o}
ocamlopt lib/lib.{a,cmxa}
ocamlopt bin/my_cli.{cmx,o}
ocamlopt bin/my_cli.exe
jbuilder exec my_cli
Hello world
As you can see, the makefile first ran jbuilder build @install
, which means “build everything you know about”, and then jbuilder exec my_cli
. The latter is shorthand for ./_build/default/bin/my_cli.exe
.
Parsing CLI Arguments
If you haven’t already, look through the docs for Reason for a primer on the syntax. The documentation for the OCaml standard library can also be useful.
Echoing Sys.argv
The default Main.re
is very simple, and doesn’t do anything with arguments.
let run () => {
print_endline "Hello world";
};
let add2 x => x + 2;
To start out let’s just print out all received arguments:
let run() => {
print_endline (String.concat ", " (Array.to_list Sys.argv));
};
Docs for
Sys.argv
are here
Now, we rebuild with make
, and this time we’ll run the executable with some extra arguments:
~$ jbuilder exec my_cli -- -an-argument another argument
my_cli, -an-argument, another, argument
Sys.argv
is an array
(fixed-length, mutable) of strings, starting with the name of the program. We wanted to display them, and the simplest way was to convert it to a list so that we could String.concat
them all together into a single string.
Reacting to arguments
Let’s do something more intersting in response to arguments. If you run my_cli beep
we’ll respond with “boop”, and if you type my_cli cowsay
followed by any text we’ll mimic that beloved utility.
let run() => {
/**
* Let's convert the args to a list again, as lists
* are generally nicer to work with in Reason,
* especially when we want to "do something with the
* first one, and then deal with the rest".
* Arrays are better if we want mutation or random access.
*/
let arglist = Array.to_list Sys.argv;
switch arglist {
/**
* This initial "_" is a placeholder, meaning
* "we don't care what's first in the list".
* Sys.argv's first item is the name of the
* executable, which we don't care about.
*/
| [_, "beep"] => print_endline "boop"
| [_, "cowsay", ...rest] => cowsay (String.concat " " rest)
/**
* This is the catchall case - any unrecognized
* invocation will be met with our help text.
*/
| _ => print_endline help_text;
}
};
Now what does cowsay look like? We’ll do a simplified version that puts everything on one line.
/* A multiline string! */
let cow = {|
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
|};
let cowsay text => {
// `^` is the way to join strings together
let message = " ( " ^ text ^ " )" ^ cow;
// We could also do
// Printf.printf " ( %s ) %s" text cow;
print_endline message;
};
In OCaml, unlike rust, javascript, golang, java, etc., all declarations must go before usage, even at the top level. This means that we’ll put
help_text
andcowsay()
above ourrun()
function in the final code.
Come up with some helpful text, and we’re all set!
let help_text = {|my_cli - a cli for all your needs!
Usage:
- my_cli beep
- my_cli cowsay some text here
|};
let cow = {|
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
|};
let cowsay text => {
let message = " ( " ^ text ^ " )" ^ cow;
print_endline message;
};
let run() => {
let arglist = Array.to_list Sys.argv;
switch arglist {
| [_, "beep"] => print_endline "boop"
| [_, "cowsay", ...rest] => cowsay (String.concat " " rest)
| _ => print_endline help_text;
}
};
We’re done!
~$ make
...
~$ ./_build/default/bin/my_cli.exe
my_cli - a cli for all your needs!
Usage:
- my_cli beep
- my_cli cowsay some text here
~$ ./_build/default/bin/my_cli.exe beep
boop
~$ ./_build/default/bin/my_cli.exe cowsay reason is awesome
( reason is awesome )
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
What’s next?
The source code for ohai
is pretty well documented, if you’re interested in looking at something a little more complicated.
I’m also planning on writing a tutorial on making a simple web server with cohttp pretty soon.
Join us in our Discord channel, and catch me on twitter with your comments!