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

Automatically generate autocompletion for bash/zsh #248

Open
mahmoudimus opened this Issue Apr 22, 2014 · 11 comments

Comments

Projects
None yet
4 participants
@mahmoudimus

mahmoudimus commented Apr 22, 2014

I'm a heavy bash user and we have a ton of cli tools, I'm evaluating cli frameworks, cement is incredibly powerful, but it would be amazing if it could generate autocompletions for the cli tool.

@derks derks added feature labels Apr 22, 2014

@derks derks self-assigned this Apr 22, 2014

@derks

This comment has been minimized.

Show comment
Hide comment
@derks

derks Apr 22, 2014

Member

Definitely! I've had that thought before, just haven't had a chance to investigate it.

Member

derks commented Apr 22, 2014

Definitely! I've had that thought before, just haven't had a chance to investigate it.

@derks

This comment has been minimized.

Show comment
Hide comment
@derks

derks Aug 8, 2014

Member

I think there are two use cases here... one would be an interactive shell (python) that would allow you to load up your app, and have direct access to operations within the running environment. The other, would actually be BASH/ZSH completion.

I found a few example, and have test it out to work pretty well... the difficulty is that the completion in BASH needs to be setup in an RC file, which would need to support multiple levels (nested controllers) and would need to be generated anytime the app is modified.

http://stackoverflow.com/questions/5302650/multi-level-bash-completion

I'm thinking an extension might be able to handle this, something like a hidden command:

$ myapp gen-bash-rc > ~/.myapp.rc

Something like that... but it would be tricky.

Member

derks commented Aug 8, 2014

I think there are two use cases here... one would be an interactive shell (python) that would allow you to load up your app, and have direct access to operations within the running environment. The other, would actually be BASH/ZSH completion.

I found a few example, and have test it out to work pretty well... the difficulty is that the completion in BASH needs to be setup in an RC file, which would need to support multiple levels (nested controllers) and would need to be generated anytime the app is modified.

http://stackoverflow.com/questions/5302650/multi-level-bash-completion

I'm thinking an extension might be able to handle this, something like a hidden command:

$ myapp gen-bash-rc > ~/.myapp.rc

Something like that... but it would be tricky.

@derks

This comment has been minimized.

Show comment
Hide comment
@derks

derks Aug 8, 2014

Member

This works for an internal app we use... commands/names are obfuscated:

_myapp_complete()
{
  local cur prev

  COMPREPLY=()
  cur=${COMP_WORDS[COMP_CWORD]}
  prev=${COMP_WORDS[COMP_CWORD-1]}

  if [ $COMP_CWORD -eq 1 ]; then
    COMPREPLY=( $(compgen -W "some-command controller1 controller2" -- $cur) )
  elif [ $COMP_CWORD -eq 2 ]; then
    case "$prev" in
      "controller1")
        COMPREPLY=( $(compgen -W "sub-command1 sub-command2" -- $cur) )
        ;;
      "controller2")
        COMPREPLY=( $(compgen -W "some-other-sub-command" -- $cur) )
        ;;
      *)
        ;;
    esac
  fi

  return 0
} &&
complete -F _myapp_complete myapp

Looks like:

$ myapp [tab] [tab]
some-command    controller1    controller2

$ myapp contr [tab] [tab]
controller1    controller2

$ myapp controller1 [tab] [tab]
sub-command1    sub-command2

$ myapp controller2 [tab] [tab]
some-other-sub-command
Member

derks commented Aug 8, 2014

This works for an internal app we use... commands/names are obfuscated:

_myapp_complete()
{
  local cur prev

  COMPREPLY=()
  cur=${COMP_WORDS[COMP_CWORD]}
  prev=${COMP_WORDS[COMP_CWORD-1]}

  if [ $COMP_CWORD -eq 1 ]; then
    COMPREPLY=( $(compgen -W "some-command controller1 controller2" -- $cur) )
  elif [ $COMP_CWORD -eq 2 ]; then
    case "$prev" in
      "controller1")
        COMPREPLY=( $(compgen -W "sub-command1 sub-command2" -- $cur) )
        ;;
      "controller2")
        COMPREPLY=( $(compgen -W "some-other-sub-command" -- $cur) )
        ;;
      *)
        ;;
    esac
  fi

  return 0
} &&
complete -F _myapp_complete myapp

Looks like:

$ myapp [tab] [tab]
some-command    controller1    controller2

$ myapp contr [tab] [tab]
controller1    controller2

$ myapp controller1 [tab] [tab]
sub-command1    sub-command2

$ myapp controller2 [tab] [tab]
some-other-sub-command

derks added a commit that referenced this issue Aug 9, 2014

@derks

This comment has been minimized.

Show comment
Hide comment
@derks

derks Aug 9, 2014

Member

See: http://cement.readthedocs.org/en/latest/examples/bash_auto_completion/

Definitely want to figure out how to auto-generate the BASH RC stuff ... but this will have to do for the near-term while I knock out some of the other outstanding issues toward 2.4 stable.

Member

derks commented Aug 9, 2014

See: http://cement.readthedocs.org/en/latest/examples/bash_auto_completion/

Definitely want to figure out how to auto-generate the BASH RC stuff ... but this will have to do for the near-term while I knock out some of the other outstanding issues toward 2.4 stable.

@derks derks assigned derks and unassigned derks Aug 21, 2014

@derks derks removed the dev/2.3.x label Aug 28, 2014

@nhumrich

This comment has been minimized.

Show comment
Hide comment
@nhumrich

nhumrich Sep 3, 2014

I like the example, but what if we want dynamic completion? For example, the git branch command auto-completes branch names. If I have positional arguments, is there a way to tab-complete those as well?

nhumrich commented Sep 3, 2014

I like the example, but what if we want dynamic completion? For example, the git branch command auto-completes branch names. If I have positional arguments, is there a way to tab-complete those as well?

@derks

This comment has been minimized.

Show comment
Hide comment
@derks

derks Sep 3, 2014

Member

You would do that the same way as completing sub-commands/controllers. You'd have to define them all in the RC file at the right level.

Member

derks commented Sep 3, 2014

You would do that the same way as completing sub-commands/controllers. You'd have to define them all in the RC file at the right level.

@nhumrich

This comment has been minimized.

Show comment
Hide comment
@nhumrich

nhumrich Sep 3, 2014

You can not enter the arguments into the RC file because they are dynamic. You would have to manually change the RC every time your possible arguments changed.

nhumrich commented Sep 3, 2014

You can not enter the arguments into the RC file because they are dynamic. You would have to manually change the RC every time your possible arguments changed.

@derks

This comment has been minimized.

Show comment
Hide comment
@derks

derks Sep 3, 2014

Member

You would. This is essentially the same as a controller/sub-command ... any time those change you would need to update the RC file as well. That said, if the possible argument values are limited (meaning the argument has to match some value) then it is possible, you just have to update the RC file like you said.

It's not ideal, and honestly maintaining a BASH RC file like this is not ideal at all, but it does work. I really have no idea at this point how to dynamically handle completion within Cement itself. The closest thing possible would be some sort of utility function that would auto-generate the RC file (that you could run before making a release), but that isn't likely to happen any time soon as it would be very complex and likely pretty fragile.

Member

derks commented Sep 3, 2014

You would. This is essentially the same as a controller/sub-command ... any time those change you would need to update the RC file as well. That said, if the possible argument values are limited (meaning the argument has to match some value) then it is possible, you just have to update the RC file like you said.

It's not ideal, and honestly maintaining a BASH RC file like this is not ideal at all, but it does work. I really have no idea at this point how to dynamically handle completion within Cement itself. The closest thing possible would be some sort of utility function that would auto-generate the RC file (that you could run before making a release), but that isn't likely to happen any time soon as it would be very complex and likely pretty fragile.

@nhumrich

This comment has been minimized.

Show comment
Hide comment
@nhumrich

nhumrich Sep 24, 2014

Another way to do this would be to pass the keywords and get the completion possibilities from app itself. This is the current solution I am using for a tool I am currently working on. This option even supports dynamic options that can change at any time (think git branch) as it asks the app to get the list. It does not have to be in bash.

Here is the bash script: (changed command to "myapp")

_myapp_complete() {
   COMPREPLY=()
   local words=( "${COMP_WORDS[@]}" )
   local word="${COMP_WORDS[COMP_CWORD]}"
   words=("${words[@]:1}")
   local completions="$(myapp completer --cmplt=\""${words[*]}"\")"
   COMPREPLY=( $(compgen -W "$completions" -- "$word") )
}
complete -F _myapp_complete myapp

Then you just need to write a "completer" controller like such:

from cement.core import controller, handler

class CompleterController(controller.CementBaseController):
    class Meta:
        label = 'completer'
        stacked_on = 'base'
        stacked_type = 'nested'
        description = 'auto-completer: hidden command'
        arguments = [
            (['--cmplt'], dict(help='command list so far')),
        ]
        hide = True

    @controller.expose(hide=True)
    def default(self):
        """
        Creates a space separated list of possible completions.
        We actually do not need to calculate the completions. We can simply
        just generate a list of ALL possibilities, and then the bash completer
         module is smart enough to filter out the ones that don't match/

         Results must be printed through stdin for the completer module
         to read then.
        """
        commands = self.app.pargs.cmplt.strip('"')

        # Get commands, filter out last one
        commands = commands.split(' ')
        word_so_far = commands[-1]
        commands = commands[0:-1]
        commands = filter(lambda x: len(x) > 0, commands)

        #Get the list of controllers
        self.controllers = handler.list('controller')
        self._filter_controllers()

        ctrlr = self._get_desired_controller(commands)

        if not ctrlr:
            return  # command entered so far is invalid, we dont need to
                    ##   worry about completion

        if word_so_far.startswith('--'):
            # Get all base option flags
            self.complete_options(ctrlr)
        else:
            if ctrlr == self.base_controller:
                # Get standard command list
                print(*[c.Meta.label for c in self.controllers])
            else:
                # A command has been provided. Complete at a deeper level
                ctrlr = ctrlr() # Instantiate so we can read all arguments

                if not hasattr(ctrlr, 'complete_command'):
                    return  # Controller does not support completion

                try:
                    ctrlr.complete_command(commands)
                except:
                    #We want to swallow ALL exceptions. We can
                    ## not print any output when trying to tab-complete
                    ## because any output gets passed to the user as
                    ## completion candidates
                    pass

    def complete_options(self, controller):
        # Get all base options (excluding the one for this controller)
        base_options = [c.option_strings[-1] for c in self.app.args._actions if
                        c.option_strings[-1] != '--cmplt']

        controller_options = [o[0][-1] for o in controller()._meta.arguments if
                              o[0][-1].startswith('--')]

        print(*base_options + controller_options)

    def _get_desired_controller(self, commands):
        if len(commands) < 1:
            return self.base_controller
        else:
            return next((c for c in self.controllers if
                         c.Meta.label == commands[0]), None)

    def _filter_controllers(self):
        #filter out unwanted controllers
        self.base_controller = next((c for c in self.controllers if
                                     c.Meta.label == 'base'), None)
        self.controllers = [c for c in self.controllers if
                            c.Meta.label != 'base' and
                            c.Meta.label != 'completer']

This is not yet ready for the generic case. But its a good start. In my code, I defined a complete_command method that prints the list of possible sub-arguments/commands for all controllers that have "dynamic competion". I do not have any second level controllers, but you could use the same method on those as well.

nhumrich commented Sep 24, 2014

Another way to do this would be to pass the keywords and get the completion possibilities from app itself. This is the current solution I am using for a tool I am currently working on. This option even supports dynamic options that can change at any time (think git branch) as it asks the app to get the list. It does not have to be in bash.

Here is the bash script: (changed command to "myapp")

_myapp_complete() {
   COMPREPLY=()
   local words=( "${COMP_WORDS[@]}" )
   local word="${COMP_WORDS[COMP_CWORD]}"
   words=("${words[@]:1}")
   local completions="$(myapp completer --cmplt=\""${words[*]}"\")"
   COMPREPLY=( $(compgen -W "$completions" -- "$word") )
}
complete -F _myapp_complete myapp

Then you just need to write a "completer" controller like such:

from cement.core import controller, handler

class CompleterController(controller.CementBaseController):
    class Meta:
        label = 'completer'
        stacked_on = 'base'
        stacked_type = 'nested'
        description = 'auto-completer: hidden command'
        arguments = [
            (['--cmplt'], dict(help='command list so far')),
        ]
        hide = True

    @controller.expose(hide=True)
    def default(self):
        """
        Creates a space separated list of possible completions.
        We actually do not need to calculate the completions. We can simply
        just generate a list of ALL possibilities, and then the bash completer
         module is smart enough to filter out the ones that don't match/

         Results must be printed through stdin for the completer module
         to read then.
        """
        commands = self.app.pargs.cmplt.strip('"')

        # Get commands, filter out last one
        commands = commands.split(' ')
        word_so_far = commands[-1]
        commands = commands[0:-1]
        commands = filter(lambda x: len(x) > 0, commands)

        #Get the list of controllers
        self.controllers = handler.list('controller')
        self._filter_controllers()

        ctrlr = self._get_desired_controller(commands)

        if not ctrlr:
            return  # command entered so far is invalid, we dont need to
                    ##   worry about completion

        if word_so_far.startswith('--'):
            # Get all base option flags
            self.complete_options(ctrlr)
        else:
            if ctrlr == self.base_controller:
                # Get standard command list
                print(*[c.Meta.label for c in self.controllers])
            else:
                # A command has been provided. Complete at a deeper level
                ctrlr = ctrlr() # Instantiate so we can read all arguments

                if not hasattr(ctrlr, 'complete_command'):
                    return  # Controller does not support completion

                try:
                    ctrlr.complete_command(commands)
                except:
                    #We want to swallow ALL exceptions. We can
                    ## not print any output when trying to tab-complete
                    ## because any output gets passed to the user as
                    ## completion candidates
                    pass

    def complete_options(self, controller):
        # Get all base options (excluding the one for this controller)
        base_options = [c.option_strings[-1] for c in self.app.args._actions if
                        c.option_strings[-1] != '--cmplt']

        controller_options = [o[0][-1] for o in controller()._meta.arguments if
                              o[0][-1].startswith('--')]

        print(*base_options + controller_options)

    def _get_desired_controller(self, commands):
        if len(commands) < 1:
            return self.base_controller
        else:
            return next((c for c in self.controllers if
                         c.Meta.label == commands[0]), None)

    def _filter_controllers(self):
        #filter out unwanted controllers
        self.base_controller = next((c for c in self.controllers if
                                     c.Meta.label == 'base'), None)
        self.controllers = [c for c in self.controllers if
                            c.Meta.label != 'base' and
                            c.Meta.label != 'completer']

This is not yet ready for the generic case. But its a good start. In my code, I defined a complete_command method that prints the list of possible sub-arguments/commands for all controllers that have "dynamic competion". I do not have any second level controllers, but you could use the same method on those as well.

@derks

This comment has been minimized.

Show comment
Hide comment
@derks

derks Sep 26, 2014

Member

This is really interesting. I like this.... but will obviously have to play around with it and make it as generic as possible. Thank you for submitting it though.. very helpful as I was kind of just relying on random Google-foo to figure out how to do bash completion in the first place.

Member

derks commented Sep 26, 2014

This is really interesting. I like this.... but will obviously have to play around with it and make it as generic as possible. Thank you for submitting it though.. very helpful as I was kind of just relying on random Google-foo to figure out how to do bash completion in the first place.

@akhilman

This comment has been minimized.

Show comment
Hide comment
@akhilman

akhilman Apr 30, 2016

Contributor

For zsh genzshcomp is ok as quick solution. But it is not support sub-commands.

Contributor

akhilman commented Apr 30, 2016

For zsh genzshcomp is ok as quick solution. But it is not support sub-commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment