(*--------------------------------------------------------------------------- Copyright (c) 2025 The cmdliner programmers. All rights reserved. SPDX-License-Identifier: ISC ---------------------------------------------------------------------------*) open B0_std open B0_testing open Cmdliner open Cmdliner.Term.Syntax (* The tests have the following structure: let test = let cmd = … (* A command definition *) in (* A few snapshots of completion protocol results *) complete … *) let cmd = Testing_cmdliner.sample_group_cmd let complete = Testing_cmdliner.snap_completion cmd let test_groups = Test.test "Cmd.group completions" @@ fun () -> complete ["--__complete="] @@ __POS_OF__ "1\n\ group\n\ Options\n\ item\n\ --version\n\ Show version information.\n\ item-end\n\ item\n\ --help\n\ Show this help in format \u{001B}[04mFMT\u{001B}[m. The value \u{001B}[04mFMT\u{001B}[m must be one of \u{001B}[01mauto\u{001B}[m, \u{001B}[01mpager\u{001B}[m, \u{001B}[01mgroff\u{001B}[m\n\ or \u{001B}[01mplain\u{001B}[m. With \u{001B}[01mauto\u{001B}[m, the format is \u{001B}[01mpager\u{001B}[m or \u{001B}[01mplain\u{001B}[m whenever the \u{001B}[01mTERM\u{001B}[m env var\n\ is \u{001B}[01mdumb\u{001B}[m or undefined.\n\ item-end\n\ group\n\ Subcommands\n\ item\n\ birds\n\ Operate on birds.\n\ item-end\n\ item\n\ mammals\n\ Operate on mammals.\n\ item-end\n\ item\n\ fishs\n\ Operate on fishs.\n\ item-end\n\ item\n\ camels\n\ Operate on camels.\n\ item-end\n\ item\n\ lookup\n\ Lookup animal by name.\n\ item-end\n"; complete ["birds"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Options\n\ item\n\ -k\n\ Kind of entity\n\ item-end\n\ item\n\ --kind\n\ Kind of entity\n\ item-end\n\ item\n\ --can-fly\n\ \u{001B}[04mBOOL\u{001B}[m indicates if the entity can fly.\n\ item-end\n\ item\n\ --version\n\ Show version information.\n\ item-end\n\ item\n\ --help\n\ Show this help in format \u{001B}[04mFMT\u{001B}[m. The value \u{001B}[04mFMT\u{001B}[m must be one of \u{001B}[01mauto\u{001B}[m, \u{001B}[01mpager\u{001B}[m, \u{001B}[01mgroff\u{001B}[m\n\ or \u{001B}[01mplain\u{001B}[m. With \u{001B}[01mauto\u{001B}[m, the format is \u{001B}[01mpager\u{001B}[m or \u{001B}[01mplain\u{001B}[m whenever the \u{001B}[01mTERM\u{001B}[m env var\n\ is \u{001B}[01mdumb\u{001B}[m or undefined.\n\ item-end\n\ group\n\ Subcommands\n\ item\n\ fly\n\ Fly birds.\n\ item-end\n\ item\n\ land\n\ Land birds.\n\ item-end\n"; () let test_no_options_after_dashsash = Test.test "no options after --" @@ fun () -> complete ["birds"; "fly"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Options\n\ item\n\ --speed\n\ Movement \u{001B}[04mSPEED\u{001B}[m in m/s\n\ item-end\n\ item\n\ --version\n\ Show version information.\n\ item-end\n\ item\n\ --help\n\ Show this help in format \u{001B}[04mFMT\u{001B}[m. The value \u{001B}[04mFMT\u{001B}[m must be one of \u{001B}[01mauto\u{001B}[m, \u{001B}[01mpager\u{001B}[m, \u{001B}[01mgroff\u{001B}[m\n\ or \u{001B}[01mplain\u{001B}[m. With \u{001B}[01mauto\u{001B}[m, the format is \u{001B}[01mpager\u{001B}[m or \u{001B}[01mplain\u{001B}[m whenever the \u{001B}[01mTERM\u{001B}[m env var\n\ is \u{001B}[01mdumb\u{001B}[m or undefined.\n\ item-end\n"; complete ["birds"; "fly"; "--"; "--__complete="] @@ __POS_OF__ "1\n"; () let test_opts_starts = Test.test "complete optional argument names" @@ fun () -> complete ["birds"; "--__complete=-"] @@ __POS_OF__ "1\n\ group\n\ Options\n\ item\n\ -k\n\ Kind of entity\n\ item-end\n\ item\n\ --kind\n\ Kind of entity\n\ item-end\n\ item\n\ --can-fly\n\ \u{001B}[04mBOOL\u{001B}[m indicates if the entity can fly.\n\ item-end\n\ item\n\ --version\n\ Show version information.\n\ item-end\n\ item\n\ --help\n\ Show this help in format \u{001B}[04mFMT\u{001B}[m. The value \u{001B}[04mFMT\u{001B}[m must be one of \u{001B}[01mauto\u{001B}[m, \u{001B}[01mpager\u{001B}[m, \u{001B}[01mgroff\u{001B}[m\n\ or \u{001B}[01mplain\u{001B}[m. With \u{001B}[01mauto\u{001B}[m, the format is \u{001B}[01mpager\u{001B}[m or \u{001B}[01mplain\u{001B}[m whenever the \u{001B}[01mTERM\u{001B}[m env var\n\ is \u{001B}[01mdumb\u{001B}[m or undefined.\n\ item-end\n\ group\n\ Subcommands\n"; complete ["birds"; "--__complete=--"] @@ __POS_OF__ "1\n\ group\n\ Options\n\ item\n\ --kind\n\ Kind of entity\n\ item-end\n\ item\n\ --can-fly\n\ \u{001B}[04mBOOL\u{001B}[m indicates if the entity can fly.\n\ item-end\n\ item\n\ --version\n\ Show version information.\n\ item-end\n\ item\n\ --help\n\ Show this help in format \u{001B}[04mFMT\u{001B}[m. The value \u{001B}[04mFMT\u{001B}[m must be one of \u{001B}[01mauto\u{001B}[m, \u{001B}[01mpager\u{001B}[m, \u{001B}[01mgroff\u{001B}[m\n\ or \u{001B}[01mplain\u{001B}[m. With \u{001B}[01mauto\u{001B}[m, the format is \u{001B}[01mpager\u{001B}[m or \u{001B}[01mplain\u{001B}[m whenever the \u{001B}[01mTERM\u{001B}[m env var\n\ is \u{001B}[01mdumb\u{001B}[m or undefined.\n\ item-end\n"; () let test_opt_value = Test.test "complete optional argument values" @@ fun () -> (* Glued *) complete ["birds"; "--__complete=--can-fly="] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ true\n\ \n\ item-end\n\ item\n\ false\n\ \n\ item-end\n"; (* next token *) complete ["birds"; "--can-fly"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ true\n\ \n\ item-end\n\ item\n\ false\n\ \n\ item-end\n"; complete ["birds"; "--can-fly=true"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Options\n\ item\n\ -k\n\ Kind of entity\n\ item-end\n\ item\n\ --kind\n\ Kind of entity\n\ item-end\n\ item\n\ --can-fly\n\ \u{001B}[04mBOOL\u{001B}[m indicates if the entity can fly.\n\ item-end\n\ item\n\ --version\n\ Show version information.\n\ item-end\n\ item\n\ --help\n\ Show this help in format \u{001B}[04mFMT\u{001B}[m. The value \u{001B}[04mFMT\u{001B}[m must be one of \u{001B}[01mauto\u{001B}[m, \u{001B}[01mpager\u{001B}[m, \u{001B}[01mgroff\u{001B}[m\n\ or \u{001B}[01mplain\u{001B}[m. With \u{001B}[01mauto\u{001B}[m, the format is \u{001B}[01mpager\u{001B}[m or \u{001B}[01mplain\u{001B}[m whenever the \u{001B}[01mTERM\u{001B}[m env var\n\ is \u{001B}[01mdumb\u{001B}[m or undefined.\n\ item-end\n"; complete ["birds"; "--can-fly"; "true"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Options\n\ item\n\ -k\n\ Kind of entity\n\ item-end\n\ item\n\ --kind\n\ Kind of entity\n\ item-end\n\ item\n\ --can-fly\n\ \u{001B}[04mBOOL\u{001B}[m indicates if the entity can fly.\n\ item-end\n\ item\n\ --version\n\ Show version information.\n\ item-end\n\ item\n\ --help\n\ Show this help in format \u{001B}[04mFMT\u{001B}[m. The value \u{001B}[04mFMT\u{001B}[m must be one of \u{001B}[01mauto\u{001B}[m, \u{001B}[01mpager\u{001B}[m, \u{001B}[01mgroff\u{001B}[m\n\ or \u{001B}[01mplain\u{001B}[m. With \u{001B}[01mauto\u{001B}[m, the format is \u{001B}[01mpager\u{001B}[m or \u{001B}[01mplain\u{001B}[m whenever the \u{001B}[01mTERM\u{001B}[m env var\n\ is \u{001B}[01mdumb\u{001B}[m or undefined.\n\ item-end\n"; () let test_context_sensitive = Test.test "context sensitive completions" @@ fun () -> complete ["lookup"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ sparrow\n\ \n\ item-end\n\ item\n\ parrot\n\ \n\ item-end\n\ item\n\ pigeon\n\ \n\ item-end\n\ item\n\ salmon\n\ \n\ item-end\n\ item\n\ trout\n\ \n\ item-end\n\ item\n\ piranha\n\ \n\ item-end\n\ group\n\ Options\n\ item\n\ -k\n\ \u{001B}[04mENUM\u{001B}[m restricts the animal kind. Must be either \u{001B}[01mbird\u{001B}[m or \u{001B}[01mfish\u{001B}[m\n\ item-end\n\ item\n\ --kind\n\ \u{001B}[04mENUM\u{001B}[m restricts the animal kind. Must be either \u{001B}[01mbird\u{001B}[m or \u{001B}[01mfish\u{001B}[m\n\ item-end\n\ item\n\ --version\n\ Show version information.\n\ item-end\n\ item\n\ --help\n\ Show this help in format \u{001B}[04mFMT\u{001B}[m. The value \u{001B}[04mFMT\u{001B}[m must be one of \u{001B}[01mauto\u{001B}[m, \u{001B}[01mpager\u{001B}[m, \u{001B}[01mgroff\u{001B}[m\n\ or \u{001B}[01mplain\u{001B}[m. With \u{001B}[01mauto\u{001B}[m, the format is \u{001B}[01mpager\u{001B}[m or \u{001B}[01mplain\u{001B}[m whenever the \u{001B}[01mTERM\u{001B}[m env var\n\ is \u{001B}[01mdumb\u{001B}[m or undefined.\n\ item-end\n"; complete ["lookup"; "-kfish"; "--__complete=s"] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ salmon\n\ \n\ item-end\n"; complete ["lookup"; "-kbird"; "--__complete=p"] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ parrot\n\ \n\ item-end\n\ item\n\ pigeon\n\ \n\ item-end\n"; () let test_restart_restricted_tool = Test.test "restart restricted tool" @@ fun () -> let cmd = Cmd.make (Cmd.info "test_restart_restricted") @@ let+ verb = Arg.(value & flag & info ["verbose"]) and+ tool = let tool = Arg.enum ~docv:"VCS" ["git", `Git; "hg", `Hg] in Arg.(required & pos 0 (some tool) None & info []) and+ args = let arg = let completion = Arg.Completion.complete_restart in Arg.Conv.of_conv Arg.string ~docv:"ARG" ~completion in Arg.(value & pos_right 0 arg [] & info []) in () in let complete = Testing_cmdliner.snap_completion cmd in complete ["--__complete="] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ git\n\ \n\ item-end\n\ item\n\ hg\n\ \n\ item-end\n\ group\n\ Options\n\ item\n\ --verbose\n\ \n\ item-end\n\ item\n\ --help\n\ Show this help in format \u{001B}[04mFMT\u{001B}[m. The value \u{001B}[04mFMT\u{001B}[m must be one of \u{001B}[01mauto\u{001B}[m, \u{001B}[01mpager\u{001B}[m, \u{001B}[01mgroff\u{001B}[m\n\ or \u{001B}[01mplain\u{001B}[m. With \u{001B}[01mauto\u{001B}[m, the format is \u{001B}[01mpager\u{001B}[m or \u{001B}[01mplain\u{001B}[m whenever the \u{001B}[01mTERM\u{001B}[m env var\n\ is \u{001B}[01mdumb\u{001B}[m or undefined.\n\ item-end\n"; complete ["--__complete=g"] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ git\n\ \n\ item-end\n"; (* Note no reset here: as there is no -- token *) complete ["git"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Options\n\ item\n\ --verbose\n\ \n\ item-end\n\ item\n\ --help\n\ Show this help in format \u{001B}[04mFMT\u{001B}[m. The value \u{001B}[04mFMT\u{001B}[m must be one of \u{001B}[01mauto\u{001B}[m, \u{001B}[01mpager\u{001B}[m, \u{001B}[01mgroff\u{001B}[m\n\ or \u{001B}[01mplain\u{001B}[m. With \u{001B}[01mauto\u{001B}[m, the format is \u{001B}[01mpager\u{001B}[m or \u{001B}[01mplain\u{001B}[m whenever the \u{001B}[01mTERM\u{001B}[m env var\n\ is \u{001B}[01mdumb\u{001B}[m or undefined.\n\ item-end\n"; complete ["--"; "git"; "--__complete="] @@ __POS_OF__ "1\n\ restart\n"; () let test_restart_any_tool = Test.test "restart any tool" @@ fun () -> let cmd = Cmd.make (Cmd.info "test_restart") @@ let arg ~docv = let completion = Arg.Completion.complete_restart in Arg.Conv.of_conv Arg.string ~docv:"TOOL" ~completion in let+ verb = Arg.(value & flag & info ["verbose"]) and+ tool = Arg.(required & pos 0 (some (arg ~docv:"TOOL")) None & info []) and+ args = Arg.(value & pos_right 0 (arg ~docv:"ARG") [] & info []) in () in let complete = Testing_cmdliner.snap_completion cmd in complete ["--__complete="] @@ __POS_OF__ "1\n\ group\n\ Options\n\ item\n\ --verbose\n\ \n\ item-end\n\ item\n\ --help\n\ Show this help in format \u{001B}[04mFMT\u{001B}[m. The value \u{001B}[04mFMT\u{001B}[m must be one of \u{001B}[01mauto\u{001B}[m, \u{001B}[01mpager\u{001B}[m, \u{001B}[01mgroff\u{001B}[m\n\ or \u{001B}[01mplain\u{001B}[m. With \u{001B}[01mauto\u{001B}[m, the format is \u{001B}[01mpager\u{001B}[m or \u{001B}[01mplain\u{001B}[m whenever the \u{001B}[01mTERM\u{001B}[m env var\n\ is \u{001B}[01mdumb\u{001B}[m or undefined.\n\ item-end\n"; (* The following two do not restart because -- is missing *) complete ["--__complete=gi"] @@ __POS_OF__ "1\n"; complete ["git"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Options\n\ item\n\ --verbose\n\ \n\ item-end\n\ item\n\ --help\n\ Show this help in format \u{001B}[04mFMT\u{001B}[m. The value \u{001B}[04mFMT\u{001B}[m must be one of \u{001B}[01mauto\u{001B}[m, \u{001B}[01mpager\u{001B}[m, \u{001B}[01mgroff\u{001B}[m\n\ or \u{001B}[01mplain\u{001B}[m. With \u{001B}[01mauto\u{001B}[m, the format is \u{001B}[01mpager\u{001B}[m or \u{001B}[01mplain\u{001B}[m whenever the \u{001B}[01mTERM\u{001B}[m env var\n\ is \u{001B}[01mdumb\u{001B}[m or undefined.\n\ item-end\n"; (* These must restart *) complete ["--"; "--__complete=gi"] @@ __POS_OF__ "1\n\ restart\n"; complete ["--"; "git"; "--__complete="] @@ __POS_OF__ "1\n\ restart\n"; () let test_context = Test.test "Context sensitive completion from optional argument" @@ fun () -> let ctx = Arg.(value & opt (some bool) None & info ["ctx"]) in let dep = let complete ctx ~token:_ = match ctx with | None -> Ok [Arg.Completion.string "ctx-parse-error"] | Some None -> Ok [Arg.Completion.string "no-context"] | Some (Some ctx) -> Ok [Arg.Completion.string (Bool.to_string ctx)] in let completion = Arg.Completion.make ~context:ctx complete in Arg.Conv.of_conv Arg.string ~docv:"SPECIAL" ~completion in let () = (* test [dep] converter on an option *) let cmd = Cmd.make (Cmd.info "test_context") @@ let+ lookup = Arg.(value & opt dep "nothing" & info ["dep"]) and+ ctx in () in let complete = Testing_cmdliner.snap_completion cmd in complete ["--dep"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ no-context\n\ \n\ item-end\n"; complete ["--ctx=hey"; "--dep"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ ctx-parse-error\n\ \n\ item-end\n"; complete ["--ctx=true"; "--dep"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ true\n\ \n\ item-end\n"; complete ["--dep"; "--__complete="; "--ctx=true"] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ true\n\ \n\ item-end\n"; in let () = let cmd = Cmd.make (Cmd.info "test_context") @@ let+ lookup = Arg.(value & pos 0 dep "nothing" & info []) and+ ctx in () in let complete = Testing_cmdliner.snap_completion cmd in complete ["--__complete=a"] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ no-context\n\ \n\ item-end\n"; complete ["--ctx=hey"; "--__complete=a"] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ ctx-parse-error\n\ \n\ item-end\n"; complete ["--ctx=true"; "--__complete=a"] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ true\n\ \n\ item-end\n"; complete ["--__complete=a"; "--ctx=true"] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ true\n\ \n\ item-end\n"; in () let test_context = Test.test "Context sensitive completion from positional argument" @@ fun () -> let ctx0 = Arg.(value & pos 0 (some bool) None & info []) in let dep = let complete ctx ~token:_ = match ctx with | None -> Ok [Arg.Completion.string "ctx-parse-error"] | Some None -> Ok [Arg.Completion.string "no-context"] | Some (Some ctx) -> Ok [Arg.Completion.string (Bool.to_string ctx)] in let completion = Arg.Completion.make ~context:ctx0 complete in Arg.Conv.of_conv Arg.string ~docv:"SPECIAL" ~completion in let () = (* test [dep] converter on an option *) let cmd = Cmd.make (Cmd.info "test_context") @@ let+ lookup = Arg.(value & opt dep "nothing" & info ["dep"]) and+ ctx0 in () in let complete = Testing_cmdliner.snap_completion cmd in complete ["--dep"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ no-context\n\ \n\ item-end\n"; complete ["bla"; "--dep"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ ctx-parse-error\n\ \n\ item-end\n"; complete ["true"; "--dep"; "--__complete="] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ true\n\ \n\ item-end\n"; complete ["--dep"; "--__complete="; "true"] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ true\n\ \n\ item-end\n"; in let () = let cmd = Cmd.make (Cmd.info "test_context") @@ let+ lookup = Arg.(value & pos 1 dep "nothing" & info []) and+ ctx0 in () in let complete = Testing_cmdliner.snap_completion cmd in complete ["hey"; "--__complete=a"] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ ctx-parse-error\n\ \n\ item-end\n"; complete ["true"; "--__complete=a"] @@ __POS_OF__ "1\n\ group\n\ Values\n\ item\n\ true\n\ \n\ item-end\n"; in () let main () = let doc = "Test completion" in Test.main ~doc @@ fun () -> Test.autorun () let () = if !Sys.interactive then () else exit (main ())