Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for shell completion #187

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
53 changes: 53 additions & 0 deletions bin/cmdliner_completion.ml
Original file line number Diff line number Diff line change
@@ -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;;
26 changes: 26 additions & 0 deletions bin/dune
Original file line number Diff line number Diff line change
@@ -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
))))

38 changes: 38 additions & 0 deletions bin/main.ml
Original file line number Diff line number Diff line change
@@ -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)
41 changes: 32 additions & 9 deletions build.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 *) ]
Expand Down Expand Up @@ -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
Expand All @@ -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"])

Expand All @@ -134,19 +154,22 @@ 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;
List.iter cp srcs;
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 ->
Expand Down
2 changes: 1 addition & 1 deletion cmdliner.opam
Original file line number Diff line number Diff line change
Expand Up @@ -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"
46 changes: 46 additions & 0 deletions completion/PROTOCOL.md
Original file line number Diff line number Diff line change
@@ -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


25 changes: 25 additions & 0 deletions completion/bash-completion.sh
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions completion/zsh-completion.sh
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions dune-project
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
(lang dune 1.4)
(name cmdliner)
(lang dune 2.7)
(name cmdliner)
7 changes: 7 additions & 0 deletions example/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
(copy_files
(files ../test/*_ex.ml))


(executables
(names cp_ex rm_ex darcs_ex tail_ex)
(libraries cmdliner))
Loading