Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

OCaml  Module 8 – Errors, input/output and command-line programs

Command-line tools with Arg and Cmdliner

Read Sys.argv, parse flags with the Arg module, build a documented word-frequency tool with Cmdliner 2 and its exit codes, and test it with a dune cram test.

  • Intermediate
  • 15 minutes
  • Examples run with ocaml 5.5.1, opam 2.6.0, dune 3.24.2 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: A command line reaches your program as plain strings · The Arg module fills your refs from a list of keywords · What Cmdliner adds to the very same flags · Practise: argv, specs and exit statuses. With a pass: 2 in-depth parts and 3 interview answers, about 10 minutes.

What you will learn

  • Parse flags and anonymous arguments with Sys.argv and the Arg module
  • Build a documented command line from Cmdliner terms
  • Report option and file errors with the exit status each deserves
  • Test a command-line tool end to end with a dune cram test

Before you start

./wordfreq --top 2 --ignore-case notes.txt reaches an OCaml program as five strings in Sys.argv and nothing else: no types, no help text, no sign of which word belongs to which option. Command-line tools with Arg and Cmdliner begin at that array and part company at once. Arg, in the standard library, walks the words against a list of keywords and fills in a few refs. Cmdliner takes typed declarations of the same options and builds the parser, the help page and the documented exit statuses from them.

You will read the argument vector and write the same word-frequency flags both ways. Two parts then go further: a documented wordfreq built from Cmdliner terms and run into each of its four exit statuses, and a dune cram test that records whole command lines with their statuses.

A command line reaches your program as plain strings

The manual describes Sys.argv as the command line arguments given to the process, whose first element is the command name used to invoke the program. Index 0 is therefore the name, a user’s arguments begin at index 1, and the array says nothing about what they mean. The shell has already split the line and taken the quotes off, so a program reading Sys.argv must decide which words are options, which belong to the option before them, and what to do with the rest:

What a program receives: three runs of the same five lines Python · argv_session.py
from session import files, run

files("showargs.ml")
run(
    """
cat showargs.ml
ocamlfind ocamlopt -g showargs.ml -o showargs
./showargs
./showargs --top 3 --ignore-case notes.txt
./showargs ''
"""
)

Output

$ cat showargs.ml
(* Prints the argument vector exactly as the process received it. *)
let () =
  Printf.printf "Sys.argv: %d entries\n" (Array.length Sys.argv);
  Array.iteri (fun i s -> Printf.printf "  %d: %S\n" i s) Sys.argv
$ ocamlfind ocamlopt -g showargs.ml -o showargs
$ ./showargs
Sys.argv: 1 entries
  0: "./showargs"
$ ./showargs --top 3 --ignore-case notes.txt
Sys.argv: 5 entries
  0: "./showargs"
  1: "--top"
  2: "3"
  3: "--ignore-case"
  4: "notes.txt"
$ ./showargs ''
Sys.argv: 2 entries
  0: "./showargs"
  1: ""

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 argv_session.py

With no arguments the array held one string, the program’s own name. With --top 3 --ignore-case notes.txt it held five, and the empty argument '' arrived as an empty string, not as nothing.

The Arg module fills your refs from a list of keywords

Arg.parse specs anon_fun usage_msg walks the words for you. Each entry of specs is a triple of a keyword, a spec that says what to do when that keyword appears, and a one-line doc string. Arg.Set r writes true into a bool ref, and Arg.Set_int r reads the next word as an integer. A word that starts with no dash is a positional argument: An argument a program identifies by where it stands on the command line rather than by a name, such as the file in wordfreq notes.txt. The Arg module calls these anonymous arguments., which Arg.parse hands to anon_fun:

Interactive · stage by stage From five strings to two settings Step through one command line, word by word. The branch on the right is the path a word the spec list does not know takes.
From five strings to two settings, step 7 of 7. Showing The shell splits a line (./argparse -top 2 …), Sys.argv (five strings, 1 to 4 left), Arg.parse reads a word (notes.txt: anonymous), The refs hold values (top = 2, file = notes.txt), Reason, then the usage (unknown option '--top'). In focus: Reason, then the usage.a word no spec knowsThe shell splits a line./argparse -top 2 …Sys.argvfive strings, 1 to 4 leftArg.parse reads a wordnotes.txt: anonymousThe refs hold valuestop = 2, file = notes.txtReason, then the usageunknown option '--top'

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The shell splits. A terminal line such as ./argparse -top 2 -ignore-case notes.txt is cut into words by the shell, which also takes the quotes off. The program never sees the line, only the words.
  2. Sys.argv. The five words arrive as Sys.argv, an array of strings. Index 0 is the name the program was started with, ./argparse, so the arguments begin at index 1.
  3. A keyword and its value. -top starts with a dash, so Arg.parse looks it up in the spec list and finds Set_int top. That spec takes the next word, 2, reads it as an integer and writes 2 into the ref.
  4. A flag takes nothing. -ignore-case is in the spec list as Set, which takes no word of its own: it only writes true into its ref. The walk goes straight on to the next word.
  5. The anonymous argument. notes.txt starts with no dash, so no spec applies. Arg.parse hands it to the function given for anonymous arguments, which stores it as the file to read.
  6. The counting runs. Arg.parse returns, the refs hold every answer, and the counting begins. The recorded run printed 4 chai and 2 and for the three lines of notes.txt.
  7. A word no spec knows. Written GNU style as --top 2, the first word matches no spec. Arg.parse prints the reason, the usage message and the option list to standard error, and ends the process with status 2. Nothing of the counting runs.

The same walk as a program: argparse.ml counts the words of its file, so the flags have something to act on, and its counts check against the word counter:

argparse.ml ML · argparse.ml
(* The same job as the Cmdliner tool, parsed with the Arg module of the standard library. *)
let top = ref 3
let ignore_case = ref false
let file = ref ""

let usage_msg = "argparse [-top N] [-ignore-case] FILE"

let set_file name =
  if !file = "" then file := name
  else raise (Arg.Bad (Printf.sprintf "one file at a time, so %S is too many" name))

let specs =
  [ ("-top", Arg.Set_int top, "N  print the N most frequent words (default 3)");
    ("-ignore-case", Arg.Set ignore_case, " count Chai and chai as one word") ]

let counts words =
  let table = Hashtbl.create 64 in
  List.iter
    (fun w -> Hashtbl.replace table w (1 + Option.value ~default:0 (Hashtbl.find_opt table w)))
    words;
  Hashtbl.fold (fun w n acc -> (w, n) :: acc) table []
  |> List.sort (fun (w1, n1) (w2, n2) -> if n1 = n2 then compare w1 w2 else compare n2 n1)

let () =
  Arg.parse specs set_file usage_msg;
  if !file = "" then (prerr_endline "argparse: a file is needed"; exit 2);
  let text = In_channel.with_open_text !file In_channel.input_all in
  let text = if !ignore_case then String.lowercase_ascii text else text in
  let words =
    String.split_on_char ' ' (String.map (fun c -> if c = '\n' || c = '.' || c = ',' then ' ' else c) text)
    |> List.filter (fun w -> w <> "")
  in
  List.iteri
    (fun i (w, n) -> if i < !top then Printf.printf "%4d  %s\n" n w)
    (counts words)
Two flags, one file, and two command lines Arg refuses Python · argparse_session.py
from session import files, run, write

files("argparse.ml")
write(
    "notes.txt",
    "chai and samosa at the stall.\nChai again, chai with ginger.\nsamosa, chutney and chai.\n",
)
run(
    """
ocamlfind ocamlopt -g argparse.ml -o argparse
./argparse -top 2 -ignore-case notes.txt
./argparse --help
!./argparse -top x notes.txt
!./argparse --top 2 notes.txt
"""
)

Output

$ ocamlfind ocamlopt -g argparse.ml -o argparse
$ ./argparse -top 2 -ignore-case notes.txt
   4  chai
   2  and
$ ./argparse --help
argparse [-top N] [-ignore-case] FILE
  -top N  print the N most frequent words (default 3)
  -ignore-case  count Chai and chai as one word
  -help  Display this list of options
  --help  Display this list of options
$ ./argparse -top x notes.txt
./argparse: wrong argument 'x'; option '-top' expects an integer.
argparse [-top N] [-ignore-case] FILE
  -top N  print the N most frequent words (default 3)
  -ignore-case  count Chai and chai as one word
  -help  Display this list of options
  --help  Display this list of options
[exit status 2]
$ ./argparse --top 2 notes.txt
./argparse: unknown option '--top'.
argparse [-top N] [-ignore-case] FILE
  -top N  print the N most frequent words (default 3)
  -ignore-case  count Chai and chai as one word
  -help  Display this list of options
  --help  Display this list of options
[exit status 2]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 argparse_session.py

Three things in that run matter. Arg.parse answers -help and --help without being asked. On an error it prints the reason, the usage message and the option list to standard error and ends the process with exit status: An integer a program returns to its parent process when it ends, where 0 means success and other values indicate failures. More in “Exceptions and resource cleanup” 2, as the manual’s own example also shows: wrong argument 'x'; option '-top' expects an integer, and unknown option '--top' for a command-line option: A word of a command line that starts with a dash and names a setting rather than data, such as -i or --top; an option that takes a value reads it from the next word or after an equals sign. missing from the spec list. And nothing after the call runs, so a mistyped flag cannot half-finish a job.

One dash or two

A keyword in a spec list is matched exactly as written. ("-top", …) accepts -top 2 and refuses --top 2, which is why the recorded run ends with unknown option '--top'. List both spellings, or use a library where --top and -n name one option.

Checkpoint

Check: What Sys.argv holds · The Arg spec list and its errors

  1. Question 1 of 2 A program is started as ./wordfreq --top 2 notes.txt. What is Array.length Sys.argv?

    Type a number.

    A hint

    Count the program name as well.

    Show the answer: Question 1 of 2

    Answer: 4 strings

    Index 0 is the name the program was started with, ./wordfreq, and the three words the user typed follow it, so the array holds four strings. The recorded run of showargs.ml shows the same count for five words.

  2. Question 2 of 2 The spec list holds ("-top", Arg.Set_int top, …) and ("-ignore-case", Arg.Set ignore_case, …). Which part of ./argparse -top 2 notes.txt reaches the function given for anonymous arguments?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: notes.txt, because it starts with no dash and no spec applies

    -top took the next word as its integer, and notes.txt started with no dash, so it went to the anonymous function, which stored it as the file to read. The run printed 4 chai and 2 and from that file.

What Cmdliner adds to the very same flags

Cmdliner is a library that turns typed declarations of your options into a parser, a man page and documented exit statuses; the OCaml.org tutorial names it as a modern interface that also generates UNIX man pages automatically, and the version installed here is 2.1.1. One option becomes one value: Arg.(value & opt int 3 & info ["n"; "top"] ~doc ~docv:"N") evaluates to an int, has the names -n and --top, defaults to 3, and carries its documentation in the labelled argument: An argument passed by name, written ~name:value at the call. Labelled arguments of a complete call may come in any order. More in “Labelled and optional arguments” ~doc and ~docv. Command-line tools with Arg and Cmdliner ask the same questions of a line, and answer them with very different amounts of your code:

The Arg module and Cmdliner on the same four flags
Criterion ArgCmdliner
Where it comes from The standard libraryThe cmdliner package, 2.1.1 here
How an option is declared A keyword, a spec, a doc stringA typed term with its names and default
Where the value lands In a ref you read afterwardsIn an argument of your function
Option spellings Exactly the keyword you wrote-n and --top are names of one option
Help Usage line and option listA generated man page, --help=plain for text
A bad value Reason and usage, exit status 2Usage line and reason, exit status 124
Exit statuses Yours to choose and document0, 123, 124 and 125, listed in the help
When to choose A script with two or three flagsA tool other people run and script

Two things follow. A documented Cmdliner command is three argument terms applied to one function, plus the information that becomes its help: the first part below builds wordfreq that way and runs it into each of its four exit statuses. A cram test: A test that records a shell session, each command with the output and exit status it should produce, in a file whose name ends in .t; dune runs the commands and compares what they print with what the file holds. records commands, their output and their exit codes in a .t file that dune: OCaml's build system. It reads dune files that describe libraries, executables and tests, calls the compilers in the right order and keeps every result in _build. More in “Your first dune project and editor setup” replays: the second part tests the finished tool with one.

Checkpoint

Check: What one Cmdliner declaration carries

  1. One question wordfreq declares its option as Arg.(value & opt int 3 & info ["n"; "top"]). Which command lines set the number to 2?

    Choose one answer.

    Show the answer: One question

    Answer: -n 2, --top 2 and --top=2 all do

    A one-character name becomes the short option -n and a longer one the long option --top, both from the same declaration, and the value of a long option may follow a space or an equals sign. The recorded runs show --top 2 --ignore-case, -i -n 2 and --top=1 giving the same kind of answer.

Practise: argv, specs and exit statuses

The questions use the two recorded runs above and the status each library ends with.

Practice that adapts to you

Practise this lesson

9 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 What does Sys.argv hold at index 0?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: The command name used to start the program

    Sys.argv holds the words the process was started with, index 0 being the command name. The recorded run started the program as ./showargs and printed "0: ./showargs".

  2. Question 2 of 9 A program is started as ./showargs '' with an empty quoted argument. What does Sys.argv hold?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: Two strings: the name and an empty string

    The run of ./showargs '' printed 2 entries and 1: "". An empty argument is a word like any other, which is why a tool that treats an empty file name as "no file given" has to test for it.

  3. Question 3 of 9 A tool must accept a file name that contains a space. Which part of the system decides whether that name arrives as one entry of Sys.argv or two?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: The shell, when it splits the line into words

    The shell splits the line and takes the quotes off, so "my notes.txt" arrives as one entry and my notes.txt as two. A tool can only report the words it received, which is why file names with spaces are a user-side quoting question.

  4. Question 4 of 9 Which options does Arg.parse answer without an entry in the spec list?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: -help and --help, both printing the usage message and the option list

    The recorded run of ./argparse --help printed the usage message, the two flags and then -help and --help, which Arg adds itself. Listing your own -help entry overrides that behaviour.

  5. Question 5 of 9 With what exit status does an Arg.parse program end when it is given an option that is not in the spec list?

    Type a number.

    A hint

    The recorded run prints the status after the usage message.

    Show the answer: Question 5 of 9

    Answer: 2

    ./argparse --top 2 notes.txt printed "unknown option '--top'", the usage message and the option list to standard error, then ended with status 2. The same status appears in the OCaml.org tutorial's own run, as [2].

  6. Question 6 of 9 A tool parsed with Arg has ("-output", Arg.Set_string out, …) and users keep typing --output report.txt. What happens, and what is the smallest honest fix?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: It stops with unknown option and status 2; add a second spec entry for --output

    Keywords are matched literally, so --output is unknown and the program ends with status 2. Adding ("--output", Arg.Set_string out, …) beside the first entry accepts both spellings; a library that holds every name of an option in one declaration avoids the duplication.

  7. Question 7 of 9 What does Arg.(value & opt int 3 & info ["n"; "top"] ~doc ~docv:"N") carry, besides the value itself?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: Both names of the option, the type to read it with, the default and the documentation

    A Cmdliner option is declared once, with every name, its converter, its default and its documentation, and the value reaches your function as an argument instead of through a ref.

  8. Question 8 of 9 A Cmdliner tool is given --top x where an integer is expected. Which exit status does it end with?

    Type a number.

    A hint

    It is the status for a command line the library could not parse.

    Show the answer: Question 8 of 9

    Answer: 124

    The recorded run printed a usage line and "option '--top': invalid value 'x', expected an integer", then ended with 124, which Cmdliner names Cmd.Exit.cli_error and lists in the generated help under EXIT STATUS.

  9. Question 9 of 9 A one-file script takes a single -v flag and runs only on your own machine. Which parser fits, and why?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: The Arg module, because a spec list of one entry needs no dependency and no generated man page

    Choose by who runs the tool. Arg suits a script with two or three flags and no dependency; Cmdliner earns its keep when other people run the tool, read its help and script against its exit statuses.

A documented wordfreq, from three Cmdliner terms to four exit statuses Pro: opens with a Pro, Premium or Ultimate pass

A Cmdliner tool is a function and one declaration per argument, joined by Term.( $ ) and described by Cmd.info. This part builds wordfreq that way, reads the help it generates, and runs it into each of the four statuses it documents.

In this part:

  • Three terms, one function, and the information that makes the help
  • The generated help and the four statuses it documents
  • Where the parser stops you, and where your own code must

1 diagram · 5 runnable examples (ML, Python) · about 9 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Testing the whole command line with a dune cram test Pro: opens with a Pro, Premium or Ultimate pass

A unit test calls your functions; a cram test runs your tool. It records commands with their output and their exit codes, so the help text, the error messages and the statuses are all under test, exactly as a user meets them.

In this part:

  • What a cram file records, down to the exit-code line
  • A change, the diff dune test prints, and dune promote

2 runnable examples (Python) · about 5 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

Warm-up (fresher to 2–5 years): what does Arg.parse do when it meets an option that is not in the spec list?

It stops the program. Arg.parse prints three things to standard error, in this order: the reason, such as unknown option '--top'; the usage message you passed it; and the list of options with their doc strings, including the -help and --help it adds itself. Then it ends the process with exit status 2, which the OCaml.org tutorial’s own run shows as [2]. Nothing after the call runs, so a mistyped option cannot start a job and abandon it half done. An option with an empty doc string is left out of that list.

Three harder questions follow: a script that cannot tell a user’s mistake from a crash, a help text that has drifted from the behaviour, and a flag that must stay usable after a rename. Decide your first move, then open each question’s first step:

  1. Mid to senior

    A nightly job runs your OCaml tool and retries it three times on any non-zero status. The tool exits 1 for a missing input file, for a bad option and for an uncaught exception alike, so every user mistake is retried for half an hour. Design the statuses, and say how the job should read them.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A tool's usage message, written by hand two years ago, offers --out FILE and a default of 10 rows. The parser actually takes -o and defaults to 25, and users keep reporting it as a bug. Explain how the two drifted apart and how you would make that impossible.

    Model answer, Pro. First step and follow-ups

  3. Mid to staff

    You want to rename --ignore-case to --fold-case in a tool that other teams call from their own scripts. Nobody knows how many scripts use the old name. Plan the rename so that no script breaks on the day you ship it, and say how it ends.

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How does a command line reach an OCaml program?

    As Sys.argv, an array of plain strings: index 0 is the program's own name, and the later entries mean nothing until your code gives them a meaning.

  2. What does Arg.parse specs anon_fun usage_msg take?

    A spec list of keyword, spec and doc string, such as an Arg.Set_int that fills a ref, a function for anonymous arguments, and a usage message. It answers -help and --help itself.

  3. What does Arg.parse do when it meets an option missing from its spec list?

    It stops the program with status 2 after printing, to standard error, the reason (such as unknown option '--top'), the usage message and the list of options.

  4. Why does a spec entry ("-top", …) refuse --top 2?

    An Arg keyword is matched exactly as written. List both spellings, or use a library where --top and -n can name one option.

  5. What does one Cmdliner declaration of an option carry?

    Every name of the option, its type, its default and its documentation; Cmdliner builds the parser, the man page and the documented exit statuses from such declarations.

Key takeaways

  • A command line reaches an OCaml program as Sys.argv, an array whose index 0 is the program’s own name and whose later entries mean nothing until your code gives them one.
  • Arg.parse takes a spec list of keyword, spec and doc string plus a function for anonymous arguments, answers -help itself, and ends with status 2 after printing the reason and the usage to standard error.
  • An Arg keyword matches exactly as written, so -top refuses --top, while one Cmdliner declaration carries every name of an option, its type, its default and its documentation.
  • Cmdliner builds the parser, the man page and the documented exit statuses from those declarations, and a cram test replays whole command lines and compares their output and statuses.

Terms in this lesson

command-line option
A word of a command line that starts with a dash and names a setting rather than data, such as -i or --top; an option that takes a value reads it from the next word or after an equals sign.
cram test
A test that records a shell session, each command with the output and exit status it should produce, in a file whose name ends in .t; dune runs the commands and compares what they print with what the file holds.
positional argument
An argument a program identifies by where it stands on the command line rather than by a name, such as the file in wordfreq notes.txt. The Arg module calls these anonymous arguments.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.