Writing Zsh Completion for Padrino

Posted on , updated by

This article is part of an ongoing series about my exploration of the Padrino web framework. If you want to read more about it please check out padrinobook.com. You sniff around in the sources of the book under GitHub.


Sign Up For The Newsletter About



I’m a user of the Z shell (zsh). It is easy to customize your prompt and to have more abilities for tab completion of the most used programs with this shell. If you are lazy or new to a new command line tool with a huge amount of options, this will help you in the beginning to remember the commands. When you use git you can press tab to see all available options you have in the current context. The same functions aren’t available for Padrino and this is how I came to this article.

Start Small with Basic tmuxinator Completion

Your first step is to create a file with the name of the command for which you want to have autocompletion. The name of the command must begin with an underscore and needs to be in your fpath. The fpath is the variable which will be used for shell searches when the autoload function is first called. Let’s enable this feature in your .zshrc:

# folder of all of your autocomplete functions
fpath=($HOME/.zsh-completions $fpath)

# enable autocomplete function
autoload -U compinit
compinit

Before going on, you need to install the tmuxinator gem with:

$ gem install tmuxinator

Let’s implement the hello-tmux and version commands for the tmuxinator gem. Inside your ~/.zsh-completions/_tmuxinator file:

#compdef tmuxinator
_tmuxinator() {
  local -a commands
  commands=(
    'hello-tmux:our first autocompletion function.'
    'version:show the used gem version.'
  )

  if (( CURRENT == 2 )); then
    _describe -t commands 'commands' commands
  fi

  return 0
}

_tmuxinator

Now reload your shell with exec zsh and type in $ tmuxinator <Tab>. Voilà, you’ve written you first autocompletion. I will explain part of the code in the next chapters in case you don’t know each detail of the code.

Autocompletion for Padrino

First of all we need to specify the name of the command for which we want to have for Padrino’s autocompletion:

#compdef padrino
typeset -A opt_args

Please note, that the first line must be comment with the compdef command followed by the name of the command. With the help of the typeset command we can set and display attributes and values for shell parameters. The -A options says, that names we pass into the autocomplete function refers to associative array parameters. opt_args makes it possible to add command-line options like -d or -f for our associative array.

Next, we are going to define the inputs of our function. We need this to be able to define states in order to get the context in which we are during our completion:

_arguments -C \
  '1:cmd:->cmds' \
  '2:generators:->generator_lists' \
  '*:: :->args' \
&& ret=0

The _arguments function is used to give a complete specification for a command whose arguments follow standard UNIX option and argument conventions. _arguments returns status zero if it was able to add matches and non-zero otherwise. The option -C makes it possible to modify the curcontext parameter. With the help of curcontext it is possible to keep track of the current context. This will be used later in our big state machine like case construct.

1:cmd:->cmds describes the first argument in our completion function. The cmd message will be printed above matches generated and the cmds action indicates what can be completed in this position. Similar, 2:generators:->generator_lists stands for the second argument with the generator_lists option. At last, we need to be able to pass in certain arguments whatever position we are in. For this case we are using *:: :->args. The two colons before any message of the command array indicates that the current context parameter refer only to the normal arguments when the action is evaluated.

Padrino Generators

Everything is set and we need to define Padrino’s command for autocompletion. Padrino uses the g option to specify the command for using a generator. After we are in this context we can chose a generator like controller, mailer, migration, model, plugin or project. Depending which generator we take, we can use different, context specific arguments like -a for the mailer generator to create a mailer for the specified sub application or the -d option for defining the orm adapter when using the project generator.

The first argument in Padrino autocompletion, 1:cmd:->cmds, is created with the help of a case statement:

#compdef padrino

typeset -A opt_args

_arguments -C \
  '1:cmd:->cmds' \
  '2:generators:->generator_lists' \
  '*:: :->args' \
&& ret=0


case "$state" in
  (cmds)
     local commands; commands=(
      'g:Padrinos generators'
     )

      _describe -t commands 'command' commands && ret=0
  ;;
ease;

return 1;

Let’s go through the parts:

  • case "$state" - The beginning of our state statement. The $state variable is used for tracking the current state we are in.
  • local commands - Definition of a variable which is assigned as a list of strings for our completion function. After the name of the command follows the explanation of the command.
  • _describe -t - The describe functions associates completions with description. All it does is taking our previously defined commands as a list variable commands for zsh compsys function and return 0 to indicate that error occurs. The -t option specify the tag we want to use we want to use in our describe function. In our case we are tagging the local commands for the completions.

Now we know that we are going to use a generator we need to find out which generator want to use in the completion. This is the '2:generators:->generator_lists' part of the _arguments function:

#compdef padrino

typeset -A opt_args

_arguments -C \
  '1:cmd:->cmds' \
  '2:generators:->generator_lists' \
  '*:: :->args' \
&& ret=0

case "$state" in
  (cmds)
     local commands; commands=(
      'g:Padrinos generators'
     )

      _describe -t commands 'command' commands && ret=0
  ;;
  (generator_lists)
    local generators; generators=(
      'controller:creates a new controller'
      'mailer:creates a new mailer'
      'migration:creates a new migration'
      'model:creates a new model'
      'project:create a new Padrino app'
      'plugin:add plugin to your app'
    )
    _describe -t generators 'generator' generators && ret=0
  ;;
esac

return 1

The code above follows nearly the same syntax as the cmds command before. The last thing we need to handle are the optional arguments for each generate state, as defined *:: :->args (imagine this thing as the default or else branch of your control structure):

#compdef padrino

typeset -A opt_args

_arguments -C \
  '1:cmd:->cmds' \
  '2:generators:->generator_lists' \
  '*:: :->args' \
&& ret=0

case "$state" in
  (cmds)
     local commands; commands=(
      'g:Padrinos generators'
     )

      _describe -t commands 'command' commands && ret=0
  ;;
  (generator_lists)
    local generators; generators=(
      'controller:creates a new controller'
      'mailer:creates a new mailer'
      'migration:creates a new migration'
      'model:creates a new model'
      'project:create a new Padrino app'
      'plugin:add plugin to your app'
    )
    _describe -t generators 'generator' generators && ret=0
  ;;
  (args)
    case $line[2] in
      (controller)
        _arguments \
          '-d:remove all generated files' \
          '-n:specify the application' \
          '-r:specify the root'
          ret=0
      ;;
      (mailer)
        local mailers; mailers=(
          '-a:creates a mailer for the specified subapp'
          '-d:removes all generated files'
          '-n:specify the application'
          '-r:specify the root destination path'
        )
        _describe -t mailers 'mailer' mailers && ret=0
      ;;
      ...
      ;;
    esac
  ;;
esac

return 1

If our second inline parameter $line[2] is controller then we have the -d, -n, and -r options. One the other hand if the second inline parameter is mailer the options -a, -d, -n, and -r are available. You can find the whole implementation code under GitHub.

Conclusion

The benefit of writing function completions for zsh is that it is available on nearly every platform. All you need is to have the zsh installed on your machine - there are no other dependencies. Documentation about writing is available but there aren’t many practical examples. Look and learn from other how used it and then experiment with it. After some time you’ll get the smell how to do it. And please, if you are writing something complex, write a blog post about your completion function to help other getting into it.

Further Reading


Sign Up For The Newsletter About



Hierarchy: previous post , next post
Tags: padrino, zsh