diff --git a/Makefile b/Makefile index a3921d7..896c7ea 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ -include $(shell ocamlc -where)/Makefile.config PREFIX=/usr +BINDIR=$(DESTDIR)$(PREFIX)/bin LIBDIR=$(DESTDIR)$(PREFIX)/lib/ocaml/cmdliner DOCDIR=$(DESTDIR)$(PREFIX)/share/doc/cmdliner NATIVE=$(shell ocamlopt -version > /dev/null 2>&1 && echo true) @@ -21,7 +22,7 @@ NATIVE=$(shell ocamlopt -version > /dev/null 2>&1 && echo true) INSTALL=install B=_build -BASE=$(B)/cmdliner +BASE=$(B)/src/cmdliner ifeq ($(NATIVE),true) BUILD-TARGETS=build-byte build-native @@ -53,27 +54,29 @@ build-byte: build-native: ocaml build.ml cmxa + ocaml build.ml exe build-native-dynlink: ocaml build.ml cmxs -create-libdir: - $(INSTALL) -d "$(LIBDIR)" +prepare-prefix: + $(INSTALL) -d "$(BINDIR)" "$(LIBDIR)" -install-common: create-libdir +install-common: prepare-prefix $(INSTALL) pkg/META $(BASE).mli $(BASE).cmi $(BASE).cmti "$(LIBDIR)" $(INSTALL) cmdliner.opam "$(LIBDIR)/opam" -install-byte: create-libdir +install-byte: prepare-prefix $(INSTALL) $(BASE).cma "$(LIBDIR)" -install-native: create-libdir +install-native: prepare-prefix $(INSTALL) $(BASE).cmxa $(BASE)$(EXT_LIB) $(wildcard $(B)/cmdliner*.cmx) \ "$(LIBDIR)" + $(INSTALL) -m 755 $(B)/bin/cmdliner.exe "$(BINDIR)/cmdliner" -install-native-dynlink: create-libdir +install-native-dynlink: prepare-prefix $(INSTALL) $(BASE).cmxs "$(LIBDIR)" .PHONY: all install install-doc clean build-byte build-native \ - build-native-dynlink create-libdir install-common install-byte \ + build-native-dynlink prepare-prefix install-common install-byte \ install-native install-dynlink diff --git a/bin/cmdliner_completion.ml b/bin/cmdliner_completion.ml new file mode 100644 index 0000000..8087ec0 --- /dev/null +++ b/bin/cmdliner_completion.ml @@ -0,0 +1,53 @@ +let zsh_completion name = Printf.sprintf {|function _%s { + words[CURRENT]="+cmdliner_complete:${words[CURRENT]}" + local line="${(@)words}" + local -a completions + local type group item item_doc + eval $line | while IFS= read -r type; do + if [[ "$type" == "group" ]]; then + if [ -n "$completions" ]; then + _describe -V unsorted completions -U + completions=() + fi + read -r group + elif [[ "$type" == "item" ]]; then + read -r item; + read -r item_doc; + completions+=("$item":"$item_doc") + elif [[ "$type" == "dir" ]]; then + _path_files -/ + elif [[ "$type" == "file" ]]; then + _path_files -f + fi + done + if [ -n "$completions" ]; then + _describe -V unsorted completions -U + fi +} +compdef _%s %s +|} name name name;;let bash_completion name = Printf.sprintf {|_%s() { + local prefix="${COMP_WORDS[COMP_CWORD]}" + COMP_WORDS[COMP_CWORD]="+cmdliner_complete:${COMP_WORDS[COMP_CWORD]}" + local line="${COMP_WORDS[@]}" + local type group item item_doc + while read type; do + if [[ $type == "group" ]]; then + read group + elif [[ $type == "dir" ]] && (type compopt &> /dev/null); then + if [[ $prefix != -* ]]; then + COMPREPLY+=( $(compgen -d "$prefix") ) + fi + elif [[ $type == "file" ]] && (type compopt &> /dev/null); then + if [[ $prefix != -* ]]; then + COMPREPLY+=( $(compgen -f "$prefix") ) + fi + elif [[ $type == "item" ]]; then + read item; + read item_doc; + COMPREPLY+=($item) + fi + done < <(eval $line) + return 0 +} +complete -F _%s %s +|} name name name;; \ No newline at end of file diff --git a/bin/dune b/bin/dune new file mode 100644 index 0000000..cdb01e3 --- /dev/null +++ b/bin/dune @@ -0,0 +1,26 @@ +(executable + (name main) + (public_name cmdliner) + (package cmdliner) + (libraries cmdliner)) + +(rule + (target cmdliner_completion.ml) + (deps ../completion/zsh-completion.sh) + (mode promote) + (action + (with-stdout-to + %{target} + (progn + (echo "let zsh_completion name = Printf.sprintf {|") + (pipe-stdout + (cat ../completion/zsh-completion.sh) + (run sed "s/NAME/%s/g")) + (echo "|} name name name;;") ; number of NAME token occurrences + (echo "let bash_completion name = Printf.sprintf {|") + (pipe-stdout + (cat ../completion/bash-completion.sh) + (run sed "s/NAME/%s/g")) + (echo "|} name name name;;") ; number of NAME token occurrences + )))) + diff --git a/bin/main.ml b/bin/main.ml new file mode 100644 index 0000000..0ff452a --- /dev/null +++ b/bin/main.ml @@ -0,0 +1,38 @@ +open Cmdliner + +type shell = Bash | Zsh + +let shell_enum = ["bash", Bash; "zsh", Zsh] + +let completion_script shell prog = + print_endline ( + match shell with + | Bash -> Cmdliner_completion.bash_completion prog + | Zsh -> Cmdliner_completion.zsh_completion prog) + +let completion_script_cmd = + let shell = + let doc = "Shell program to output the completion script for" in + Arg.(required & opt (some (enum shell_enum)) None & info ["shell"] ~docv:"SHELL" ~doc) + in + let prog = + let doc = "Program to output the completion script for" in + Arg.(required & pos 0 (some string) None & info [] ~docv:"PROGRAM" ~doc) + in + let name = "completion-script" in + let doc = "Output the completion script for the shell." in + let man = [ + `S Manpage.s_description; + `P "Output the completion script for the shell. Example usage is the following:"; + `Pre (Printf.sprintf " eval \"\\$(cmdliner %s --shell zsh myprog)\"" name); + ] in + let info = Cmd.info name ~doc ~man in + Cmd.v info Term.(const completion_script $ shell $ prog) + +let main_cmd = + let doc = "a helper for cmdliner based programs" in + let info = Cmd.info "cmdliner" ~version:"%%VERSION%%" ~doc in + let default = Term.(ret (const (fun () -> `Help (`Pager, None)) $ const ())) in + Cmd.group info ~default [completion_script_cmd] + +let () = exit (Cmd.eval main_cmd) diff --git a/build.ml b/build.ml index e0d9a3c..2911ece 100755 --- a/build.ml +++ b/build.ml @@ -3,9 +3,14 @@ (* Usage: ocaml build.ml [cma|cmxa|cmxs|clean] *) let root_dir = Sys.getcwd () -let build_dir = "_build" +let root_build_dir = Filename.concat root_dir "_build" let src_dir = "src" +type unit = Lib | Bin + +let unit_dir = function Lib -> "src" | Bin -> "bin" +let build_dir u = Filename.concat root_build_dir (unit_dir u) + let base_ocaml_opts = [ "-g"; "-bin-annot"; "-safe-string"; (* Remove once we require >= 4.06 *) ] @@ -89,18 +94,26 @@ let read_cmd args = (* Create and delete directories *) -let mkdir dir = +let rec mkdir dir = + let parent = Filename.dirname dir in + if String.equal dir parent then () + else mkdir (Filename.dirname dir); try match Sys.file_exists dir with | true -> () | false -> run_cmd ["mkdir"; dir] with | Sys_error e -> err "%s: %s" dir e -let rmdir dir = +let rec rmdir dir = try match Sys.file_exists dir with | false -> () | true -> - let rm f = Sys.remove (fpath ~dir f) in + let rm f = + let p = fpath ~dir f in + if Sys.is_directory p + then rmdir p + else Sys.remove (fpath ~dir f) + in Array.iter rm (Sys.readdir dir); run_cmd ["rmdir"; dir] with @@ -125,6 +138,13 @@ let sort_srcs srcs = let common srcs = base_ocaml_opts @ sort_srcs srcs +let exe src = + let lib = build_dir Lib in + ["-I"; lib; "cmdliner.cmxa"] @ common src + +let build_exe srcs = + run_cmd ([ocamlopt ()] @ exe srcs @ ["-o"; "cmdliner.exe"]) + let build_cma srcs = run_cmd ([ocamlc ()] @ common srcs @ ["-a"; "-o"; "cmdliner.cma"]) @@ -134,9 +154,11 @@ let build_cmxa srcs = let build_cmxs srcs = run_cmd ([ocamlopt ()] @ common srcs @ ["-shared"; "-o"; "cmdliner.cmxs"]) -let clean () = rmdir build_dir +let clean () = rmdir root_build_dir -let in_build_dir f = +let in_build_dir u f = + let src_dir = unit_dir u in + let build_dir = build_dir u in let srcs = ml_srcs src_dir in let cp src = cp (fpath ~dir:src_dir src) (fpath ~dir:build_dir src) in mkdir build_dir; @@ -144,9 +166,10 @@ let in_build_dir f = Sys.chdir build_dir; f srcs; Sys.chdir root_dir let main () = match Array.to_list Sys.argv with -| _ :: [ "cma" ] -> in_build_dir build_cma -| _ :: [ "cmxa" ] -> in_build_dir build_cmxa -| _ :: [ "cmxs" ] -> in_build_dir build_cmxs +| _ :: [ "exe" ] -> in_build_dir Bin build_exe +| _ :: [ "cma" ] -> in_build_dir Lib build_cma +| _ :: [ "cmxa" ] -> in_build_dir Lib build_cmxa +| _ :: [ "cmxs" ] -> in_build_dir Lib build_cmxs | _ :: [ "clean" ] -> clean () | [] | [_] -> err "Missing argument: cma, cmxa, cmxs or clean\n"; | cmd :: args -> diff --git a/cmdliner.opam b/cmdliner.opam index ac7b447..cbee459 100644 --- a/cmdliner.opam +++ b/cmdliner.opam @@ -29,7 +29,7 @@ depends: [ ] build: [make "all" "PREFIX=%{prefix}%"] install: [ - [make "install" "LIBDIR=%{_:lib}%" "DOCDIR=%{_:doc}%"] + [make "install" "BINDIR=%{_:bin}%" "LIBDIR=%{_:lib}%" "DOCDIR=%{_:doc}%"] [make "install-doc" "LIBDIR=%{_:lib}%" "DOCDIR=%{_:doc}%"] ] dev-repo: "git+https://erratique.ch/repos/cmdliner.git" diff --git a/completion/PROTOCOL.md b/completion/PROTOCOL.md new file mode 100644 index 0000000..3dee283 --- /dev/null +++ b/completion/PROTOCOL.md @@ -0,0 +1,46 @@ +# Shell completion protocol + +This document describes the protocol between cmdliner based programs (*the +program* going further) and shell completion scripts (*the script* going +further) which drive the completion. + +The script, when completion is requested, invokes the program with a modified +argv line, replacing a token `TOKEN` where completion is requested with a +prefixed token `+cmdliner-complete:TOKEN`. + +The program when invoked, produces a list of completions commands which are then +interpreted by the script. There the following commands (but please note that +the script might ignore some if the corresponding shell lacks support for +certain features). + +#### `group` + +Define a completion group: + + group + NAME + +Completions followed by this command will be presented to user as a single +group. There could be multiple groups. + +#### `item` + +Define a completion item: + + item + COMPLETION + DESCRIPTION + +#### `file` + +Present filenames as completion items: + + file + +#### `dir` + +Present dirnames as completion items: + + dir + + diff --git a/completion/bash-completion.sh b/completion/bash-completion.sh new file mode 100644 index 0000000..030f48e --- /dev/null +++ b/completion/bash-completion.sh @@ -0,0 +1,25 @@ +_NAME() { + local prefix="${COMP_WORDS[COMP_CWORD]}" + COMP_WORDS[COMP_CWORD]="+cmdliner_complete:${COMP_WORDS[COMP_CWORD]}" + local line="${COMP_WORDS[@]}" + local type group item item_doc + while read type; do + if [[ $type == "group" ]]; then + read group + elif [[ $type == "dir" ]] && (type compopt &> /dev/null); then + if [[ $prefix != -* ]]; then + COMPREPLY+=( $(compgen -d "$prefix") ) + fi + elif [[ $type == "file" ]] && (type compopt &> /dev/null); then + if [[ $prefix != -* ]]; then + COMPREPLY+=( $(compgen -f "$prefix") ) + fi + elif [[ $type == "item" ]]; then + read item; + read item_doc; + COMPREPLY+=($item) + fi + done < <(eval $line) + return 0 +} +complete -F _NAME NAME diff --git a/completion/zsh-completion.sh b/completion/zsh-completion.sh new file mode 100644 index 0000000..d867946 --- /dev/null +++ b/completion/zsh-completion.sh @@ -0,0 +1,27 @@ +function _NAME { + words[CURRENT]="+cmdliner_complete:${words[CURRENT]}" + local line="${(@)words}" + local -a completions + local type group item item_doc + eval $line | while IFS= read -r type; do + if [[ "$type" == "group" ]]; then + if [ -n "$completions" ]; then + _describe -V unsorted completions -U + completions=() + fi + read -r group + elif [[ "$type" == "item" ]]; then + read -r item; + read -r item_doc; + completions+=("$item":"$item_doc") + elif [[ "$type" == "dir" ]]; then + _path_files -/ + elif [[ "$type" == "file" ]]; then + _path_files -f + fi + done + if [ -n "$completions" ]; then + _describe -V unsorted completions -U + fi +} +compdef _NAME NAME diff --git a/dune-project b/dune-project index f4beddd..4c1d17b 100644 --- a/dune-project +++ b/dune-project @@ -1,2 +1,2 @@ -(lang dune 1.4) -(name cmdliner) \ No newline at end of file +(lang dune 2.7) +(name cmdliner) diff --git a/example/dune b/example/dune new file mode 100644 index 0000000..843d819 --- /dev/null +++ b/example/dune @@ -0,0 +1,7 @@ +(copy_files + (files ../test/*_ex.ml)) + + +(executables + (names cp_ex rm_ex darcs_ex tail_ex) + (libraries cmdliner)) diff --git a/src/cmdliner.mli b/src/cmdliner.mli index 7e886cd..ef85edb 100644 --- a/src/cmdliner.mli +++ b/src/cmdliner.mli @@ -829,15 +829,13 @@ module Arg : sig type 'a printer = Format.formatter -> 'a -> unit (** The type for converted argument printers. *) - [@@@alert "-deprecated"] (* Need to be able to mention them ! *) - type 'a conv = 'a parser * 'a printer - (** The type for argument converters. - - {b Warning.} Do not use directly, use {!val-conv} or {!val-conv'}. - This type will become abstract in the next major version of cmdliner. *) - [@@@alert "+deprecated"] (* Need to be able to mention them ! *) + type 'a conv + (** The type for argument converters. *) val conv : + ?complete:(string -> (string * string) list) -> + ?complete_file:bool -> + ?complete_dir:bool -> ?docv:string -> (string -> ('a, [`Msg of string]) result) * 'a printer -> 'a conv (** [conv ~docv (parse, print)] is an argument converter @@ -847,6 +845,9 @@ module Arg : sig ["VALUE"]. *) val conv' : + ?complete:(string -> (string * string) list) -> + ?complete_file:bool -> + ?complete_dir:bool -> ?docv:string -> (string -> ('a, string) result) * 'a printer -> 'a conv (** [conv'] is like {!val-conv} but the [Error] case has an unlabelled diff --git a/src/cmdliner_arg.ml b/src/cmdliner_arg.ml index c6ae9e9..22966f1 100644 --- a/src/cmdliner_arg.ml +++ b/src/cmdliner_arg.ml @@ -21,27 +21,33 @@ let str_of_pp pp v = pp Format.str_formatter v; Format.flush_str_formatter () (* Argument converters *) -type 'a parser = string -> [ `Ok of 'a | `Error of string ] -type 'a printer = Format.formatter -> 'a -> unit - -type 'a conv = 'a parser * 'a printer +type 'a parser = 'a Cmdliner_base.parser +type 'a printer = 'a Cmdliner_base.printer + +type 'a conv = 'a Cmdliner_base.conv = { + parse: 'a parser; + print: 'a printer; + complete: Cmdliner_base.complete; +} type 'a converter = 'a conv let default_docv = "VALUE" -let conv ?docv (parse, print) = +let conv ?complete ?complete_file ?complete_dir ?docv (parse, print) = + let complete = Cmdliner_base.complete ?complete ?file:complete_file ?dir:complete_dir () in let parse s = match parse s with Ok v -> `Ok v | Error (`Msg e) -> `Error e in - parse, print + {parse; print; complete} -let conv' ?docv (parse, print) = +let conv' ?complete ?complete_file ?complete_dir ?docv (parse, print) = + let complete = Cmdliner_base.complete ?complete ?file:complete_file ?dir:complete_dir () in let parse s = match parse s with Ok v -> `Ok v | Error e -> `Error e in - parse, print + {parse; print; complete} -let pconv ?docv conv = conv +let pconv ?docv (parse, print) = {parse; print; complete=Cmdliner_base.no_complete} -let conv_parser (parse, _) = +let conv_parser {parse; _} = fun s -> match parse s with `Ok v -> Ok v | `Error e -> Error (`Msg e) -let conv_printer (_, print) = print +let conv_printer {print; _} = print let conv_docv _ = default_docv let err_invalid s kind = `Msg (strf "invalid value '%s', expected %s" s kind) @@ -91,9 +97,9 @@ let try_env ei a parse ~absent = match Cmdliner_info.Arg.env a with | `Error e -> err (Cmdliner_msg.err_env_parse env ~err:e) | `Ok v -> report_deprecated_env ei env; Ok v -let arg_to_args = Cmdliner_info.Arg.Set.singleton -let list_to_args f l = - let add acc v = Cmdliner_info.Arg.Set.add (f v) acc in +let arg_to_args a complete = Cmdliner_info.Arg.Set.singleton a complete +let list_to_args f l complete = + let add acc v = Cmdliner_info.Arg.Set.add (f v) complete acc in List.fold_left add Cmdliner_info.Arg.Set.empty l let flag a = @@ -104,7 +110,7 @@ let flag a = | [_, f, Some v] -> err (Cmdliner_msg.err_flag_value f v) | (_, f, _) :: (_ ,g, _) :: _ -> err (Cmdliner_msg.err_opt_repeated f g) in - arg_to_args a, convert + arg_to_args a Cmdliner_base.no_complete, convert let flag_all a = if Cmdliner_info.Arg.is_pos a then invalid_arg err_not_opt else @@ -121,7 +127,7 @@ let flag_all a = Ok (List.rev_map truth l) with Failure e -> err e in - arg_to_args a, convert + arg_to_args a Cmdliner_base.no_complete, convert let vflag v l = let convert _ cl = @@ -145,7 +151,7 @@ let vflag v l = let flag (_, a) = if Cmdliner_info.Arg.is_pos a then invalid_arg err_not_opt else a in - list_to_args flag l, convert + list_to_args flag l Cmdliner_base.no_complete, convert let vflag_all v l = let convert _ cl = @@ -169,13 +175,13 @@ let vflag_all v l = if Cmdliner_info.Arg.is_pos a then invalid_arg err_not_opt else Cmdliner_info.Arg.make_all_opts a in - list_to_args flag l, convert + list_to_args flag l Cmdliner_base.no_complete, convert let parse_opt_value parse f v = match parse v with | `Ok v -> v | `Error err -> failwith (Cmdliner_msg.err_opt_parse f ~err) -let opt ?vopt (parse, print) v a = +let opt ?vopt {parse; print; complete} v a = if Cmdliner_info.Arg.is_pos a then invalid_arg err_not_opt else let absent = match Cmdliner_info.Arg.absent a with | Cmdliner_info.Arg.Doc d as a when d <> "" -> a @@ -197,9 +203,9 @@ let opt ?vopt (parse, print) v a = end | (_, f, _) :: (_, g, _) :: _ -> err (Cmdliner_msg.err_opt_repeated g f) in - arg_to_args a, convert + arg_to_args a complete, convert -let opt_all ?vopt (parse, print) v a = +let opt_all ?vopt {parse; print; complete} v a = if Cmdliner_info.Arg.is_pos a then invalid_arg err_not_opt else let absent = match Cmdliner_info.Arg.absent a with | Cmdliner_info.Arg.Doc d as a when d <> "" -> a @@ -223,7 +229,7 @@ let opt_all ?vopt (parse, print) v a = (List.sort rev_compare (List.rev_map parse l))) with | Failure e -> err e in - arg_to_args a, convert + arg_to_args a complete, convert (* Positional arguments *) @@ -231,7 +237,7 @@ let parse_pos_value parse a v = match parse v with | `Ok v -> v | `Error err -> failwith (Cmdliner_msg.err_pos_parse a ~err) -let pos ?(rev = false) k (parse, print) v a = +let pos ?(rev = false) k {parse; print; complete} v a = if Cmdliner_info.Arg.is_opt a then invalid_arg err_not_pos else let absent = match Cmdliner_info.Arg.absent a with | Cmdliner_info.Arg.Doc d as a when d <> "" -> a @@ -245,9 +251,9 @@ let pos ?(rev = false) k (parse, print) v a = (try Ok (parse_pos_value parse a v) with Failure e -> err e) | _ -> assert false in - arg_to_args a, convert + arg_to_args a complete, convert -let pos_list pos (parse, _) v a = +let pos_list pos {parse; complete; _} v a = if Cmdliner_info.Arg.is_opt a then invalid_arg err_not_pos else let a = Cmdliner_info.Arg.make_pos ~pos a in let convert ei cl = match Cmdliner_cline.pos_arg cl a with @@ -256,7 +262,7 @@ let pos_list pos (parse, _) v a = try Ok (List.rev (List.rev_map (parse_pos_value parse a) l)) with | Failure e -> err e in - arg_to_args a, convert + arg_to_args a complete, convert let all = Cmdliner_info.Arg.pos ~rev:false ~start:0 ~len:None let pos_all c v a = pos_list all c v a @@ -274,16 +280,16 @@ let pos_right ?(rev = false) k = (* Arguments as terms *) let absent_error args = - let make_req a acc = + let make_req a v acc = let req_a = Cmdliner_info.Arg.make_req a in - Cmdliner_info.Arg.Set.add req_a acc + Cmdliner_info.Arg.Set.add req_a v acc in Cmdliner_info.Arg.Set.fold make_req args Cmdliner_info.Arg.Set.empty let value a = a let err_arg_missing args = - err @@ Cmdliner_msg.err_arg_missing (Cmdliner_info.Arg.Set.choose args) + err @@ Cmdliner_msg.err_arg_missing (fst (Cmdliner_info.Arg.Set.choose args)) let required (args, convert) = let args = absent_error args in diff --git a/src/cmdliner_arg.mli b/src/cmdliner_arg.mli index 1166b13..780513f 100644 --- a/src/cmdliner_arg.mli +++ b/src/cmdliner_arg.mli @@ -7,14 +7,24 @@ type 'a parser = string -> [ `Ok of 'a | `Error of string ] type 'a printer = Format.formatter -> 'a -> unit -type 'a conv = 'a parser * 'a printer +type 'a conv = 'a Cmdliner_base.conv = { + parse: 'a parser; + print: 'a printer; + complete: Cmdliner_base.complete; +} type 'a converter = 'a conv val conv : + ?complete:(string -> (string * string) list) -> + ?complete_file:bool -> + ?complete_dir:bool -> ?docv:string -> (string -> ('a, [`Msg of string]) result) * 'a printer -> 'a conv val conv' : + ?complete:(string -> (string * string) list) -> + ?complete_file:bool -> + ?complete_dir:bool -> ?docv:string -> (string -> ('a, string) result) * 'a printer -> 'a conv val pconv : ?docv:string -> 'a parser * 'a printer -> 'a conv @@ -37,7 +47,8 @@ type 'a t = 'a Cmdliner_term.t type info val info : ?deprecated:string -> ?absent:string -> ?docs:string -> ?docv:string -> - ?doc:string -> ?env:env -> string list -> info + ?doc:string -> ?env:env -> + string list -> info val ( & ) : ('a -> 'b) -> 'a -> 'b diff --git a/src/cmdliner_base.ml b/src/cmdliner_base.ml index f1c659c..dc3866b 100644 --- a/src/cmdliner_base.ml +++ b/src/cmdliner_base.ml @@ -80,8 +80,9 @@ let pp_lines ppf s = in loop 0 s +let is_space = function ' ' | '\n' | '\r' | '\t' -> true | _ -> false + let pp_tokens ~spaces ppf s = (* collapse white and hint spaces (maybe) *) - let is_space = function ' ' | '\n' | '\r' | '\t' -> true | _ -> false in let i_max = String.length s - 1 in let flush start stop = pp_str ppf (String.sub s start (stop - start + 1)) in let rec skip_white i = @@ -142,65 +143,87 @@ let err_invalid_val = err_invalid "value" let err_sep_miss sep s = err_invalid_val s (strf "missing a '%c' separator" sep) +(* Completions *) + +type complete = { + complete_file : bool; + complete_dir : bool; + complete : (string -> (string * string) list); +} + +let complete ?(file=false) ?(dir=false) ?complete () = + let complete = Option.value complete ~default:(fun _ -> []) in + {complete_file=file; complete_dir=dir; complete} + +let no_complete = complete () + (* Converters *) type 'a parser = string -> [ `Ok of 'a | `Error of string ] type 'a printer = Format.formatter -> 'a -> unit -type 'a conv = 'a parser * 'a printer - -let some ?(none = "") (parse, print) = - let parse s = match parse s with `Ok v -> `Ok (Some v) | `Error _ as e -> e in +type 'a conv = { + parse: 'a parser; + print: 'a printer; + complete: complete; +} + +let some ?(none = "") conv = + let parse s = match conv.parse s with `Ok v -> `Ok (Some v) | `Error _ as e -> e in let print ppf v = match v with | None -> Format.pp_print_string ppf none - | Some v -> print ppf v + | Some v -> conv.print ppf v in - parse, print + {parse; print; complete=conv.complete} -let some' ?none (parse, print) = - let parse s = match parse s with `Ok v -> `Ok (Some v) | `Error _ as e -> e in +let some' ?none conv = + let parse s = match conv.parse s with `Ok v -> `Ok (Some v) | `Error _ as e -> e in let print ppf = function - | None -> (match none with None -> () | Some v -> print ppf v) - | Some v -> print ppf v + | None -> (match none with None -> () | Some v -> conv.print ppf v) + | Some v -> conv.print ppf v in - parse, print + {parse; print; complete=conv.complete} let bool = let parse s = try `Ok (bool_of_string s) with | Invalid_argument _ -> `Error (err_invalid_val s (alts_str ~quoted:true ["true"; "false"])) in - parse, Format.pp_print_bool + {parse; print=Format.pp_print_bool; complete=no_complete} let char = let parse s = match String.length s = 1 with | true -> `Ok s.[0] | false -> `Error (err_invalid_val s "expected a character") in - parse, pp_char + {parse; print=pp_char; complete=no_complete} let parse_with t_of_str exp s = try `Ok (t_of_str s) with Failure _ -> `Error (err_invalid_val s exp) let int = - parse_with int_of_string "expected an integer", Format.pp_print_int + let parse = parse_with int_of_string "expected an integer" in + {parse; print=Format.pp_print_int; complete=no_complete} let int32 = - parse_with Int32.of_string "expected a 32-bit integer", - (fun ppf -> pp ppf "%ld") + let parse = parse_with Int32.of_string "expected a 32-bit integer" in + let print ppf = pp ppf "%ld" in + {parse; print; complete=no_complete} let int64 = - parse_with Int64.of_string "expected a 64-bit integer", - (fun ppf -> pp ppf "%Ld") + let parse = parse_with Int64.of_string "expected a 64-bit integer" in + let print ppf = pp ppf "%Ld" in + {parse; print; complete=no_complete} let nativeint = - parse_with Nativeint.of_string "expected a processor-native integer", - (fun ppf -> pp ppf "%nd") + let parse = parse_with Nativeint.of_string "expected a processor-native integer" in + let print ppf = pp ppf "%nd" in + {parse; print; complete=no_complete} let float = - parse_with float_of_string "expected a floating point number", - Format.pp_print_float + let parse = parse_with float_of_string "expected a floating point number" in + {parse; print=Format.pp_print_float; complete=no_complete} -let string = (fun s -> `Ok s), pp_str +let string = {parse=(fun s -> `Ok s); print=pp_str; complete=no_complete} let enum sl = if sl = [] then invalid_arg err_empty_list else let t = Cmdliner_trie.of_list sl in @@ -218,28 +241,29 @@ let enum sl = try pp_str ppf (List.assoc v sl_inv) with Not_found -> invalid_arg (err_incomplete_enum (List.map fst sl)) in - parse, print + let complete = complete ~complete:(fun _prefix -> List.map (fun (s, _) -> s, "") sl) () in + {parse; print; complete} let file = let parse s = match Sys.file_exists s with | true -> `Ok s | false -> `Error (err_no "file or directory" s) in - parse, pp_str + {parse; print=pp_str; complete=complete ~dir:true ~file:true ()} let dir = let parse s = match Sys.file_exists s with | true -> if Sys.is_directory s then `Ok s else `Error (err_not_dir s) | false -> `Error (err_no "directory" s) in - parse, pp_str + {parse; print=pp_str; complete=complete ~dir:true ()} let non_dir_file = let parse s = match Sys.file_exists s with | true -> if not (Sys.is_directory s) then `Ok s else `Error (err_is_dir s) | false -> `Error (err_no "file" s) in - parse, pp_str + {parse; print=pp_str; complete=complete ~file:true ()} let split_and_parse sep parse s = (* raises [Failure] *) let parse sub = match parse sub with @@ -257,25 +281,25 @@ let split_and_parse sep parse s = (* raises [Failure] *) in split [] (String.length s - 1) -let list ?(sep = ',') (parse, pp_e) = - let parse s = try `Ok (split_and_parse sep parse s) with +let list ?(sep = ',') conv = + let parse s = try `Ok (split_and_parse sep conv.parse s) with | Failure e -> `Error (err_element "list" s e) in let rec print ppf = function - | v :: l -> pp_e ppf v; if (l <> []) then (pp_char ppf sep; print ppf l) + | v :: l -> conv.print ppf v; if (l <> []) then (pp_char ppf sep; print ppf l) | [] -> () in - parse, print + {parse; print; complete=no_complete} -let array ?(sep = ',') (parse, pp_e) = - let parse s = try `Ok (Array.of_list (split_and_parse sep parse s)) with +let array ?(sep = ',') conv = + let parse s = try `Ok (Array.of_list (split_and_parse sep conv.parse s)) with | Failure e -> `Error (err_element "array" s e) in let print ppf v = let max = Array.length v - 1 in - for i = 0 to max do pp_e ppf v.(i); if i <> max then pp_char ppf sep done + for i = 0 to max do conv.print ppf v.(i); if i <> max then pp_char ppf sep done in - parse, print + {parse; print; complete=no_complete} let split_left sep s = try @@ -284,36 +308,36 @@ let split_left sep s = Some ((String.sub s 0 i), (String.sub s (i + 1) (len - i - 1))) with Not_found -> None -let pair ?(sep = ',') (pa0, pr0) (pa1, pr1) = - let parser s = match split_left sep s with +let pair ?(sep = ',') conv0 conv1 = + let parse s = match split_left sep s with | None -> `Error (err_sep_miss sep s) | Some (v0, v1) -> - match pa0 v0, pa1 v1 with + match conv0.parse v0, conv1.parse v1 with | `Ok v0, `Ok v1 -> `Ok (v0, v1) | `Error e, _ | _, `Error e -> `Error (err_element "pair" s e) in - let printer ppf (v0, v1) = pp ppf "%a%c%a" pr0 v0 sep pr1 v1 in - parser, printer + let print ppf (v0, v1) = pp ppf "%a%c%a" conv0.print v0 sep conv1.print v1 in + {parse; print; complete=no_complete} let t2 = pair -let t3 ?(sep = ',') (pa0, pr0) (pa1, pr1) (pa2, pr2) = +let t3 ?(sep = ',') conv0 conv1 conv2 = let parse s = match split_left sep s with | None -> `Error (err_sep_miss sep s) | Some (v0, s) -> match split_left sep s with | None -> `Error (err_sep_miss sep s) | Some (v1, v2) -> - match pa0 v0, pa1 v1, pa2 v2 with + match conv0.parse v0, conv1.parse v1, conv2.parse v2 with | `Ok v0, `Ok v1, `Ok v2 -> `Ok (v0, v1, v2) | `Error e, _, _ | _, `Error e, _ | _, _, `Error e -> `Error (err_element "triple" s e) in let print ppf (v0, v1, v2) = - pp ppf "%a%c%a%c%a" pr0 v0 sep pr1 v1 sep pr2 v2 + pp ppf "%a%c%a%c%a" conv0.print v0 sep conv1.print v1 sep conv2.print v2 in - parse, print + {parse; print; complete=no_complete} -let t4 ?(sep = ',') (pa0, pr0) (pa1, pr1) (pa2, pr2) (pa3, pr3) = +let t4 ?(sep = ',') conv0 conv1 conv2 conv3 = let parse s = match split_left sep s with | None -> `Error (err_sep_miss sep s) | Some(v0, s) -> @@ -323,15 +347,15 @@ let t4 ?(sep = ',') (pa0, pr0) (pa1, pr1) (pa2, pr2) (pa3, pr3) = match split_left sep s with | None -> `Error (err_sep_miss sep s) | Some (v2, v3) -> - match pa0 v0, pa1 v1, pa2 v2, pa3 v3 with + match conv0.parse v0, conv1.parse v1, conv2.parse v2, conv3.parse v3 with | `Ok v1, `Ok v2, `Ok v3, `Ok v4 -> `Ok (v1, v2, v3, v4) | `Error e, _, _, _ | _, `Error e, _, _ | _, _, `Error e, _ | _, _, _, `Error e -> `Error (err_element "quadruple" s e) in let print ppf (v0, v1, v2, v3) = - pp ppf "%a%c%a%c%a%c%a" pr0 v0 sep pr1 v1 sep pr2 v2 sep pr3 v3 + pp ppf "%a%c%a%c%a%c%a" conv0.print v0 sep conv1.print v1 sep conv2.print v2 sep conv3.print v3 in - parse, print + {parse; print; complete=no_complete} let env_bool_parse s = match String.lowercase_ascii s with | "" | "false" | "no" | "n" | "0" -> `Ok false @@ -339,3 +363,20 @@ let env_bool_parse s = match String.lowercase_ascii s with | s -> let alts = alts_str ~quoted:true ["true"; "yes"; "false"; "no" ] in `Error (err_invalid_val s alts) + +let string_has_prefix ~prefix s = + let prefix_len = String.length prefix in + let s_len = String.length s in + if prefix_len > s_len then false else + let rec loop i = + if i = prefix_len then true + else if String.get prefix i = String.get s i then loop (i + 1) + else false + in + loop 0 + +let string_drop_prefix ~prefix s = + if string_has_prefix ~prefix s then + let drop = String.length prefix in + Some (String.sub s drop (String.length s - drop)) + else None diff --git a/src/cmdliner_base.mli b/src/cmdliner_base.mli index 3b12e73..5bdec75 100644 --- a/src/cmdliner_base.mli +++ b/src/cmdliner_base.mli @@ -28,11 +28,27 @@ val err_unknown : val err_multi_def : kind:string -> string -> ('b -> string) -> 'b -> 'b -> string +(** {1:complete Completion strategies} *) + +type complete = { + complete_file : bool; + complete_dir : bool; + complete : (string -> (string * string) list); +} + +val no_complete : complete + +val complete : + ?file:bool -> + ?dir:bool -> + ?complete:(string -> (string * string) list) -> + unit -> complete + (** {1:conv Textual OCaml value converters} *) type 'a parser = string -> [ `Ok of 'a | `Error of string ] type 'a printer = Format.formatter -> 'a -> unit -type 'a conv = 'a parser * 'a printer +type 'a conv = {parse: 'a parser; print: 'a printer; complete: complete} val some : ?none:string -> 'a conv -> 'a option conv val some' : ?none:'a -> 'a conv -> 'a option conv @@ -58,3 +74,8 @@ val t4 : ('a * 'b * 'c * 'd) conv val env_bool_parse : bool parser + +val is_space : char -> bool + +val string_has_prefix : prefix:string -> string -> bool +val string_drop_prefix : prefix:string -> string -> string option diff --git a/src/cmdliner_cline.ml b/src/cmdliner_cline.ml index cc81702..132a94a 100644 --- a/src/cmdliner_cline.ml +++ b/src/cmdliner_cline.ml @@ -39,7 +39,7 @@ let arg_info_indexes args = their arg_info, a list with all arg_info for positional arguments and a cmdline mapping each arg_info to an empty [arg]. *) let rec loop optidx posidx cl = function - | [] -> optidx, posidx, cl + | [] -> optidx, List.rev posidx, cl | a :: l -> match Cmdliner_info.Arg.is_pos a with | true -> loop optidx (a :: posidx) (Amap.add a (P []) cl) l @@ -87,6 +87,14 @@ let hint_matching_opt optidx s = | true, [] -> [short_opt] | true, l -> if List.mem short_opt l then l else short_opt :: l +let complete_prefix = "+cmdliner_complete:" + +let maybe_complete_token s = + Cmdliner_base.string_drop_prefix ~prefix:complete_prefix s + +exception Completion_requested of + string * [ `Opt of Cmdliner_info.Arg.t | `Arg of Cmdliner_info.Arg.t | `Any ] + let parse_opt_args ~peek_opts optidx cl args = (* returns an updated [cl] cmdline according to the options found in [args] with the trie index [optidx]. Positional arguments are returned in order @@ -109,8 +117,11 @@ let parse_opt_args ~peek_opts optidx cl args = | [] -> None, args | v :: rest -> if is_opt v then None, args else Some v, rest in + (match Option.bind value maybe_complete_token with + | Some prefix -> raise (Completion_requested (prefix, `Opt a)) + | None -> let arg = O ((k, name, value) :: opt_arg cl a) in - loop errs (k + 1) (Amap.add a arg cl) pargs args + loop errs (k + 1) (Amap.add a arg cl) pargs args) | `Not_found when peek_opts -> loop errs (k + 1) cl pargs args | `Not_found -> let hints = hint_matching_opt optidx s in @@ -129,11 +140,14 @@ let parse_opt_args ~peek_opts optidx cl args = let take_range start stop l = let rec loop i acc = function - | [] -> List.rev acc + | [] -> `Range (List.rev acc) | v :: vs -> + match maybe_complete_token v with + | Some prefix -> `Complete prefix + | None -> if i < start then loop (i + 1) acc vs else if i <= stop then loop (i + 1) (v :: acc) vs else - List.rev acc + `Range (List.rev acc) in loop 0 [] l @@ -159,7 +173,11 @@ let process_pos_args posidx cl pargs = | Some n -> pos rev (Cmdliner_info.Arg.pos_start apos + n - 1) in let start, stop = if rev then stop, start else start, stop in - let args = take_range start stop pargs in + let args = + match take_range start stop pargs with + | `Range args -> args + | `Complete prefix -> raise (Completion_requested (prefix, `Arg a)) + in let max_spec = max stop max_spec in let cl = Amap.add a (P args) cl in let misses = match Cmdliner_info.Arg.is_req a && args = [] with @@ -169,17 +187,30 @@ let process_pos_args posidx cl pargs = loop misses cl max_spec al in let misses, cl, max_spec = loop [] cl (-1) posidx in - if misses <> [] then Error (Cmdliner_msg.err_pos_misses misses, cl) else + let consume_excess () = + match take_range (max_spec + 1) last pargs with + | `Range args -> args + | `Complete prefix -> raise (Completion_requested (prefix, `Any)) + in + if misses <> [] then ( + let _ : string list = consume_excess () in + Error (Cmdliner_msg.err_pos_misses misses, cl)) else if last <= max_spec then Ok cl else - let excess = take_range (max_spec + 1) last pargs in - Error (Cmdliner_msg.err_pos_excess excess, cl) + Error (Cmdliner_msg.err_pos_excess (consume_excess ()), cl) let create ?(peek_opts = false) al args = let optidx, posidx, cl = arg_info_indexes al in + try match parse_opt_args ~peek_opts optidx cl args with - | Ok (cl, _) when peek_opts -> Ok cl - | Ok (cl, pargs) -> process_pos_args posidx cl pargs - | Error (errs, cl, _) -> Error (errs, cl) + | Ok (cl, _) when peek_opts -> `Ok cl + | Ok (cl, pargs) -> + (match process_pos_args posidx cl pargs with + | Ok v -> `Ok v + | Error v -> `Error v) + | Error (errs, cl, pargs) -> + let _ : _ result = process_pos_args posidx cl pargs in + `Error (errs, cl) + with Completion_requested (prefix, kind) -> `Completion (prefix, kind) let deprecated_msgs cl = let add i arg acc = match Cmdliner_info.Arg.deprecated i with diff --git a/src/cmdliner_cline.mli b/src/cmdliner_cline.mli index f9075b0..22844ca 100644 --- a/src/cmdliner_cline.mli +++ b/src/cmdliner_cline.mli @@ -9,7 +9,10 @@ type t val create : ?peek_opts:bool -> Cmdliner_info.Arg.Set.t -> string list -> - (t, string * t) result + [ `Ok of t + | `Completion of + string * [ `Opt of Cmdliner_info.Arg.t | `Arg of Cmdliner_info.Arg.t | `Any ] + | `Error of string * t ] val opt_arg : t -> Cmdliner_info.Arg.t -> (int * string * (string option)) list val pos_arg : t -> Cmdliner_info.Arg.t -> string list diff --git a/src/cmdliner_docgen.ml b/src/cmdliner_docgen.ml index 3a36df5..f91a550 100644 --- a/src/cmdliner_docgen.ml +++ b/src/cmdliner_docgen.ml @@ -116,11 +116,11 @@ let synopsis ?parents cmd = match Cmdliner_info.Cmd.children cmd with Cmdliner_info.Arg.rev_pos_cli_order a0 a1 in let args = Cmdliner_info.Cmd.args cmd in - let oargs, pargs = Cmdliner_info.Arg.(Set.partition is_opt args) in + let oargs, pargs = Cmdliner_info.Arg.(Set.partition (fun a _ -> is_opt a) args) in let oargs = (* Keep only those that are listed in the s_options section and that are not [--version] or [--help]. * *) - let keep a = + let keep a _ = let drop_names n = n = "--help" || n = "--version" in Cmdliner_info.Arg.docs a = Cmdliner_manpage.s_options && not (List.exists drop_names (Cmdliner_info.Arg.opt_names a)) @@ -223,7 +223,7 @@ let arg_docs ~errs ~subst ~buf ei = in if c <> 0 then c else order_args a0 a1 in - let keep_arg a acc = + let keep_arg a _ acc = if not Cmdliner_info.Arg.(is_pos a && (docv a = "" || doc a = "")) then (a :: acc) else acc in @@ -274,7 +274,7 @@ let env_docs ~errs ~subst ~buf ~has_senv ei = let envs = (Cmdliner_info.Env.info_docs e, `I (var, doc)) :: envs in seen, envs in - let add_arg_env a acc = match Cmdliner_info.Arg.env a with + let add_arg_env a _ acc = match Cmdliner_info.Arg.env a with | None -> acc | Some e -> add_env_item ~subst:(arg_info_subst ~subst a) acc e in diff --git a/src/cmdliner_eval.ml b/src/cmdliner_eval.ml index e4b50be..8794b1a 100644 --- a/src/cmdliner_eval.ml +++ b/src/cmdliner_eval.ml @@ -116,13 +116,13 @@ let find_term args cmd = let args = List.rev_append args_rev args_rest in match (cmd : 'a Cmdliner_cmd.t) with | Cmd (i, t) -> - args, t, i, parents, Ok () + args, t, i, parents, [], Ok () | Group (i, (Some t, children)) -> - args, t, i, parents, Ok () + args, t, i, parents, children, Ok () | Group (i, (None, children)) -> let dom = cmd_name_dom children in let err = Cmdliner_msg.err_cmd_missing ~dom in - args, never_term, i, parents, Error err + args, never_term, i, parents, children, Error err in let rec loop args_rev parents cmd = function | ("--" :: _ | [] as rest) -> stop rest args_rev parents cmd @@ -132,7 +132,7 @@ let find_term args cmd = match cmd with | Cmd (i, t) -> let args = List.rev_append args_rev (arg :: args) in - args, t, i, parents, Ok () + args, t, i, parents, [], Ok () | Group (i, (t, children)) -> let index = cmd_name_trie children in match Cmdliner_trie.find index arg with @@ -144,13 +144,13 @@ let find_term args cmd = let dom = cmd_name_dom children in let kind = "command" in let err = Cmdliner_base.err_unknown ~kind ~dom ~hints arg in - args, never_term, i, parents, Error err + args, never_term, i, parents, children, Error err | `Ambiguous -> let args = List.rev_append args_rev (arg :: args) in let ambs = Cmdliner_trie.ambiguities index arg in let ambs = List.sort compare ambs in let err = Cmdliner_base.err_ambiguous ~kind:"command" arg ~ambs in - args, never_term, i, parents, Error err + args, never_term, i, parents, children, Error err in loop [] [] cmd args @@ -170,19 +170,79 @@ let do_deprecated_msgs err_ppf cl ei = if msgs <> [] then Cmdliner_msg.pp_err err_ppf ei ~err:(String.concat "\n" msgs) +module Complete = struct + + let file () = print_endline "file" + let dir () = print_endline "dir" + + let group fmt = + print_endline "group"; + Printf.ksprintf print_endline fmt + + let item ~prefix (name, doc) = + if Cmdliner_base.string_has_prefix ~prefix name then ( + print_endline "item"; + print_endline name; + let doc = String.map (fun c -> if Cmdliner_base.is_space c then ' ' else c) doc in + print_endline doc) +end + +let handle_completion args cmd cmd_children (prefix, kind) = + let complete_arg_names () = + Complete.group "Options"; + let args = Cmdliner_info.Cmd.args cmd in + Cmdliner_info.Arg.Set.iter (fun arg _ -> + let names = Cmdliner_info.Arg.opt_names arg in + let doc = Cmdliner_info.Arg.doc arg in + List.iter (fun name -> + Complete.item ~prefix (name, doc)) names) + args; + in + let complete_arg_values arg = + match Cmdliner_info.Arg.Set.find_opt arg args with + | None -> () + | Some { complete_file; complete_dir; complete } -> + Complete.group "Values"; + List.iter (Complete.item ~prefix) (complete prefix); + if complete_file then Complete.file (); + if complete_dir then Complete.dir () + in + let complete_subcommands () = + Complete.group "Subcommands"; + List.iter (fun cmd -> + let info = Cmdliner_cmd.get_info cmd in + let name = Cmdliner_info.Cmd.name info in + let doc = Cmdliner_info.Cmd.doc info in + Complete.item ~prefix (name, doc)) + cmd_children + in + (match kind with + | `Opt a -> + complete_arg_values a + | `Arg a -> + complete_arg_values a; + complete_arg_names () + | `Any -> + (match cmd_children with + | [] -> complete_arg_names () + | _ -> complete_subcommands ())); + exit 0 + let eval_value ?help:(help_ppf = Format.std_formatter) ?err:(err_ppf = Format.err_formatter) ?(catch = true) ?(env = env_default) ?(argv = Sys.argv) cmd = - let args, f, cmd, parents, res = find_term (remove_exec argv) cmd in + let args, f, cmd, parents, children, res = find_term (remove_exec argv) cmd in let ei = Cmdliner_info.Eval.v ~cmd ~parents ~env ~err_ppf in let help, version, ei = add_stdopts ei in let term_args = Cmdliner_info.Cmd.args @@ Cmdliner_info.Eval.cmd ei in let res = match res with | Error msg -> (* Command lookup error, we still prioritize stdargs *) let cl = match Cmdliner_cline.create term_args args with - | Error (_, cl) -> cl | Ok cl -> cl + | `Completion compl -> handle_completion term_args cmd children compl + | `Error (_, cl) -> cl + | `Ok cl -> cl in begin match try_eval_stdopts ~catch ei cl help version with | Some e -> e @@ -190,12 +250,13 @@ let eval_value end | Ok () -> match Cmdliner_cline.create term_args args with - | Error (e, cl) -> + | `Completion compl -> handle_completion term_args cmd children compl + | `Error (e, cl) -> begin match try_eval_stdopts ~catch ei cl help version with | Some e -> e | None -> Error (`Error (true, e)) end - | Ok cl -> + | `Ok cl -> match try_eval_stdopts ~catch ei cl help version with | Some e -> e | None -> @@ -219,12 +280,13 @@ let eval_peek_opts let cli_args = remove_exec argv in let v, ret = match Cmdliner_cline.create ~peek_opts:true term_args cli_args with - | Error (e, cl) -> + | `Completion arg -> failwith "TODO: eval_peek_opts" + | `Error (e, cl) -> begin match try_eval_stdopts ~catch:true ei cl help version with | Some e -> None, e | None -> None, Error (`Error (true, e)) end - | Ok cl -> + | `Ok cl -> let ret = run_parser ~catch:true ei cl f in let v = match ret with Ok v -> Some v | Error _ -> None in match try_eval_stdopts ~catch:true ei cl help version with diff --git a/src/cmdliner_info.ml b/src/cmdliner_info.ml index 561a60e..0f1e440 100644 --- a/src/cmdliner_info.ml +++ b/src/cmdliner_info.ml @@ -153,7 +153,26 @@ module Arg = struct let rev_pos_cli_order a0 a1 = pos_cli_order a1 a0 let compare a0 a1 = Int.compare a0.id a1.id - module Set = Set.Make (struct type nonrec t = t let compare = compare end) + + module Set = struct + type arg = t + type complete = Cmdliner_base.complete + + module Map = Map.Make (struct type t = arg let compare = compare end) + include Map + + type t = Cmdliner_base.complete Map.t + + let find_opt k m = try Some (Map.find k m) with Not_found -> None + + let elements m = List.map fst (bindings m) + + let union a b = + Map.merge (fun k v v' -> + match v, v' with + | Some v, _ | _, Some v -> Some v + | None, None -> assert false) a b + end end (* Commands *) diff --git a/src/cmdliner_info.mli b/src/cmdliner_info.mli index 76ea15b..96c3311 100644 --- a/src/cmdliner_info.mli +++ b/src/cmdliner_info.mli @@ -64,7 +64,8 @@ module Arg : sig type t val v : ?deprecated:string -> ?absent:string -> ?docs:string -> ?docv:string -> - ?doc:string -> ?env:Env.info -> string list -> t + ?doc:string -> ?env:Env.info -> + string list -> t val id : t -> int val deprecated : t -> string option @@ -93,7 +94,25 @@ module Arg : sig val rev_pos_cli_order : t -> t -> int val compare : t -> t -> int - module Set : Set.S with type elt = t + + module Set : sig + type arg = t + type complete = Cmdliner_base.complete + + type t + + val empty : t + val add : arg -> complete -> t -> t + val choose : t -> arg * complete + val partition : (arg -> complete -> bool) -> t -> t * t + val filter : (arg -> complete -> bool) -> t -> t + val iter : (arg -> complete -> unit) -> t -> unit + val singleton : arg -> complete -> t + val fold : (arg -> complete -> 'acc -> 'acc) -> t -> 'acc -> 'acc + val elements : t -> arg list + val union : t -> t -> t + val find_opt : arg -> t -> complete option + end end (** Commands. *) diff --git a/src/cmdliner_term.ml b/src/cmdliner_term.ml index fd34e13..b77d2e1 100644 --- a/src/cmdliner_term.ml +++ b/src/cmdliner_term.ml @@ -81,7 +81,7 @@ let with_used_args (al, v) : (_ * string list) t = al, fun ei cl -> match v ei cl with | Ok x -> - let actual_args arg_info acc = + let actual_args arg_info _ acc = let args = Cmdliner_cline.actual_args cl arg_info in List.rev_append args acc in diff --git a/test/darcs_ex.ml b/test/darcs_ex.ml index 12a74ba..ad89ac9 100644 --- a/test/darcs_ex.ml +++ b/test/darcs_ex.ml @@ -27,12 +27,13 @@ let help copts man_format cmds topic = match topic with | None -> `Help (`Pager, None) (* help about the program. *) | Some topic -> let topics = "topics" :: "patterns" :: "environment" :: cmds in - let conv, _ = Cmdliner.Arg.enum (List.rev_map (fun s -> (s, s)) topics) in - match conv topic with - | `Error e -> `Error (false, e) - | `Ok t when t = "topics" -> List.iter print_endline topics; `Ok () - | `Ok t when List.mem t cmds -> `Help (man_format, Some t) - | `Ok t -> + let conv = Cmdliner.Arg.enum (List.rev_map (fun s -> (s, s)) topics) in + let parse = Cmdliner.Arg.conv_parser conv in + match parse topic with + | Error (`Msg e) -> `Error (false, e) + | Ok t when t = "topics" -> List.iter print_endline topics; `Ok () + | Ok t when List.mem t cmds -> `Help (man_format, Some t) + | Ok t -> let page = (topic, 7, "", "", ""), [`S topic; `P "Say something";] in `Ok (Cmdliner.Manpage.print man_format Format.std_formatter page)