Cmdliner completions

Howdy, I’m pretty green to ocaml so I decided to take some of my existing bash scripts and rewrite them using ocaml. I wrote a cli for one of them in the hopes of making it more idiomatic to have completions. Currently I am using cmdliner and it is pretty sweet, but I’m confused at how to get the completions working. According to the docs, using dirpath should auto complete directories without checks, that’s a great start for what I’m doing and I want to eventually do something more complex. However, after compiling it, I can’t seem to get the completion to trigger. Any tips are welcome and appreciated. Here is the bit that I’m referring to:

let target_dir =
let doc = “Target directory, optionally with #devShell” in
Arg.(required & pos 0 (some dirpath) None & info [] ~docv:“DIR[#ATTR]” ~doc)
;;

let cmd =
let doc = “Enter a nix shell in the target directory” in
let info = Cmd.info “ns” ~doc ~version in
Cmd.v info Term.(const main $ target_dir)
;;

How did you setup completions in your shell? Here’s what I do with zsh.

First I add a bin/dune rule to build a completion script for me:

  (rule
   (target _mach)
   (mode promote)
   (action
    (with-stdout-to
     %{target}
     (progn
      (run cmdliner tool-completion --standalone-completion zsh mach)))))

This builds _mach file with zsh shell completion driver for an executable called mach. Next I need to configure FPATH environment variable to point to a dir which has this _mach file (while developing I just put it in .envrc like this):

export FPATH="$PWD/bin:$FPATH"

This then should work completing mach <TAB> and similar lines.

1 Like

Oh, interesting. I didn’t see this in the docs but that makes sense. Previously, I’ve used cargo clap-completions and it just spits out a script to stdout and I eval that. It rebuilds it on changes using a build.rs file, so nearly the same type of thing.

Well, I was able to generate the file like this and I did try to export the FPATH as well but that didn’t seem to do it.

Oh, I forgot to mention, you need to run compinit after changing FPATH.

This is actually in my shell hook:

autoload -Uz compinit add-zsh-hook

recompinit() {
  unset _comps
  compinit
}

recompinit

# Auto re-init compinit if FPATH changes
typeset -g __fpath_snapshot="$FPATH"
_auto_compinit_if_fpath_changed() {
  if [[ "$FPATH" != "$__fpath_snapshot" ]]; then
    recompinit
    __fpath_snapshot="$FPATH"
  fi
}
add-zsh-hook precmd _auto_compinit_if_fpath_changed

But I don’t know, maybe there’s a better way to load/unload zsh completions dynamically.

Is there any other docs on generating completions for cmdliner? Or just docs about this in general?

For some reason it wasn’t showing up before… Anyhow, this works lol

            postInstall = ''
              mkdir -p $out/bin/completions
              ${cmdliner}/bin/cmdliner tool-completion --standalone-completion bash ns > $out/bin/completions/ns
              ${cmdliner}/bin/cmdliner tool-completion --standalone-completion zsh ns > $out/bin/completions/_ns
            '';
1 Like

It’s a bit unclear to me the context in which you are trying to make things work. But the end user documentation for completion is here. Did you go through this ?

@andreypopp if you already have the cmdliner generic support in your environment what you are doing is needlessly complicated. As mentioned in the last paragraph here. You just need to invoke:

autoload _cmdliner_generic
compdef _cmdliner_generic mach

I prefer to produce standalone completion scripts — this makes it so cmdliner is not needed to be installed and found on $PATH. In fact for some of the tools, I even vendor cmdliner sources as an amalgamated cmdliner.ml.

The shell hook is a complicated one, I agree but it is made so I can switch between projects and direnv will make sure to load/unload completions.

Otherwise it is (1) a dune rule to produce scripts and (2) dune install sections for the scripts. Doesn’t look complicated to me. Although I’d prefer dune to make it for me — maybe we can have (cmdliner_completion) thing in (executable) stanza?

This is a misunderstanding. You don’t need the cmdliner tool in your PATH you need the generic cmdliner completion script to be in your completion environment.

With a properly setup environment the direnv is all you need.

You are right, no need for the cmdliner tool on PATH, but still need generic cmdliner script from cmdliner package.

With a properly setup environment the direnv is all you need.

Hm… I wasn’t able to load/unload completions property without the shell hook, purely with direnv. If you can share your setup, I’d be curious to see.

But your direnv likely extends variables to look in the dev env’s unix prefix to lookup the completion of these other tools you are using. The generic cmdliner script will be there too.

I don’t know, to each one their setup :–) But before people start adding ad-hoc rules to their projects along the lines they found above I’d just like to mention that the recommended way of installing completion (and manpages) for your tools with dune is documented here.

I meant the direnv machinery, not sure if your shell hook is needed :–)

1 Like

That’s a good point, I should have looked at the docs before sharing my setup.

But still I wish dune would handle this machinery automatically for (executable (with_cmdliner) ...)or something like this.

Here’s how I ended up doing it, nix handles discovery of the completions for me if I just put them in the correct place in the nix-store:

I was struggling a bit to find what I was looking for, but I since found it!

The only thing I had to do for a change to take effect was close my current shell and open a new one, but I can see how for fast iteration that would be a bit annoying. For me, it’s convenient enough. I don’t have to manage anything, nix just figures it out automatically for me.

Not sure exactly in which context this happens but if it’s meant for driving installs of your tool this would be a better idea:

${cmdliner}/bin/cmdliner install tool-support ns $out/

(Possibly with --standalone-completion, but note that it may be slower in some shells)

This will also install man page in the prefix and add support for more shells as they are added to cmdliner (e.g. you did not install powershell’s completions). Directories are created as needed by the tool. Invoke with --dry-run for a description of the effects.

2 Likes

Out of curiosity, what makes the standalone script slower, and in which shells? Nix doesn’t support powershell, which is why I just selected the two which both nix and cmdliner support.

I don’t remember (and don’t really want :–), I wrote that in the doc string of --standalone-completion. I think some shells reparse/execute the whole completion support file on each completion and since the completion function is there they have to reparse it aswell.

From a maintenance perspective you are just better off using tool-support. Btw. I’m not familiar with nix but isn’t that powershell on nix ?