├── .circleci ├── config.yml └── setup ├── .gitignore ├── .ocp-indent ├── LICENSE ├── Makefile ├── README.md ├── cmdliner-cheatsheet.opam ├── dune-project └── src ├── Demo_arg_main.ml ├── Demo_subcmd_main.ml └── dune /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Circle CI configuration. Runs each time we push a new commit to Github. 3 | # 4 | version: 2.1 5 | 6 | jobs: 7 | build: 8 | docker: 9 | - image: mjambon/mj-ocaml:ubuntu 10 | working_directory: ~/cmdliner-cheatsheet 11 | steps: 12 | - checkout 13 | - run: 14 | name: Install dependencies 15 | command: ./.circleci/setup 16 | - run: 17 | name: Build 18 | command: opam exec -- make 19 | - run: 20 | name: Test 21 | command: opam exec -- make test 22 | 23 | workflows: 24 | version: 2 25 | build: 26 | jobs: 27 | - build 28 | -------------------------------------------------------------------------------- /.circleci/setup: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # Setup script for building and testing the project. 4 | # Assumes opam is installed. 5 | # 6 | set -eu 7 | 8 | eval $(opam env) 9 | opam update 10 | opam install -y dune cmdliner 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | bin 3 | -------------------------------------------------------------------------------- /.ocp-indent: -------------------------------------------------------------------------------- 1 | # See https://github.com/OCamlPro/ocp-indent/blob/master/.ocp-indent for more 2 | 3 | # Indent for clauses inside a pattern-match (after the arrow): 4 | # match foo with 5 | # | _ -> 6 | # ^^^^bar 7 | # the default is 2, which aligns the pattern and the expression 8 | match_clause = 4 9 | 10 | # When nesting expressions on the same line, their indentation are in 11 | # some cases stacked, so that it remains correct if you close them one 12 | # at a line. This may lead to large indents in complex code though, so 13 | # this parameter can be used to set a maximum value. Note that it only 14 | # affects indentation after function arrows and opening parens at end 15 | # of line. 16 | # 17 | # for example (left: `none`; right: `4`) 18 | # let f = g (h (i (fun x -> # let f = g (h (i (fun x -> 19 | # x) # x) 20 | # ) # ) 21 | # ) # ) 22 | max_indent = 2 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # This makefile is for building the sample code and make sure it works 3 | # (well, compiles). 4 | # 5 | 6 | .PHONY: build 7 | build: 8 | dune build @install 9 | test -e bin || ln -s _build/install/default/bin 10 | 11 | .PHONY: test 12 | test: 13 | ./bin/cmdliner-demo-arg . foo -j 99 bar --user-name mj 14 | ./bin/cmdliner-demo-subcmd subcmd1 15 | ./bin/cmdliner-demo-subcmd subcmd2 --bar 16 | 17 | .PHONY: install 18 | install: 19 | dune install 20 | 21 | .PHONY: clean 22 | clean: 23 | git clean -dfX 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cmdliner cheatsheet [![CircleCI badge](https://circleci.com/gh/mjambon/cmdliner-cheatsheet.svg?style=svg)](https://app.circleci.com/pipelines/github/mjambon/cmdliner-cheatsheet) 2 | == 3 | 4 | [Cmdliner](https://erratique.ch/software/cmdliner) is a 5 | feature-complete library for parsing the command line from an OCaml 6 | program. The output of `--help` is similar to a man page, see for 7 | example `opam --help`. 8 | Cmdliner is to be used instead of the `Arg` module from the standard library. 9 | 10 | This cheatsheet is a compact reference for common patterns. 11 | 12 | [Two sample programs](src) are provided for exploration 13 | purposes or as templates for new projects. Clone this git repository 14 | and test it as follows (requires `dune` and of course `cmdliner`): 15 | ``` 16 | $ make 17 | $ make test 18 | 19 | $ ./bin/cmdliner-demo-arg --help 20 | $ ./bin/cmdliner-demo-arg # try with some options 21 | $ ./bin/cmdliner-demo-subcmd 22 | $ ./bin/cmdliner-demo-subcmd subcmd1 # try with some options 23 | ``` 24 | 25 | Anonymous argument at specific position 26 | -- 27 | 28 | ``` 29 | $ foo 42 -j 8 data.csv -> Some "data.csv" 30 | ^^ ^^^^^^^^ 31 | 0 1 32 | ``` 33 | 34 | Optional argument at a specific position: 35 | ```ocaml 36 | let input_file_term = 37 | let info = 38 | Arg.info [] (* list must be empty for anonymous arguments *) 39 | ~doc:"Example of an anonymous argument at a fixed position" 40 | in 41 | Arg.value (Arg.pos 1 (Arg.some Arg.file) None info) 42 | ``` 43 | 44 | Required argument at a specific position: 45 | ```ocaml 46 | let input_file_term = 47 | let info = 48 | Arg.info [] (* list must be empty for anonymous arguments *) 49 | ~doc:"Example of an anonymous argument at a fixed position" 50 | in 51 | Arg.required (Arg.pos 1 (Arg.some Arg.file) None info) 52 | ``` 53 | 54 | Any number of anonymous arguments 55 | -- 56 | 57 | ``` 58 | $ foo -> [] 59 | $ foo a.csv b.csv c.csv -> ["a.csv"; "b.csv"; "c.csv"] 60 | ``` 61 | 62 | ```ocaml 63 | Arg.value (Arg.pos_all Arg.file [] info) 64 | ``` 65 | 66 | Any number of anonymous arguments, defaulting to non-empty list 67 | -- 68 | 69 | ``` 70 | $ foo -> ["."] 71 | $ foo a.csv b.csv c.csv -> ["a.csv"; "b.csv"; "c.csv"] 72 | ``` 73 | 74 | ```ocaml 75 | let default = ["."] in 76 | Arg.value (Arg.pos_all Arg.file default info) 77 | ``` 78 | 79 | Simple flag 80 | -- 81 | 82 | ``` 83 | $ foo -> false 84 | $ foo --no-exe -> true 85 | ``` 86 | 87 | ```ocaml 88 | let no_exe_term = 89 | let info = 90 | Arg.info ["no-exe"] 91 | ~doc:"Example of a flag, which sets a boolean to true." 92 | in 93 | Arg.value (Arg.flag info) 94 | ``` 95 | 96 | Optional argument specification 97 | -- 98 | 99 | ``` 100 | $ foo -> 1 101 | $ foo -j 8 -> 8 102 | $ foo --num-cores 8 -> 8 103 | ``` 104 | 105 | ```ocaml 106 | let num_cores_term = 107 | let default = 1 in 108 | let info = 109 | Arg.info ["j"; "num-cores"] (* '-j' and '--num-cores' will be synonyms *) 110 | ~docv:"NUM" 111 | ~doc:"Example of an optional argument with a default. 112 | The value of \\$\\(docv\\) is $(docv)." 113 | in 114 | Arg.value (Arg.opt Arg.int default info) 115 | ``` 116 | 117 | Optional value without a default 118 | -- 119 | 120 | ``` 121 | $ foo -> None 122 | $ foo --bar thing -> Some "thing" 123 | ``` 124 | 125 | ```ocaml 126 | Arg.value (Arg.opt (Arg.some Arg.string) None info) 127 | ``` 128 | 129 | Optional value with a default 130 | -- 131 | 132 | ``` 133 | $ foo -> 1 134 | $ foo --bar 8 -> 8 135 | ``` 136 | 137 | ```ocaml 138 | let default = 1 in 139 | Arg.value (Arg.opt Arg.int default info) 140 | ``` 141 | 142 | Predefined argument converters 143 | -- 144 | 145 | ``` 146 | Raw argument OCaml value Converter 147 | 148 | abc "abc" Arg.string 149 | true true Arg.bool 150 | false false Arg.bool 151 | x 'x' Arg.char 152 | % '%' Arg.char 153 | 123 123 Arg.int 154 | 123 123. Arg.float 155 | 123.4 123.4 Arg.float 156 | -1.2e6 -1.2e6 Arg.float 157 | 123 123l Arg.int32 158 | 123 123L Arg.int64 159 | foo Foo Arg.enum ["foo", Foo; "bar", Bar] 160 | data.csv "data.csv" Arg.file (* path must exist *) 161 | data/ "data/" Arg.file (* path must exist *) 162 | data/ "data/" Arg.dir (* must be a folder *) 163 | data.csv "data.csv" Arg.non_dir_file (* file must exist *) 164 | foo.sock "foo.sock" Arg.non_dir_file (* file must exist *) 165 | ab,c,d ["ab"; "c"; "d"] Arg.list Arg.string 166 | 17,42 [|17; 42|] Arg.array Arg.int 167 | ab:c:d ["ab"; "c"; "d"] Arg.list ~sep:':' Arg.string 168 | ``` 169 | 170 | Subcommands 171 | -- 172 | 173 | ``` 174 | $ foo # displays root help page 175 | $ foo --help # also displays root help page 176 | $ foo subcmd1 # returns 'Subcmd1 { ... }' 177 | $ foo subcmd2 --bar # returns 'Subcmd2 { ... }' 178 | ``` 179 | 180 | Each subcommand is defined as if it were its own command. They are 181 | then combined into one. See demo in [src](src). 182 | 183 | ```ocaml 184 | ... 185 | 186 | type cmd_conf = 187 | | Subcmd1 of subcmd1_conf 188 | | Subcmd2 of subcmd2_conf 189 | 190 | ... 191 | 192 | let subcmd1_info = 193 | Term.info "subcmd1" 194 | ~doc:subcmd1_doc 195 | ~man:subcmd1_man 196 | 197 | ... 198 | 199 | let subcmd1 = (subcmd1_term, subcmd1_info) 200 | let subcmd2 = (subcmd2_term, subcmd2_info) 201 | 202 | let root_term = Term.ret (Term.const (`Help (`Pager, None))) 203 | let root_subcommand = (root_term, root_info) 204 | 205 | let parse_command_line () : cmd_conf = 206 | match Term.eval_choice root_subcommand [subcmd1; subcmd2] with 207 | | `Error _ -> exit 1 208 | | `Version | `Help -> exit 0 209 | | `Ok conf -> conf 210 | ``` 211 | 212 | Upgrade to cmdliner 1.1.x 213 | -- 214 | 215 | ``` 216 | Error (alert deprecated): Cmdliner.Term.info 217 | Use Cmd.info instead. 218 | ``` 219 | 220 | ``` 221 | Error (alert deprecated): Cmdliner.Term.eval 222 | Use Cmd.v and one of Cmd.eval* instead. 223 | ``` 224 | 225 | ``` 226 | Error (alert deprecated): Cmdliner.Term.eval_choice 227 | Use Cmd.group and one of Cmd.eval* instead. 228 | ``` 229 | 230 | If you're getting the errors/warnings above after having upgraded 231 | cmdliner to version >= 1.1.x, check out 232 | [this diff](https://github.com/mjambon/cmdliner-cheatsheet/commit/8bfd6a87c57cc1445e5d338ef40711a9782ba524) 233 | showing a way to migrate. The main `run` function that contains the 234 | business logic of your application is now passed around as an 235 | argument, which isn't great. 236 | [Let us know](https://github.com/mjambon/cmdliner-cheatsheet/issues) 237 | if you know an easier way to separate command-line parsing 238 | from the rest of the application. 239 | 240 | Alternatively, the deprecation notice can be silenced by placing 241 | [this floating attribute](https://ocaml.org/manual/alerts.html) 242 | at the beginning of the file: 243 | 244 | ```ocaml 245 | [@@@alert "-deprecated"] 246 | ``` 247 | 248 | Conclusion 249 | -- 250 | 251 | Cmdliner offers various advanced and useful features that are not 252 | covered here. Please consult the [official 253 | documentation](https://erratique.ch/software/cmdliner/doc/Cmdliner.html). 254 | -------------------------------------------------------------------------------- /cmdliner-cheatsheet.opam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjambon/cmdliner-cheatsheet/f799c3a02fceee6d4e4aea6250f6c7334554e612/cmdliner-cheatsheet.opam -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.7) 2 | -------------------------------------------------------------------------------- /src/Demo_arg_main.ml: -------------------------------------------------------------------------------- 1 | (* 2 | Source code for the cmdliner-cheatsheet executable. 3 | This can serve as a template for new programs. 4 | 5 | This sample program exercises various features of cmdliner. It 6 | can be compiled and run to check how the various options work. 7 | 8 | See also 'Demo_subcmd_main.ml' for how to implement subcommands. 9 | *) 10 | 11 | open Printf 12 | 13 | (* Provide the 'Arg', 'Term', and 'Manpage' modules. *) 14 | open Cmdliner 15 | 16 | (* 17 | We store the result of parsing the command line into a single 'conf' 18 | record of the following type. 19 | *) 20 | type conf = { 21 | input_file: string option; 22 | num_cores: int; (* matches long option name "--num-cores" for consistency *) 23 | user_name: string option; 24 | tags: string list; 25 | } 26 | 27 | (* 28 | The core of the application. 29 | *) 30 | let run conf = 31 | printf "\ 32 | Configuration: 33 | input file: %s 34 | number of cores: %i 35 | user name: %s 36 | tags: %s 37 | " 38 | (match conf.input_file with 39 | | None -> "none" 40 | | Some s -> sprintf "%S" s) 41 | conf.num_cores 42 | (match conf.user_name with 43 | | None -> "none" 44 | | Some s -> sprintf "%S" s) 45 | (sprintf "[%s]" (List.map (sprintf "%S") conf.tags 46 | |> String.concat ", ")) 47 | 48 | (************************* Command-line management *************************) 49 | 50 | (* 51 | For each kind of command-line argument, we define a "term" object. 52 | *) 53 | 54 | let input_file_term = 55 | let info = 56 | Arg.info [] (* list must be empty for anonymous arguments *) 57 | ~docv:"FILE" 58 | ~doc:"Example of an anonymous argument at a fixed position." 59 | in 60 | Arg.value (Arg.pos 0 (Arg.some Arg.file) None info) 61 | 62 | let num_cores_term = 63 | let default = 1 in 64 | let info = 65 | Arg.info ["j"; "num-cores"] (* '-j' and '--num-cores' will be synonyms *) 66 | ~docv:"NUM" 67 | ~doc:"Example of an optional argument with a default. 68 | The value of \\$\\(docv\\) is $(docv)." 69 | in 70 | Arg.value (Arg.opt Arg.int default info) 71 | 72 | let user_name_term = 73 | let info = 74 | Arg.info ["u"; "user-name"] 75 | ~docv:"NAME" 76 | ~doc:"Example of an optional argument without a default." 77 | in 78 | Arg.value (Arg.opt (Arg.some Arg.string) None info) 79 | 80 | let tag_term = 81 | let info = 82 | Arg.info [] 83 | ~docv:"TAG" 84 | ~doc:"Example of a list of anonymous arguments." 85 | in 86 | Arg.value (Arg.pos_all Arg.string [] info) 87 | 88 | (* 89 | Combine the values collected for each kind of argument into a single 90 | 'conf' object, which is then passed to our main 'run' function. 91 | 92 | Some merging and tweaking can be useful here but most often 93 | we just map each argument to its own record field. 94 | *) 95 | let cmdline_term run = 96 | let combine input_file num_cores user_name tags = 97 | let only_tags = 98 | match tags with 99 | | [] -> [] 100 | | _input_file :: tags -> tags 101 | in 102 | let conf = { 103 | input_file; 104 | num_cores; 105 | user_name; 106 | tags = only_tags; 107 | } in 108 | run conf 109 | in 110 | Term.(const combine 111 | $ input_file_term 112 | $ num_cores_term 113 | $ user_name_term 114 | $ tag_term 115 | ) 116 | 117 | (* 118 | Inspirational headline for the help/man page. 119 | *) 120 | let doc = 121 | "showcase cmdliner features" 122 | 123 | (* 124 | The structure of the help page. 125 | *) 126 | let man = [ 127 | (* 'NAME' and 'SYNOPSIS' sections are inserted here by cmdliner. *) 128 | 129 | `S Manpage.s_description; (* standard 'DESCRIPTION' section *) 130 | `P "Multi-line, general description goes here."; 131 | `P "This is another paragraph."; 132 | 133 | (* 'ARGUMENTS' and 'OPTIONS' sections are inserted here by cmdliner. *) 134 | 135 | `S Manpage.s_examples; (* standard 'EXAMPLES' section *) 136 | `P "Here is some code:"; 137 | `Pre "let four = 2 + 2"; 138 | 139 | `S Manpage.s_authors; 140 | `P "Your Name Here "; 141 | 142 | `S Manpage.s_bugs; 143 | `P "Contribute documentation improvements at 144 | https://github.com/mjambon/cmdliner-cheatsheet"; 145 | 146 | `S Manpage.s_see_also; 147 | `P "Cmdliner project https://erratique.ch/software/cmdliner/doc/Cmdliner" 148 | ] 149 | 150 | (* 151 | Parse the command line into a 'conf' record and pass it to the 152 | main function 'run'. 153 | *) 154 | let parse_command_line_and_run run = 155 | let info = 156 | Cmd.info 157 | ~doc 158 | ~man 159 | "cmdliner-cheatsheet" (* program name as it will appear in --help *) 160 | in 161 | Cmd.v info (cmdline_term run) |> Cmd.eval |> exit 162 | 163 | let safe_run conf = 164 | try run conf 165 | with 166 | | Failure msg -> 167 | eprintf "Error: %s\n%!" msg; 168 | exit 1 169 | | e -> 170 | let trace = Printexc.get_backtrace () in 171 | eprintf "Error: exception %s\n%s%!" 172 | (Printexc.to_string e) 173 | trace 174 | 175 | let main () = 176 | Printexc.record_backtrace true; 177 | parse_command_line_and_run safe_run 178 | 179 | let () = main () 180 | -------------------------------------------------------------------------------- /src/Demo_subcmd_main.ml: -------------------------------------------------------------------------------- 1 | (* 2 | A minimal program demonstrating how to implement subcommands with cmdliner. 3 | 4 | We're implementing two subcommands 'subcmd1' and 'subcmd2' to be used as: 5 | 6 | $ cmdliner-demo-subcmd subcmd1 --foo # doesn't support '--bar' 7 | $ cmdliner-demo-subcmd subcmd2 --bar # doesn't support '--foo' 8 | 9 | For a generic template and examples on specifying different types 10 | of arguments, consult 'Demo_arg_main.ml'. 11 | *) 12 | 13 | open Printf 14 | 15 | (* Provide 'Term', 'Arg', and 'Manpage' modules. *) 16 | open Cmdliner 17 | 18 | type subcmd1_conf = { 19 | foo: bool; 20 | } 21 | 22 | type subcmd2_conf = { 23 | bar: bool; 24 | } 25 | 26 | (* 27 | The result of parsing the command line successfully. 28 | *) 29 | type cmd_conf = 30 | | Subcmd1 of subcmd1_conf 31 | | Subcmd2 of subcmd2_conf 32 | 33 | (* 34 | Do something with the parsed command line. 35 | *) 36 | let run cmd_conf = 37 | match cmd_conf with 38 | | Subcmd1 conf -> 39 | printf "\ 40 | subcmd1 configuration: 41 | foo: %B 42 | " 43 | conf.foo 44 | | Subcmd2 conf -> 45 | printf "\ 46 | subcmd2 configuration: 47 | bar: %B 48 | " 49 | conf.bar 50 | 51 | (*** Define different kinds of arguments and options (see other demo) ***) 52 | 53 | let foo_term = 54 | let info = 55 | Arg.info ["foo"] 56 | ~doc:"Enable the foo!" 57 | in 58 | Arg.value (Arg.flag info) 59 | 60 | let bar_term = 61 | let info = 62 | Arg.info ["bar"] 63 | ~doc:"Enable the bar!" 64 | in 65 | Arg.value (Arg.flag info) 66 | 67 | (*** Putting together subcommand 'subcmd1' ***) 68 | 69 | let subcmd1_term run = 70 | let combine foo = 71 | Subcmd1 { foo } |> run 72 | in 73 | Term.(const combine 74 | $ foo_term 75 | ) 76 | 77 | let subcmd1_doc = "[some headline for subcmd1]" 78 | 79 | let subcmd1_man = [ 80 | `S Manpage.s_description; 81 | `P "[multiline overview of subcmd1]"; 82 | ] 83 | 84 | let subcmd1 run = 85 | let info = 86 | Cmd.info "subcmd1" 87 | ~doc:subcmd1_doc 88 | ~man:subcmd1_man 89 | in 90 | Cmd.v info (subcmd1_term run) 91 | 92 | (*** Putting together subcommand 'subcmd2' ***) 93 | 94 | let subcmd2_term run = 95 | let combine bar = 96 | Subcmd2 { bar } |> run 97 | in 98 | Term.(const combine 99 | $ bar_term 100 | ) 101 | 102 | let subcmd2_doc = "[some headline for subcmd2]" 103 | 104 | let subcmd2_man = [ 105 | `S Manpage.s_description; 106 | `P "[multiline overview of subcmd2]"; 107 | ] 108 | 109 | let subcmd2 run = 110 | let info = 111 | Cmd.info "subcmd2" 112 | ~doc:subcmd2_doc 113 | ~man:subcmd2_man 114 | in 115 | Cmd.v info (subcmd2_term run) 116 | 117 | (*** Putting together the main command ***) 118 | 119 | let root_doc = "[some headline for the main command]" 120 | 121 | let root_man = [ 122 | `S Manpage.s_description; 123 | `P "[multiline overview of the main command]"; 124 | ] 125 | 126 | (* 127 | Use the built-in action consisting in displaying the help page. 128 | *) 129 | let root_term = 130 | Term.ret (Term.const (`Help (`Pager, None))) 131 | 132 | let root_info = 133 | Cmd.info "cmdliner-demo-subcmd" 134 | ~doc:root_doc 135 | ~man:root_man 136 | 137 | (*** Parse the command line and do something with it ***) 138 | 139 | let subcommands run = [ 140 | subcmd1 run; 141 | subcmd2 run; 142 | ] 143 | 144 | (* 145 | $ cmdliner-demo-subcmd -> parsed as root subcommand 146 | $ cmdliner-demo-subcmd --help -> also parsed as root subcommand 147 | $ cmdliner-demo-subcmd subcmd1 -> parsed as 'subcmd1' subcommand 148 | 149 | If there is a request to display the help page, it displayed at this point, 150 | returning '`Help'. 151 | 152 | Otherwise, 'conf' is returned to the application. 153 | *) 154 | let parse_command_line_and_run (run : cmd_conf -> unit) = 155 | Cmd.group root_info (subcommands run) |> Cmd.eval |> exit 156 | 157 | let main () = 158 | parse_command_line_and_run run 159 | 160 | let () = main () 161 | -------------------------------------------------------------------------------- /src/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (public_names 3 | cmdliner-demo-arg 4 | cmdliner-demo-subcmd 5 | ) 6 | (names 7 | Demo_arg_main 8 | Demo_subcmd_main 9 | ) 10 | (libraries cmdliner) 11 | ) 12 | --------------------------------------------------------------------------------