davetron5000 / gli

Make awesome command-line applications the easy way
http://davetron5000.github.io/gli
Apache License 2.0
1.26k stars 102 forks source link

Add a `command_missing` hook #315

Closed camertron closed 2 years ago

camertron commented 2 years ago

First off, thank you so much for GLI. I'm a huge fan ❤️

I'd like to propose adding a new hook called command_missing which works much like Ruby's method_missing method. command_missing is called whenever a top-level command can't be found, and expects the block it captures to return an instance of GLI::Command.

I'm the author of the Kuby project, a tool for deploying Rails apps. Kuby features a plugin system, and therein lies my use-case. I would like plugins to have the ability to specify their own sets of commands, eg. kuby plugin_name plugin_subcommand ... The problem is that the list of plugins isn't known until the config file is loaded, which happens in a GLI pre hook. I need to be able to defer defining commands until the config file has been loaded, but before all the CLI options have been parsed. With the changes in this PR, the following is now possible:

module Kuby
  class Commands
    extend GLI::App

    def self.load_kuby_config!(global_options)
      # loading stuff here
    end

    command_missing do |command_name, global_options|
      load_kuby_config!(global_options)

      # command_name is also the name of the plugin
      if plugin_klass = Kuby.plugins.find(command_name)
        if plugin_klass.respond_to?(:commands)
          desc "Run commands for the #{command_name} plugin."
          command command_name do |c|
            # the plugin now defines its own commands on c
            plugin_klass.commands(c)
          end
        end
      end
    end

    pre do |global_options, options, args|
      load_kuby_config!
      # more code here
    end
  end
end

One of the key changes here is that the command method now returns the command object, and since that's the last line of the command_missing block, it gets returned to the parser. It's a bit funky to be sure, but it works.

Let me know what you think!

davetron5000 commented 2 years ago

Hey, thanks for the PR. Quick scan it looks good. Did you know about commands_from? It's a glorified require but that was added to allow GLI commands to have plugins. Though I guess if it's not run when the app loads it may not work? Can you see if that would work for your use case? If not, I'll give this a more detailed review

camertron commented 2 years ago

@davetron5000 oh cool I didn't know about commands_from 😄 I took a look at the docs and unfortunately I don't think it covers my use-case. Part of my particular problem is that load_kuby_config! depends on the global_options hash (because it contains the path to the Kuby config file), which AFAIK isn't available until GLI::GlobalOptionParser is finished doing its thing.

I also tried adding the commands inside the pre hook, but it looks like commands have to exist before pre is called.

davetron5000 commented 2 years ago

OK, sounds good. Like I said, this looks great, so gonna merge and do a realease real quick

davetron5000 commented 2 years ago

Released as 2.21.0. Thanks!

camertron commented 2 years ago

Sweet, thank you!