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

use dsl in multiple files for plugin system #280

Closed logicminds closed 5 years ago

logicminds commented 5 years ago

I have an app that has a plugin system and currently using GLI. Right now I just have one big file that uses the GLI DSL for all my commands. However, I would like to split this into multiple files where each command would have its own command set so that each plugin could set their own flags and switches without having to modify the main repository.

This would mean the CLI app file would call each plugin to build out the rest of the commands and subcommands.

Can this be done with GLI?

davetron5000 commented 5 years ago

Yeah, the commands_from method is a way to do that (it is a fancy wrapper around require).

http://davetron5000.github.io/gli/rdoc/classes/GLI/App.html#method-i-commands_from

Let me know if that's sufficient or if you need more info on how it works.

logicminds commented 5 years ago

I can't figure out what to do with the commands_from.

Do I create a class that uses include Gli::App for the subcommand / decomposition piece?

Do I override desc and command inside the class?

Any example source code with examples you can toss my way?

davetron5000 commented 5 years ago

Do you have your work in progress you could share?

There is a test app in this repo that uses it: https://github.com/davetron5000/gli/blob/gli-2/test/apps/todo/bin/todo#L36

But basically, if your bin/my_app looks like so:

require "gli"

include GLI::App

commands_from "my_app/commands"

And then, assuming you are working in a Ruby Gem:

# lib/my_app/commands/list.rb

desc "List stuff"
command [ :list, :ls ] do |c|

  # ... normal stuff you'd do

end

# lib/my_app/commands/new.rb

desc "Create a thing"
command [ :new ] do |c|

  # ...

end

note that commands_from uses the current Ruby path, so if you do this in a RubyGem, you'll have to run your app via bundle exec bin/my_app while doing development. Once you bundle your app as a gem and distribute it, everything should work as normal (e.g. my_app)

Let me know if that helps, but if not, and you can share your work in progress, I might be able to give more specific help on that.

logicminds commented 5 years ago

In my bin file I have:

#!/usr/bin/env ruby
require 'crossbelt/cli'

cli = Crossbelt::Cli.new
exit cli.run(ARGV)

where the Cli class looks something like:

module Crossbelt
  class Cli

    include GLI::App

    def initialize
      initialize_gli
    end

    def spinner
      @spinner ||= TTY::Spinner.new
    end

    def clear_screen
      puts "\e[H\e[2J"
    end

    def client
      @client ||= Crossbelt::Client.new
    end

    def initialize_gli
      program_desc 'The Crossbelt command line app'
      desc "Update Crossbelt Cloud Rig Info"
      command :update do |update|
        update.default_command :one_off

        update.desc "One off manual update"
        update.command :one_off do |one_off|
          one_off.action do |global_options,options,args|
            spinner.auto_spin unless ENV['GLI_DEBUG']
            client.run_action -> do
              require 'crossbelt/commands/update'
              Crossbelt::Command::Update.new(options).run
            end
            spinner.stop unless ENV['GLI_DEBUG']
          end
        end
    end
  end
end

It is my understanding that if I use commands_from that it would go into my initialize_gli method.

def initialize_gli
    program_desc 'The Crossbelt command line app'
    commands_from "crossbelt/commands"
end

Then inside the update command file crossbelt/commands/update.rb I have

require 'crossbelt/command_action'

# crossbelt/commands/update.rb
update.command :one_off do |one_off|
          one_off.action do |global_options,options,args|
            spinner.auto_spin unless ENV['GLI_DEBUG']
            client.run_action -> do
              require 'crossbelt/commands/update'
              Crossbelt::Command::Update.new(options).run
            end
            spinner.stop unless ENV['GLI_DEBUG']
          end
        end
  end
end
module Crossbelt
  module Command
    class Update < Crossbelt::CommandAction
      def initialize(opts = {})
        @options = opts
      end

      def run
        update_data
      end
    end
  end
end

At the moment I am getting the following error:

be exe/cb
bundler: failed to load command: exe/cb (exe/cb)
NoMethodError: undefined method `desc' for main:Object
logicminds commented 5 years ago

Also I started down another path and was trying to not use the DSL and instead subclass the GLI::Command class. Although this did not seem to make much sense since I would have to create a create a bunch of heavy weight subclassed command objects upfront vs the lightweight version the DSL creates now.

davetron5000 commented 5 years ago

OK, you have a slightly unusual setup, but I see what you need to do.

commands_from is really doing a require underneath, so the files you are extracting your commands into need to have the same class structure.

Putting GLI's DSL into a class and into methods is probably not what you want. If you don't want to dump everything into bin/ you'll need to:

# bin/my_app
#!/usr/bin/env ruby

require "cli"
exit Crossbelt::Cli.run(ARGV)

# lib/crossbelt/cli.rb
module Crossbelt
  module Cli
    extend GLI::App

    program_desc "My App"

    commands_from "crossbelt/commands"
  end
end

# lib/crossbelt/commands
module Crossbelt
  module Cli
    desc "Some command"
    command :foo do |c|
      # ...
    end
  end
end
logicminds commented 5 years ago

This worked great. Thanks.