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

Access options outside the action method or common code between several sub-commands #331

Closed noraj closed 1 month ago

noraj commented 1 month ago

I have a question ❓ about this example:

https://github.com/davetron5000/gli/blob/19eae87ca23ab03237b3533833aa51170ea30e84/lib/gli/command.rb#L176

How is it possible to have a |global,options,args| for command? It sounds to me, it is possible only for action.

The reason I was led to ask myself this question is because I want some code to be executed for several sub-commands. Because the code would have been always the same I felt bad about copy/pasting it in each sub-command.

So I thought about copying it on the command action block like that:

      # dns duzdu
      # since 0.0.2
      dns.desc 'DNS unsecure zone dynamic update (DUZDU)'
      dns.command :duzdu do |duzdu|
        # stuff common to all sub-commands, initializing a DUZDU instance
        duz = nil
        duzdu.action do |_global_options, options, _args|
          parent_options = options[GLI::Command::PARENT]
          dns_opts = parent_options[:nameserver].nil? ? nil : { nameserver: [parent_options[:nameserver]] }
          duz = ADAssault::DNS::DUZDU.new(parent_options[:domain], dns_opts)
        end
        # dns duzdu add
        duzdu.desc 'TODO'
        duzdu.command :add do |add|
          add.action do |_global_options, _options, _args|
            duz.addv4
          end
        end
        # dns duzdu delete
        duzdu.desc 'TODO'
        duzdu.command :delete do |delete|
          delete.action do |_global_options, _options, _args|
            duz.deletev4
          end
        end
        # dns duzdu update
        duzdu.desc 'TODO'
        duzdu.command :update do |update|
          update.action do |_global_options, _options, _args|
            duz.updatev4
          end
        end
        # dns duzdu check
        duzdu.desc 'TODO'
        duzdu.command :check do |check|
          check.action do |_global_options, _options, _args|
            puts duz.checkv4
          end
        end
        # dns duzdu get
        duzdu.desc 'TODO'
        duzdu.command :get do |get|
          get.action do |_global_options, _options, _args|
            duz.getv4
          end
        end
      end

The issue is it will be executed only when there is no sub-command behind, e.g. ada dns duzdu. It is not executed when a sub-command exists, e.g. ada dns duzdu add.

For it to be called even when there is a sub-command I could have put it outside the action block (I don't even know if it would work), but then I don't have access to options.

In fact, I would have needed something like pre but pre works only on app top-level, not a command level, e.g. duzdu.pre. Somthing like the setup method of minitest.

That's why when I saw command with a |global,options,args|, I was thinking that could be useful for what I try to achieve.

I think the better temporary workaround I have right now is putting the 3 lines of code in a function and calling that function in each sub-command action block.

noraj commented 1 month ago

My temporary workaround:

      # dns duzdu
      # since 0.0.2
      dns.desc 'DNS unsecure zone dynamic update (DUZDU)'
      dns.command :duzdu do |duzdu|
        # stuff common to all duzdu's sub-commands, initializing a DUZDU instance
        def self.duzdu_init(options)
          parent_options = options.dig(GLI::Command::PARENT, GLI::Command::PARENT)
          dns_opts = parent_options[:nameserver].nil? ? nil : { nameserver: [parent_options[:nameserver]] }
          ADAssault::DNS::DUZDU.new(parent_options[:domain], dns_opts)
        end
        # dns duzdu add
        duzdu.desc 'TODO'
        duzdu.command :add do |add|
          add.action do |_global_options, options, _args|
            duz = duzdu_init(options)
            puts duz.addv4
          end
        end
        # dns duzdu delete
        duzdu.desc 'TODO'
        duzdu.command :delete do |delete|
          delete.action do |_global_options, options, _args|
            duz = duzdu_init(options)
            puts duz.deletev4
          end
        end
        # dns duzdu update
        duzdu.desc 'TODO'
        duzdu.command :update do |update|
          update.action do |_global_options, _options, _args|
            duz = duzdu_init(options)
            puts duz.updatev4
          end
        end
        # dns duzdu check
        duzdu.desc 'TODO'
        duzdu.command :check do |check|
          check.action do |_global_options, options, _args|
            duz = duzdu_init(options)
            puts duz.checkv4
          end
        end
        # dns duzdu get
        duzdu.desc 'TODO'
        duzdu.command :get do |get|
          get.action do |_global_options, _options, _args|
            duz = duzdu_init(options)
            puts duz.getv4
          end
        end
      end
davetron5000 commented 1 month ago

Eh, that looks like a typo? I'm not sure that works.

the GLI DSL is best used to locate code in some other class to execute, i.e. you don't want to put your core logic directly inside the action block. When you have a highly nested set of commands and subcommands, you might want to have a bunch of regular Ruby classes that manage your core logic and use conventional means of re-use like inheritance of mixins to manage duplication.

e.g.

# In e.g. lib/actions/duzdu
class DUZDUBaseAction
  def initialize(options)
    parent_options = options.dig(GLI::Command::PARENT, GLI::Command::PARENT)
    dns_opts = parent_options[:nameserver].nil? ? nil : { nameserver: [parent_options[:nameserver]] }
    @duzdu = ADAssault::DNS::DUZDU.new(parent_options[:domain], dns_opts)
  end

  def perform!
    raise "subclass must implement"
  end
end

class Add < DUZDUBaseAction
  def perform!
    @duzdu.addv4
  end
end

class Delete < DUZDUBaseAction
  def perform!
    @duzdu.deletev4
  end
end

# then, in your GLI "app"

dns.command :duzdu do |duzdu|
  duzdu.command :add do |add|
    add.action do |_global_options, options, _args|
      Add.new(options).perform!
    end
  end
  # dns duzdu delete
  duzdu.desc 'TODO'
  duzdu.command :delete do |delete|
    delete.action do |_global_options, options, _args|
      Delete.new(options).perform!
    end
  end
end

If you want to get really fancy, you can use Ruby's protocol for turning objects into procs:

# In e.g. lib/actions/duzdu
class DUZDUBaseAction
  def initialize(options)
    parent_options = options.dig(GLI::Command::PARENT, GLI::Command::PARENT)
    dns_opts = parent_options[:nameserver].nil? ? nil : { nameserver: [parent_options[:nameserver]] }
    @duzdu = ADAssault::DNS::DUZDU.new(parent_options[:domain], dns_opts)
  end

  def perform!
    raise "subclass must implement"
  end

  def self.to_proc
    ->(_global, options, args) {
      self.new(options)
    }
  end
end

class Add < DUZDUBaseAction
  def perform!
    @duzdu.addv4
  end
end

class Delete < DUZDUBaseAction
  def perform!
    @duzdu.deletev4
  end
end

# then, in your GLI "app"

dns.command :duzdu do |duzdu|
  duzdu.command :add do |add|
    add.action(&Add)
  end
  # dns duzdu delete
  duzdu.desc 'TODO'
  duzdu.command :delete do |delete|
    delete.action(&Delete)
  end
end

But in any case, you ideally want your action blocks deferring to other code if possible, especially when your app has a lot of commands and sub-commands