[SOLVED] Cmdliner : sub-sub commands

cmdliner

#1

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 :

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 :

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 :

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 :

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 :

ompdc playback --stop --next 

will not try to launch the two commands.

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


#2

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


#3

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 to be used with the ret 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:

   ompdc playback CMD [ARG]...

where ARG is parsed by yourself according to CMD.

Sub-sub commands are not well supported yet but with a little bit of effort you can document them decently see for example the implementation of topkg opam.


#4

@dbuenzli,

I finally found the time to do this :

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 :

$ 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.

#5

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


#6

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

ompdc playback-pause
ompdc playback-prev
ompdc playback-stop

#7

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.