Skip to content

gridbugs/climate

Repository files navigation

Climate

test status

A declarative command-line parser for OCaml.

Rationale

Programs written in OCaml should conform to existing UX conventions so as to match the expectations of users coming from other tools. For command-line programs which wish to parse their arguments in a declarative style, existing solutions all seem to deviate from the conventions established by common Unix tools. The two popular libraries for declarative command-line argument parsing in OCaml arecmdliner and base's Command module. Both of these libraries present unconventional behaviour in that non-ambiguous prefixes of arguments are treated as the full argument names. Additionally, cmdliner lacks support for generating shell autocompletion scripts, and base only supports arguments beginning with a single -.

This library aims to be an alternative to cmdliner and Base.Command with support for generating autocompletion scripts and which behaves as conventionally as possible.

Example

Here's a complete example program with built-in support for generating its own completion script.

open Climate

module Color = struct
  type t =
    | Red
    | Green
    | Blue

  (* Tell climate how to handle colours *)
  let conv =
    let open Arg_parser in
    enum ~default_value_name:"COLOR" [ "red", Red; "green", Green; "blue", Blue ]
  ;;
end

(* Ansi escape sequence to reset the termminal style *)
let ansi_reset = "\x1b[0m"

(* Returns the escape sequence to set the terminal style *)
let ansi_style ~bold ~underline ~color =
  let effects =
    List.append (if bold then [ ";1" ] else []) (if underline then [ ";4" ] else [])
  in
  let color_code =
    match (color : Color.t option) with
    | None -> 0
    | Some Red -> 31
    | Some Green -> 32
    | Some Blue -> 34
  in
  Printf.sprintf "\x1b[%d%sm" color_code (String.concat "" effects)
;;

(* Print the words in the given style *)
let main ~bold ~underline ~color words =
  print_string (ansi_style ~bold ~underline ~color);
  print_string (String.concat " " words);
  print_string ansi_reset;
  print_newline ()
;;

let () =
  let command =
    Command.singleton ~desc:"Echo with style!"
    @@
    let open Arg_parser in
    (* Describe and parse the command line arguments:*)
    let+ bold = flag [ "bold" ] ~desc:"Make the text bold"
    and+ underline = flag [ "underline" ] ~desc:"Underline the text"
    and+ color = named_opt [ "color" ] Color.conv ~desc:"Set the text color"
    and+ words = pos_all string
    and+ completion =
      flag [ "completion" ] ~desc:"Print this program's completion script and exit"
    in
    if completion
    then `Completion
    else `Main (fun () -> main ~bold ~underline ~color words)
  in
  (* Run the parser yielding either a main function to call or an indication
     that we should print the completion script. *)
  match Command.run command with
  | `Completion -> print_endline (Command.completion_script_bash command)
  | `Main main -> main ()
;

This program lives in examples/echo_ansi.ml. Run it with dune exec examples/echo_ansi.exe -- <ARGS>. E.g.

$ dune exec examples/echo_ansi.exe -- --help
Usage: _build/default/examples/echo_ansi.exe [OPTIONS] [STRING]...

Echo with style!

Options:
 --bold   Make the text bold
 --underline   Underline the text
 --color <COLOR>   Set the text color
 --completion   Print this program's completion script and exit
 --help, -h   Print help

The easiest way to setup the completion script is to first put the executable in a directory in your PATH. E.g.

$ mkdir -p ~/bin
$ export PATH=$HOME/bin:$PATH
$ dune build
$ cp _build/default/examples/echo_ansi.exe ~/bin/echo_ansi

You can ask echo_ansi to print out its own completion script with:

$ echo_ansi --completion
#!/usr/bin/env bash
# Completion script for echo_ansi. Generated by climate.

__climate_complete_1184462387__complete() {
    __climate_complete_1184462387__comp_words_traverse_init
    if [ "$COMP_CWORD" == "0" ]; then
...

Put the completion script in a file and then source that file in your shell to enable completions for echo_ansi:

$ echo_ansi --completion > /tmp/echo_ansi_completions.sh
$ source /tmp/echo_ansi_completions.sh
$ echo_ansi --color <TAB>
blue   green  red

Currently only bash is supported, though it also works in zsh if bash compatibility is enabled:

# put this in your ~/.zshrc
autoload -Uz compinit bashcompinit
compinit
bashcompinit

Manual

Terminology

Term will refer to each space-delimited string on the command line after the program name. The command ls -l --color=always /etc/ has 3 terms. The program name is ls (not a term), and the terms are -l, --color=always, and /etc/.

Argument will refer to each distinct piece of information passed to the program on the command line. The command make -td --jobs 4 all has 4 arguments. The -td term is made up of two arguments combined into a single term: -t and -d (more on this later). --jobs 4 is a single argument comprising two terms, where 4 is a parameter to the argument --jobs. The final term all is also an argument.

Arguments may be positional or named. Positional arguments are identified by their position in the argument list rather than by name. Named arguments may have two forms: short and long. Short named arguments begin with a single - followed by a single non - character, such as -l. Long named arguments begin with -- followed by one or more non - characters, such as --jobs. A collection of short named arguments may be combined together with a single leading - followed by each short argument name. For example in ls -la, the -la is an alternative way of writing -l -a.

A named argument may take a parameter. A parameter is a single value which follows the argument on the command line. Using make's --jobs argument as an example, here are the different ways of passing a parameter to a named argument on the command line:

make --jobs=4   # long name with equals sign
make --jobs 4   # long name space delimited
make -j 4       # short name space delimited
make -j4        # short name without space

If multiple short arguments are combined into a single term then only one of those arguments may take a parameter. If the parameterized argument appears as the final argument in the sequence then the following term will be treated as its parameter, such as in make -dj 4, which is equivalent to make -d -j 4. If the parameterized argument appears in a non-final position within the sequence then the remainder of the sequence is treated as its parameter, such as in make -dj4 which is also equivalent to make -d -j 4.