(*--------------------------------------------------------------------------- Copyright (c) 2025 The cmdliner programmers. All rights reserved. SPDX-License-Identifier: ISC ---------------------------------------------------------------------------*) open B0_std open B0_testing open Cmdliner (* Snapshotting command line evaluations *) let capture_fmt f = let buf = Buffer.create 255 in let fmt = Format.formatter_of_buffer buf in let ret = f fmt in ret, (Buffer.contents buf) let make_argv cmd args = Array.of_list (Cmd.name cmd :: args) let env_dumb_term = function | "TERM" -> Some "dumb" | var -> Sys.getenv_opt var let t_eval_result ok = let test_eval_error : Cmd.eval_error Test.T.t = let pp ppf = function | `Parse -> Fmt.string ppf "`Parse" | `Term -> Fmt.string ppf "`Term" | `Exn -> Fmt.string ppf "`Exn" in Test.T.make ~equal:(=) ~pp () in let test_eval_ok ok = let pp ppf = function | `Ok v -> Test.T.pp ok ppf v | `Version -> Fmt.string ppf "`Version" | `Help -> Fmt.string ppf "`Help" in let equal v0 v1 = match v0, v1 with | `Ok v0, `Ok v1 -> Test.T.equal ok v0 v1 | v0, v1 -> v0 = v1 in Test.T.make ~equal ~pp () in Test.T.result' ~ok:(test_eval_ok ok) ~error:test_eval_error let get_eval_value ?__POS__ = function | Ok (`Ok v) -> v | (Error _ | Ok `Version | Ok `Help) as v -> Test.failstop ?__POS__ "Unexpected evalution: %a" (Test.T.pp (t_eval_result Test.T.any)) v let test_eval_result ?__POS__ ?env t cmd args exp = let argv = make_argv cmd args in let (ret, _), _ = (* Ignore outputs *) capture_fmt @@ fun err -> capture_fmt @@ fun help -> Cmd.eval_value ?env ~help ~err cmd ~argv in Test.eq ?__POS__ (t_eval_result t) ret exp let snap_parse ?env t cmd args exp = let loc = Test.Snapshot.loc exp in let argv = make_argv cmd args in let ret = Cmd.eval_value ?env cmd ~argv in Test.snap t (get_eval_value ~__POS__:loc ret) exp let snap_parse_warnings ?env cmd args exp = let loc = Test.Snapshot.loc exp in let argv = make_argv cmd args in let ret, err = capture_fmt @@ fun err -> Cmd.eval_value ?env ~err cmd ~argv in ignore (get_eval_value ~__POS__:loc ret); Snap.lines err exp let snap_eval_error ?env error cmd args exp = let loc = Test.Snapshot.loc exp in let argv = make_argv cmd args in let ret, err = capture_fmt @@ fun err -> Cmd.eval_value ?env ~err cmd ~argv in Test.eq (t_eval_result Test.T.any) ret (Error error) ~__POS__:loc ; Snap.lines err exp let snap_help ?env retv cmd args exp = let loc = Test.Snapshot.loc exp in let argv = make_argv cmd args in let (ret, help), err = capture_fmt @@ fun err -> capture_fmt @@ fun help -> Cmd.eval_value ?env ~help ~err cmd ~argv in Test.string err ""; Test.eq (t_eval_result Test.T.any) ret retv ~__POS__:loc; Snap.lines help exp let snap_completion ?env cmd args exp = snap_help ?env (Ok `Help) cmd ("--__complete" :: args) exp let snap_man ?env ?(args = ["--help=plain"]) cmd exp = snap_help ?env (Ok `Help) cmd args exp (* Sample commands *) open Cmdliner open Cmdliner.Term.Syntax let sample_group_cmd = let man = [ `P "Invoke command with $(cmd), the command name is \ $(cmd.name), the parent is $(cmd.parent) and the tool \ name is $(tool)." ] in let kind = let doc = "Kind of entity" in Arg.(value & opt (some string) None & info ["k";"kind"] ~doc) in let speed = let doc = "Movement $(docv) in m/s" in Arg.(value & opt int 2 & info ["speed"] ~doc ~docv:"SPEED") in let can_fly = let doc = "$(docv) indicates if the entity can fly." in Arg.(value & opt bool false & info ["can-fly"] ~doc) in let birds = let bird = let doc = "Use $(docv) specie." in Arg.(value & pos 0 string "pigeon" & info [] ~doc ~docv:"BIRD") in let fly = Cmd.make (Cmd.info "fly" ~doc:"Fly birds." ~man) @@ let+ bird and+ speed in () in let land' = Cmd.make (Cmd.info "land" ~doc:"Land birds." ~man) @@ let+ bird in () in let info = Cmd.info "birds" ~doc:"Operate on birds." ~man in Cmd.group ~default:Term.(const (fun _ _-> ()) $ kind $ can_fly) info @@ [fly; land'] in let mammals = let man_xrefs = [`Main; `Cmd "birds" ] and doc = "Operate on mammals." in Cmd.make (Cmd.info "mammals" ~doc ~man_xrefs ~man) @@ Term.(const (fun () -> ()) $ const ()) in let fishs = let name' = let doc = "Use fish named $(docv)." in Arg.(value & pos 0 (some string) None & info [] ~doc ~docv:"NAME") in Cmd.make (Cmd.info "fishs" ~doc:"Operate on fishs." ~man) @@ let+ name' in () in let camels = let herd = let doc = "Find in herd $(docv)." and docv = "HERD" in let deprecated = "Herds $(docv) are ignored." in Arg.(value & pos 0 (some string) None & info [] ~deprecated ~doc ~docv) in let bactrian = let deprecated = "Use nothing instead of $(env), $(b,HA!)." in let doc = "Specify a bactrian camel." in let env = Cmd.Env.info "BACTRIAN" ~deprecated in Arg.(value & flag & info ["bactrian"; "b"] ~deprecated ~env ~doc) in let deprecated = "Use $(b,mammals) instead." in Cmd.make (Cmd.info "camels" ~deprecated ~doc:"Operate on camels." ~man) @@ let+ bactrian and+ herd in () in let lookup = let kind_opt = let kinds = ["bird", `Bird; "fish", `Fish] in let doc = "$(docv) restricts the animal kind. Must be " ^ Arg.doc_alts_enum kinds in Arg.(value & opt (some (enum kinds)) None & info ["k"; "kind"] ~doc) in let name_conv = let bird_names = ["sparrow"; "parrot"; "pigeon"] in let fish_names = ["salmon"; "trout"; "piranha"] in let completion = let select ~token:prefix n = if String.starts_with ~prefix n then Some (Arg.Completion.string n) else None in let func kind ~token = match Option.join kind with | None -> Ok (List.filter_map (select ~token) (bird_names @ fish_names)) | Some `Bird -> Ok (List.filter_map (select ~token) bird_names) | Some `Fish -> Ok (List.filter_map (select ~token) fish_names) in Arg.Completion.make ~context:kind_opt func in Arg.Conv.of_conv Arg.string ~completion in Cmd.make (Cmd.info "lookup" ~doc:"Lookup animal by name.") @@ let+ kind_opt and+ name = let doc = "$(docv) is the animal name to lookup" and docv = "NAME" in Arg.(required & pos 0 (some name_conv) None & info [] ~doc ~docv) in () in Cmd.group (Cmd.info "test_group" ~version:"X.Y.Z" ~man) @@ [birds; mammals; fishs; camels; lookup]