Search code examples
rubycommand-line-interfacethor

Variable in Thor desc


With a cli project I would like to have a shared super class with thor tasks. When calling the sub-command help it should expose the shared command with a custom description.

I came up with the code below, but the variable @plural is not substituted in the description.

Is this possible with Thor and some meta programming?

module MyModule
  class ResourceSubcommand < Thor

    def initialize(*args)
      super
    end

    desc "list", "list all #{@plural}"
    def list
      list_object(@default_list_columns)
    end
  end
end

module MyModule
  class Account < MyModule::ResourceSubcommand

    def initialize(*args)
      super
      @plural = 'accounts'
    end

  end
end

module MyModule
  class Commands < Thor

    desc "account SUBCOMMAND ...ARGS", "manage Exact Online accounts"
    subcommand "account", Account

  end
end

running $ thorcli account help should output:

Commands:
  thorcli account help [COMMAND]     # Describe subcommands or one specific subcommand
  thorcli account list               # list all accounts

Solution

  • The string passed to desc is evaluated in the context of the class, but the initialize method is evaluated in the context of instances, so the two @plural belong to two different objects.

    Further more, desc is called immediately when defining the superclass MyModule::ResourceSubcommand, and there is no easy way to postpone its evaluation after the superclass gets inherited and the @plural in the subclass is set, your goal seems very hard to achieve.

    P.S. I've tried overriding MyModule::ResourceSubcommand::inherited, and MyModule::ResourceSubcommand.singleton_class::inherited, and I failed. Maybe you could define MyModule::ResourceSubcommand as a module, override its self.included, and include it after setting @plural in the subclass.

    UPDATE

    I finally succeed. Here is my solution:

    module MyModule
    
      # Change from class to module
      module ResourceSubcommand
    
        # A hook called when this module is included by other modules
        def self.included(base)
          base.class_eval do
            desc "list", "list all #{@plural}"
            def list
              list_object(@default_list_columns)
            end
          end
        end
      end
    end
    
    module MyModule
    
      # No inheritance
      class Account
    
        # Don't put this in any instance methods, including #initialize
        @plural = 'accounts'
    
        # Be sure to include after @plural is set
        include ResourceSubcommand
      end
    end