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