Better Shell Completion for Your Tools

In November 2015 I started my App::Spec commandline framework and wrote this blogpost.

It's not only a framework for perl. It can also generate shell tab completion for other tools.

Since then I have been busy with other things, but recently continued working on it for several reasons, and fixed several bugs, mostly for bash.

Last year I started a collection of generated completion scripts for bash and zsh:

https://github.com/perlpunk/shell-completions

Today it contains completions for 20 tools, mostly for perl commands. If you miss a tool there, let me know, or try to write your own YAML specification and generate the completion.

Below you will see some examples.

Features

It supports tools with nested subcommands, option and parameter completion.

Zsh has a builtin feature for showing descriptions; I built something similar for bash.

Completing subcommands:

bash $ fatpack <TAB><TAB>
file          -- Recurses into the lib and fatlib directories and bundles all .pm files found into a BEGIN block...
help          -- Show command help
packlists-for -- Searches your perls @INC for .packlist files containing the .pm files
pack          -- Pack script and modules
trace         -- Writes out a trace file containing every module required
tree          -- Takes a list of packlist files and copies their contents into a tree at the requested location

bash $ dzil <TAB><TAB>
add        -- add modules to an existing dist
authordeps -- list your distributions author dependencies
build      -- build your dist
clean      -- clean up after build, test, or install
commands   -- list the applications commands
help       -- Show command help
install    -- install your dist
listdeps   -- print your distributions prerequisites
new        -- mint a new dist
nop        -- do nothing: initialize dzil, then exit
release    -- release your dist
run        -- run stuff in a dir where your dist is built
setup      -- set up a basic global config file
smoke      -- smoke your dist
test       -- test your dist

Completing options:

bash $ fatpack trace -<TAB><TAB>
--help      -- Show command help
-h          -- Show command help
--to        -- Location of trace file
--to-stderr -- Write the trace to STDERR instead
--use       -- Specify module(s) to be included additionally

bash $ dzil build -<TAB><TAB>
--help           -- Show command help
-h               -- Show command help
-I               -- additional @INC dirs
--in             -- the directory in which to build the distribution
--lib-inc        -- additional @INC dirs
--tgz            -- build a tarball (default behavior)
--trial          -- build a trial release that PAUSE will not index
--verbose        -- log additional output
--verbose-plugin -- log additional output from some plugins only
-v               -- log additional output
-V               -- log additional output from some plugins only

As you can see, for example --help and -h show up on their own lines, which is a disadvantage of this feature and can make completion very verbose. I might add an option to disable descriptions for bash.

This is handled much better in zsh:

zsh % dzil build -<TAB>
Completing option
--help            -h  -- Show command help
--in                  -- the directory in which to build the distribution
--lib-inc         -I  -- additional @INC dirs
--tgz                 -- build a tarball (default behavior)
--trial               -- build a trial release that PAUSE will not index
--verbose         -v  -- log additional output
--verbose-plugin  -V  -- log additional output from some plugins only

Completing enums (for options or parameters):

bash $ plackup -L <TAB><TAB>
Delayed        Plack::Loader  Restarter      Shotgun

The corresponding lines in the spec:

- name: loader
  type: string
  enum: [Plack::Loader, Restarter, Delayed, Shotgun]
  summary: Specifies the server loading subclass
  aliases: [L]

Dynamic completion

You can also specify an external command for completion. When the -M switch for prove is completed, it calls a command to find all installed modules in @INC:

bash $ prove -M Parse::<TAB><TAB>
ANSIColor::Tiny   BBCode::HTML      BBCode::Tag       BBCode::XHTML     RecDescent
BBCode            BBCode::Markdown  BBCode::Text      CPAN::Meta

The corresponding lines in the spec:

- name: M
  summary: Load a module
  type: string
  multiple: true
  completion:
    # TODO filter directories like x86_64-linux
    command_string: |-
        \
        for incpath in $(perl -wE'say for @INC'); do \
          find $incpath -name "*.pm" -printf "%P\n" \
          | perl -plE's{/}{::}g; s{\.pm}{}' \
          | grep "^$CURRENT_WORD"; \
        done

When adding such custom callbacks, it's important to use a syntax which is bash and zsh compatible.

For the module completion for cpan and cpanm I used Ingy's trick with grepping the 02packages.details.txt.gz file in your cpan/cpanm directory.

How it works

The specification for a command is written in YAML. The appspec tool can then generate completions from it.

YAML comes in very handy especially if some subcommands have the same options. You don't need to repeat them but can use the YAML alias feature.

# specs/dzil.yaml
# [...]
  build:
    summary: build your dist
    options:
    - &trial trial --build a trial release that PAUSE will not index
    - tgz          --build a tarball (default behavior)
    - in=s         --the directory in which to build the distribution
# [...]
  release:
    summary: release your dist
    options:
    - *trial   # Reuse option from above

At the beginning the format was quite verbose for simple options:

options:
- name: proxy
  summary: Set HTTP proxy
  aliases: [p]
  type: string

At some point I added a shorter syntax, which was inspired by the Getopt::Long syntax and Ingy's Schematype syntax.

options:
- proxy|p=s   --Set HTTP proxy

Usage

See the usage instructions at shell-completions.

For zsh, you can just add the path to the $fpath variable. Completions in this directory will not be loaded every time you start a zsh session, but they will be autoloaded when the command is first used.

For bash it might be better to just pick the completions you really want to use.

TODOs

There are still problems with special characters like quotes and spaces. They have to be escaped correctly, and unfortunately this works different in bash and zsh.

Some months ago Ingy started a similar project: complete-shell.

While it has the same goal, adding completions for existing commands, it works quite differently.

Try out both and report bugs and feature requests to us ;-)

Leave a comment

About tinita

user-pic just another perl punk,