# \[SOLVED\] Cmdliner : sub-sub commands

**URL:** <https://discuss.ocaml.org/t/solved-cmdliner-sub-sub-commands/675>\
**Category:** Learning\
**Tags:** cmdliner\
**Created:** [August 5, 2017, 5:36pm UTC](https://discuss.ocaml.org/t/solved-cmdliner-sub-sub-commands/675 "2017-08-05T17:36:31Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![cedlemo](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.ocaml.org/cedlemo/32/1213_2.png) [@cedlemo](https://discuss.ocaml.org/u/cedlemo)\
**Post date:** [August 5, 2017, 5:36pm UTC](https://discuss.ocaml.org/t/solved-cmdliner-sub-sub-commands/675/1 "2017-08-05T17:36:31Z")

</div>

Sorry for the meaningless title. In the terminology of _Cmdliner_, a flag is an optional argument without a value, hence the meaningless of the title, but I did not found a better one.

I wanted to writte a little program that could be used like this :

```bash
ompdc playback pause 
ompdc playback prev
ompdc playback stop

```

I had been able to make it work using `Cmdliner.Arg.vflags_all` like in this part of code :

```auto
type playback_cmds = Play | Next | Prev | Pause | Stop

let playback_cmds_to_string = function
  | Next -> "next"
  | Pause -> "pause"
  | Play -> "play"
  | Prev -> "prev"
  | Stop -> "stop"

let initialize_client {host; port} =
   let connection = Mpd.Connection.initialize host port in
   let client = Mpd.Client.initialize connection in
   let _ = print_endline ("Mpd server : " ^ (Mpd.Client.mpd_banner client)) in
   client

let playback common_opts cmd =
  let {host; port} = common_opts in
  let client = initialize_client {host; port} in
  let cmd_str = playback_cmds_to_string cmd in
  let _ = match cmd with
    | Next -> ignore (Mpd.Playback.next client)
    | Pause -> ()
    | Play -> ()
    | Prev -> ignore (Mpd.Playback.prev client)
    | Stop -> ignore (Mpd.Playback.stop client)
  in
  let message = Printf.sprintf "%s:%d %s" host port cmd_str in
  print_endline message

let playback_action =
    let doc = "Play next song." in
    let next = Next, Arg.info ["next"] ~doc in
    let doc = "Toggle Play/Stop." in
    let pause = Pause, Arg.info ["pause"] ~doc in
    let doc = "Play the current song in the Mpd queue." in
    let play = Play, Arg.info ["play"] ~doc in
    let doc = "Stop playing songs." in
    let stop = Stop, Arg.info ["stop"] ~doc in
    let doc = "Play previous song." in
    let prev = Prev, Arg.info ["prev"] ~doc in
    Arg.(last & vflag_all [Pause] [next; pause; play; prev; stop])

let playback_t =
    let doc = "Playback commands"
    in
    let man = [
               `S Manpage.s_description;
               `P "Playback commands for the current playlist (queue).";
               `Blocks help_section; ]
    in
    Term.(const playback $ common_opts_t $ playback_action),
    Term.info "playback" ~doc ~sdocs ~exits ~man

```

Now as you can see in the code, it remains two arguments that I have not implemented because I need to link a value/argument to these flags like this :

```bash
ompdc playback --pause 
ompdc playback --prev
ompdc playback --stop
ompdc playback --play 1
opmdc playback --pause false

```

My problem is that all the commands in the playback Cmdline.term (play, prev, stop, play …) should be implemented with a flags because it force to choose a least the last one command :

for example :

```auto
ompdc playback --stop --next 

```

will choose the last one because I used `Arg.(last & vflag_all`

but if I choose to implement the _play_ command as a `Arg.opt` which is an optionnal argument with a value, I am not sure that :

```auto
ompdc playback --stop --next 

```

will not try to launch the two commands.

I hope I am clear enough, this is not easy to explain.

---

<div class="post-metadata">

**Author:** ![anon72795300](https://avatars.discourse-cdn.com/v4/letter/a/8e7dd6/32.png) [@anon72795300](https://discuss.ocaml.org/u/anon72795300)\
**Post date:** [August 5, 2017, 7:28pm UTC](https://discuss.ocaml.org/t/solved-cmdliner-sub-sub-commands/675/2 "2017-08-05T19:28:43Z")

</div>

The author, @dbuenzli, uses the forum so he might be able to answer that.

---

<div class="post-metadata">

**Author:** ![dbuenzli](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.ocaml.org/dbuenzli/32/18_2.png) [@dbuenzli](https://discuss.ocaml.org/u/dbuenzli)\
**Post date:** [August 8, 2017, 8:53am UTC](https://discuss.ocaml.org/t/solved-cmdliner-sub-sub-commands/675/3 "2017-08-08T08:53:37Z")

</div>

You can’t express this with the `Arg` combinators. What you can always do is accept a less constrained cli and then resolve further constrains in the lifted function which should return a value of type [`ret`](http://erratique.ch/software/cmdliner/doc/Cmdliner.Term.html#TYPEret) to be used with the [`ret`](http://erratique.ch/software/cmdliner/doc/Cmdliner.Term.html#VALret) combinator which allows to hook you in the cmdliner error reporting machinery.

That said since it seems there always a required command in `playback`, I don’t think you should use an _optional_ argument for this. You may rather want to have the synopsis:

```auto
   ompdc playback CMD [ARG]...

```

where `ARG` is parsed by yourself according to `CMD`.

Sub-sub commands are not [well supported yet](https://github.com/dbuenzli/cmdliner/issues/24) but with a little bit of effort you can document them decently see for example the implementation of [`topkg opam`](https://github.com/dbuenzli/topkg/blob/61ea78db61c131584526aeb7d1bd98317385cc1e/src-bin/opam.ml#L99).

---

<div class="post-metadata">

**Author:** ![cedlemo](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.ocaml.org/cedlemo/32/1213_2.png) [@cedlemo](https://discuss.ocaml.org/u/cedlemo)\
**Post date:** [August 20, 2017, 5:41pm UTC](https://discuss.ocaml.org/t/solved-cmdliner-sub-sub-commands/675/4 "2017-08-20T17:41:12Z")

</div>

@dbuenzli,

I finally found the time to do this :

```ocaml
open Cmdliner

let version = "not.yet"
let sdocs = Manpage.s_common_options
let docs = Manpage.s_common_options
let exits = Term.default_exits

let help_section = [
  `S Manpage.s_common_options;
  `P "These options are common to all commands.";
  `S Manpage.s_bugs; `P "Check bug reports at ....";
  `S Manpage.s_authors; `P "cedlemo"
    ]

(* Options common to all commands *)
type mpd_opts = {host : string; port : int}

let common_opts host port =
  {host; port}

let common_opts_t =
  let host =
    let doc = "Set the address of the Mpd server." in
    let env = Arg.env_var "OMPDC_HOST" ~doc in
    Arg.(value & opt string "127.0.0.1" & info ["h"; "host"] ~docs ~env ~docv:"HOST")
  in
  let port =
    let doc = "Set the port of the Mpd server." in
    let env = Arg.env_var "OMPDC_PORT" ~doc in
    Arg.(value & opt int 6600 & info ["p"; "port"] ~docs ~env ~docv:"PORT")
  in
  Term.(const common_opts $ host $ port)

let playback common_opts cmd args =
  let show_message host port cmd args =
    let _args = match args with | None -> "no args" | Some s -> s in
    let message = Printf.sprintf "%s:%d %s %s" host port cmd _args in
    print_endline message
  in
  let {host; port} = common_opts in
  match cmd with
  | `Next -> show_message host port "next" args
  | `Pause -> show_message host port "pause" args
  | `Play -> show_message host port "play" args
  | `Prev -> show_message host port "prev" args
  | `Stop -> show_message host port "stop" args

let playback_actions =
  let actions = ["play", `Play;
                 "stop", `Stop;
                 "prev", `Prev;
                 "next", `Next;
                 "pause", `Pause
  ] in
  let substitue = Printf.sprintf in
  let action_docs = List.map (fun (str, sym) ->
    match sym with
    | `Play -> substitue "$(b,%s) [ARG]" str
    | `Pause -> substitue "$(b,%s) [ARG]" str
    | `Stop | `Prev | `Next -> substitue "$(b,%s)" str
  ) actions in
  let doc = substitue "The action to perform. $(docv) must be one of: %s."
      (String.concat ", " action_docs)
  in
  let action = Arg.enum actions in
  Arg.(required & pos 0 (some action) None & info [] ~doc ~docv:"ACTION")

let playback_args =
  let doc = "An argument if the action need it. In playback mode, only the
  $(b,play) and $(b,pause) actions accept an argument.
  $(b,play) take an integer for the song id to play. $(b,pause) take a
  boolean in order to switch between play/pause." in
  Arg.(value & pos 1 (some string) None & info [] ~doc ~docv:"ARG")

let playback_t =
    let doc = "Playback commands"
    in
    let man = [
               `S Manpage.s_description;
               `P "Playback commands for the current playlist (queue).";
               `Blocks help_section; ]
    in
    Term.(const playback $ common_opts_t $ playback_actions $ playback_args),
    Term.info "playback" ~doc ~sdocs ~exits ~man

let default_cmd =
  let doc = "a Mpd client written in OCaml." in
  let man = help_section in
  Term.(ret (const (fun _ -> `Help (`Pager, None)) $ common_opts_t)),
  Term.info "ompdc" ~version ~doc ~sdocs ~exits ~man

let cmds = [playback_t]

let () = Term.(exit @@ eval_choice default_cmd cmds) 

```

so now I am able to run commands like this :

```bash
$ ompdc.exe playback pause true
127.0.0.1:6600 pause true
$ ompdc.exe playback play 1
127.0.0.1:6600 play 1
$ ompdc.exe playback stop
127.0.0.1:6600 stop no args

```

And the documentation looks like this :

```
ompdc-playback(1) Ompdc Manual ompdc-playback(1)

NAME
       ompdc-playback - Playback commands

SYNOPSIS
       ompdc playback [OPTION]... ACTION [ARG]

DESCRIPTION
       Playback commands for the current playlist (queue).

ARGUMENTS
       ACTION (required)
           The action to perform. ACTION must be one of: play [ARG], stop,
           prev, next, pause [ARG].

       ARG An argument if the action need it. In playback mode, only the play
           and pause actions accept an argument. play take an integer for the
           song id to play. pause take a boolean in order to switch between
           play/pause.

COMMON OPTIONS

```

I find it not that bad but if you can review this to see if/confirm that it is the way to do it, it would be great.

Is there another way to add end of lines in doc strings for example, I wanted to have :

```
ACTION (required)
           The action to perform. ACTION must be one of: 
             play [ARG] play the song ARG,
             stop: stop playing songs,
             prev: play the prev song,
             next: play the next song,
             pause [ARG] toggle pause/play with ARG as a boolean.
```

---

<div class="post-metadata">

**Author:** ![dbuenzli](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.ocaml.org/dbuenzli/32/18_2.png) [@dbuenzli](https://discuss.ocaml.org/u/dbuenzli)\
**Post date:** [August 20, 2017, 5:47pm UTC](https://discuss.ocaml.org/t/solved-cmdliner-sub-sub-commands/675/5 "2017-08-20T17:47:58Z")

</div>

> [@cedlemo](#):
>
> Is there another way to add end of lines in doc strings for example

Not in the doc strings of command line arguments. See the way it’s done in the example I linked to.

---

<div class="post-metadata">

**Author:** ![yawaramin](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.ocaml.org/yawaramin/32/3384_2.png) [@yawaramin](https://discuss.ocaml.org/u/yawaramin)\
**Post date:** [August 21, 2017, 12:35am UTC](https://discuss.ocaml.org/t/solved-cmdliner-sub-sub-commands/675/6 "2017-08-21T00:35:06Z")

</div>

Another option is to ‘fuse’ the sub- and sub-subcommands into just subcommands:

```bash
ompdc playback-pause
ompdc playback-prev
ompdc playback-stop

```

---

<div class="post-metadata">

**Author:** ![dbuenzli](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.ocaml.org/dbuenzli/32/18_2.png) [@dbuenzli](https://discuss.ocaml.org/u/dbuenzli)\
**Post date:** [August 21, 2017, 12:39am UTC](https://discuss.ocaml.org/t/solved-cmdliner-sub-sub-commands/675/7 "2017-08-21T00:39:06Z")

</div>

These long names may not be very usable, however depending on what the other commands are, having `ompdc (play|stop|prev|next|pause)` may be indeed be fine and a better solution.
