lib/thor.rb in atli-0.1.9 vs lib/thor.rb in atli-0.1.10

- old
+ new

@@ -1,837 +1,853 @@ require "set" require 'nrser' -require 'semantic_logger' require "thor/base" require 'thor/example' -require 'thor/completion/bash' class Thor - include Thor::Completion::Bash::Thor - class << self - # Allows for custom "Command" package naming. - # - # === Parameters - # name<String> - # options<Hash> - # - def package_name(name, _ = {}) - @package_name = name.nil? || name == "" ? nil : name - end + # Class Methods + # ========================================================================== - # Sets the default command when thor is executed without an explicit - # command to be called. - # - # ==== Parameters - # meth<Symbol>:: name of the default command - # - def default_command(meth = nil) - if meth - @default_command = meth == :none ? "help" : meth.to_s - else - @default_command ||= from_superclass(:default_command, "help") - end + # Allows for custom "Command" package naming. + # + # === Parameters + # name<String> + # options<Hash> + # + def self.package_name(name, _ = {}) + @package_name = name.nil? || name == "" ? nil : name + end + + + # Sets the default command when thor is executed without an explicit + # command to be called. + # + # ==== Parameters + # meth<Symbol>:: name of the default command + # + def self.default_command(meth = nil) + if meth + @default_command = meth == :none ? "help" : meth.to_s + else + @default_command ||= from_superclass(:default_command, "help") end - alias_method :default_task, :default_command + end - # Registers another Thor subclass as a command. - # - # ==== Parameters - # klass<Class>:: Thor subclass to register - # command<String>:: Subcommand name to use - # usage<String>:: Short usage for the subcommand - # description<String>:: Description for the subcommand - def register(klass, subcommand_name, usage, description, options = {}) - if klass <= Thor::Group - desc usage, description, options - define_method(subcommand_name) { |*args| invoke(klass, args) } - else - desc usage, description, options - subcommand subcommand_name, klass - end + singleton_class.send :alias_method, :default_task, :default_command + + + # Registers another Thor subclass as a command. + # + # ==== Parameters + # klass<Class>:: Thor subclass to register + # command<String>:: Subcommand name to use + # usage<String>:: Short usage for the subcommand + # description<String>:: Description for the subcommand + def self.register(klass, subcommand_name, usage, description, options = {}) + if klass <= Thor::Group + desc usage, description, options + define_method(subcommand_name) { |*args| invoke(klass, args) } + else + desc usage, description, options + subcommand subcommand_name, klass end + end - # Defines the usage and the description of the next command. - # - # ==== Parameters - # usage<String> - # description<String> - # options<String> - # - def desc(usage, description, options = {}) - if options[:for] - command = find_and_refresh_command(options[:for]) - command.usage = usage if usage - command.description = description if description - else - @usage = usage - @desc = description - @hide = options[:hide] || false - end + + # Defines the usage and the description of the next command. + # + # ==== Parameters + # usage<String> + # description<String> + # options<String> + # + def self.desc(usage, description, options = {}) + if options[:for] + command = find_and_refresh_command(options[:for]) + command.usage = usage if usage + command.description = description if description + else + @usage = usage + @desc = description + @hide = options[:hide] || false end + end - # Defines the long description of the next command. - # - # ==== Parameters - # long description<String> - # - def long_desc(long_description, options = {}) - if options[:for] - command = find_and_refresh_command(options[:for]) - command.long_description = long_description if long_description - else - @long_desc = long_description - end + + # Defines the long description of the next command. + # + # ==== Parameters + # long description<String> + # + def self.long_desc(long_description, options = {}) + if options[:for] + command = find_and_refresh_command(options[:for]) + command.long_description = long_description if long_description + else + @long_desc = long_description end + end - # Maps an input to a command. If you define: - # - # map "-T" => "list" - # - # Running: - # - # thor -T - # - # Will invoke the list command. - # - # ==== Parameters - # Hash[String|Array => Symbol]:: Maps the string or the strings in the - # array to the given command. - # - def map(mappings = nil) - @map ||= from_superclass(:map, {}) - if mappings - mappings.each do |key, value| - if key.respond_to?(:each) - key.each { |subkey| @map[subkey] = value } - else - @map[key] = value - end + # Maps an input to a command. If you define: + # + # map "-T" => "list" + # + # Running: + # + # thor -T + # + # Will invoke the list command. + # + # @example Map a single alias to a command + # map 'ls' => :list + # + # @example Map multiple aliases to a command + # map ['ls', 'show'] => :list + # + # @note + # + # + # + # @param [nil | Hash<#to_s | Array<#to_s>, #to_s>?] mappings + # When `nil`, all mappings for the class are returned. + # + # When a {Hash} is provided, sets the `mappings` before returning + # all mappings. + # + # @return [HashWithIndifferentAccess<String, Symbol>] + # Mapping of command aliases to command method names. + # + def self.map mappings = nil + @map ||= from_superclass :map, HashWithIndifferentAccess.new + + if mappings + mappings.each do |key, value| + if key.respond_to? :each + key.each { |subkey| @map[ subkey.to_s ] = value.to_s } + else + @map[ key.to_s ] = value.to_s end end - - @map end - # Declares the options for the next command to be declared. - # - # ==== Parameters - # Hash[Symbol => Object]:: The hash key is the name of the option and the value - # is the type of the option. Can be :string, :array, :hash, :boolean, :numeric - # or :required (string). If you give a value, the type of the value is used. - # - def method_options(options = nil) - @method_options ||= {} - build_options(options, @method_options) if options - @method_options - end + @map + end - alias_method :options, :method_options - # Adds an option to the set of method options. If :for is given as option, - # it allows you to change the options from a previous defined command. - # - # def previous_command - # # magic - # end - # - # method_option :foo => :bar, :for => :previous_command - # - # def next_command - # # magic - # end - # - # ==== Parameters - # name<Symbol>:: The name of the argument. - # options<Hash>:: Described below. - # - # ==== Options - # :desc - Description for the argument. - # :required - If the argument is required or not. - # :default - Default value for this argument. It cannot be required and - # have default values. - # :aliases - Aliases for this option. - # :type - The type of the argument, can be :string, :hash, :array, - # :numeric or :boolean. - # :banner - String to show on usage notes. - # :hide - If you want to hide this option from the help. - # - def method_option(name, options = {}) - scope = if options[:for] - find_and_refresh_command(options[:for]).options - else - method_options - end + # Declares the options for the next command to be declared. + # + # ==== Parameters + # Hash[Symbol => Object]:: The hash key is the name of the option and the value + # is the type of the option. Can be :string, :array, :hash, :boolean, :numeric + # or :required (string). If you give a value, the type of the value is used. + # + def self.method_options(options = nil) + @method_options ||= HashWithIndifferentAccess.new + build_options(options, @method_options) if options + @method_options + end - build_option(name, options, scope) + singleton_class.send :alias_method, :options, :method_options + + + # Adds an option to the set of method options. If :for is given as option, + # it allows you to change the options from a previous defined command. + # + # def previous_command + # # magic + # end + # + # method_option :foo => :bar, :for => :previous_command + # + # def next_command + # # magic + # end + # + # ==== Parameters + # name<Symbol>:: The name of the argument. + # options<Hash>:: Described below. + # + # ==== Options + # :desc - Description for the argument. + # :required - If the argument is required or not. + # :default - Default value for this argument. It cannot be required and + # have default values. + # :aliases - Aliases for this option. + # :type - The type of the argument, can be :string, :hash, :array, + # :numeric or :boolean. + # :banner - String to show on usage notes. + # :hide - If you want to hide this option from the help. + # + def self.method_option name, **options + scope = if options[:for] + find_and_refresh_command(options[:for]).options + else + method_options end - alias_method :option, :method_option - # Prints help information for the given command. - # - # @param [Thor::Shell] shell - # - # @param [String] command_name - # - # @param [Boolean] subcommand - # *Alti* *addition* - passed from {#help} when that command is being - # invoked as a subcommand. - # - # The values is passed through to {.banner} and eventually - # {Command#formatted_usage} so that it can properly display the usage - # message - # - # basename subcmd cmd ARGS... - # - # versus what it did when I found it: - # - # basename cmd ARGS... - # - # which, of course, doesn't work if +cmd+ is inside +subcmd+. - # - # @return [nil] - # - def command_help(shell, command_name, subcommand = false) - meth = normalize_command_name(command_name) - command = all_commands[meth] - handle_no_command_error(meth) unless command + build_option(name, options, scope) + end - shell.say "Usage:" - shell.say " #{banner(command, nil, subcommand)}" - shell.say + singleton_class.send :alias_method, :option, :method_option + + + # Prints help information for the given command. + # + # @param [Thor::Shell] shell + # + # @param [String] command_name + # + # @param [Boolean] subcommand + # *Alti* *addition* - passed from {#help} when that command is being + # invoked as a subcommand. + # + # The values is passed through to {.banner} and eventually + # {Command#formatted_usage} so that it can properly display the usage + # message + # + # basename subcmd cmd ARGS... + # + # versus what it did when I found it: + # + # basename cmd ARGS... + # + # which, of course, doesn't work if +cmd+ is inside +subcmd+. + # + # @return [nil] + # + def self.command_help(shell, command_name, subcommand = false) + meth = normalize_command_name(command_name) + command = all_commands[meth] + handle_no_command_error(meth) unless command + + shell.say "Usage:" + shell.say " #{banner(command, nil, subcommand)}" + shell.say + + class_options_help \ + shell, + command.options.values.group_by { |option| option.group } + + if command.long_description + shell.say "Description:" + shell.print_wrapped(command.long_description, :indent => 2) + else + shell.say command.description + end + + unless command.examples.empty? + shell.say "\n" + shell.say "Examples:" + shell.say "\n" - class_options_help \ - shell, - command.options.values.group_by { |option| option.group } - - if command.long_description - shell.say "Description:" - shell.print_wrapped(command.long_description, :indent => 2) - else - shell.say command.description - end - - unless command.examples.empty? - shell.say "\n" - shell.say "Examples:" - shell.say "\n" + command.examples.each_with_index do |example, index| + lines = example.lines - command.examples.each_with_index do |example, index| - lines = example.lines - - shell.say "1. #{ lines[0] }" - - lines[1..-1].each do |line| - shell.say " #{ line }" - end - end + bullet = "#{ index + 1}.".ljust 4 + + shell.say "#{ bullet }#{ lines[0] }" - shell.say "\n" + lines[1..-1].each do |line| + shell.say " #{ line }" + end end - nil + shell.say "\n" end - alias_method :task_help, :command_help + + nil + end - # Prints help information for this class. - # - # @param [Thor::Shell] shell - # @return (see Thor::Base::ClassMethods#class_options_help) - # - def help(shell, subcommand = false) - list = printable_commands(true, subcommand) - Thor::Util.thor_classes_in(self).each do |klass| - list += klass.printable_commands(false) - end - list.sort! { |a, b| a[0] <=> b[0] } + singleton_class.send :alias_method, :task_help, :command_help - if defined?(@package_name) && @package_name - shell.say "#{@package_name} commands:" - else - shell.say "Commands:" - end - shell.print_table(list, :indent => 2, :truncate => true) - shell.say - class_options_help(shell) + # Prints help information for this class. + # + # @param [Thor::Shell] shell + # @return (see Thor::Base::ClassMethods#class_options_help) + # + def self.help(shell, subcommand = false) + list = printable_commands(true, subcommand) + Thor::Util.thor_classes_in(self).each do |klass| + list += klass.printable_commands(false) end + list.sort! { |a, b| a[0] <=> b[0] } - # Returns commands ready to be printed. - def printable_commands(all = true, subcommand = false) - (all ? all_commands : commands).map do |_, command| - next if command.hidden? - item = [] - item << banner(command, false, subcommand) - item << ( command.description ? - "# #{command.description.gsub(/\s+/m, ' ')}" : "" ) - item - end.compact + if defined?(@package_name) && @package_name + shell.say "#{@package_name} commands:" + else + shell.say "Commands:" end - alias_method :printable_tasks, :printable_commands - - - # List of subcommand names, including those inherited from super - # classes. + + shell.print_table(list, :indent => 2, :truncate => true) + shell.say + class_options_help(shell) + end + + + # Returns commands ready to be printed. + # + # @param [Boolean] all + # When `true`, + # + # @return [Array<(String, String)>] + # Array of pairs with: + # + # - `[0]` - The "usage" format. + # - `[1]` - The command description. + # + def self.printable_commands all = true, subcommand = false + (all ? all_commands : commands).map do |_, command| + next if command.hidden? + item = [] + item << banner(command, false, subcommand) + item << ( command.description ? + "# #{command.description.gsub(/\s+/m, ' ')}" : "" ) + item + end.compact + end + singleton_class.send :alias_method, :printable_tasks, :printable_commands + + + # List of subcommand names, including those inherited from super + # classes. + # + # @return [Array<String>] + # + def self.subcommands + @subcommands ||= from_superclass(:subcommands, []) + end + singleton_class.send :alias_method, :subtasks, :subcommands + + + # Map of subcommand names to Thor classes for *this* Thor class only. + # + # @note + # `.subcommands` is not necessarily equal to `.subcommand_classes.keys` + # - it won't be when there are subcommands inherited from super classes. + # + # @note + # I'm not really sure how this relates to {Thor::Group}... and I'm not + # going to take the time to find out now. + # + # @return [Hash<String, Class<Thor::Base>] + # + def self.subcommand_classes + @subcommand_classes ||= {} + end + + + # Declare a subcommand by providing an UI name and subcommand class. + # + # @example + # class MySub < Thor + # desc 'blah', "Blah!" + # def blah + # # ... + # end + # end + # + # class Main < Thor + # subcommand 'my-sub', MySub, desc: "Do sub stuff" + # end + # + # @param [String | Symbol] ui_name + # The name as you would like it to appear in the user interface + # (help, etc.). + # + # {String#underscore} is called on this argument to create the "method + # name", which is used as the name of the method that will handle + # subcommand invokation, and as the "internal name" that is added to + # {#subcommands} and used as it's key in {#subcommand_classes}. + # + # The subcommand should be callable from the CLI using both names. + # + # @param [Thor::Base] subcommand_class + # The subcommand class. + # + # Note that I have not tried using {Thor::Group} as subcommand classes, + # and the documentation and online search results around it are typically + # vague and uncertain. + # + # @param [String?] desc: + # Optional description of the subcommand for help messages. + # + # If this option is provided, a {Thor.desc} call will be issued before + # the subcommand hanlder method is dynamically added. + # + # If you don't provide this option, you probably want to make your + # own call to {Thor.desc} before calling `.subcommand` like: + # + # desc 'my-sub SUBCOMMAND...', "Do subcommand stuff." + # subcommand 'my-sub', MySub + # + # The `desc:` keyword args lets you consolidate this into one call: + # + # subcommand 'my-sub', MySub, desc: "Do subcommand stuff." + # + # @return [nil] + # This seemed to just return "whatever" (`void` as we say), but I threw + # `nil` in there to be clear. + # + def self.subcommand ui_name, subcommand_class, desc: nil + ui_name = ui_name.to_s + method_name = ui_name.underscore + subcommands << method_name + subcommand_class.subcommand_help ui_name + subcommand_classes[method_name] = subcommand_class + + if desc + self.desc "#{ ui_name } SUBCOMMAND...", desc + end + + define_method method_name do |*args| + args, opts = Thor::Arguments.split(args) + invoke_args = [ + args, + opts, + {:invoked_via_subcommand => true, :class_options => options} + ] + invoke_args.unshift "help" if opts.delete("--help") || opts.delete("-h") + invoke subcommand_class, *invoke_args + end + + # Sigh... this used to just be `subcommand_class.commands...`, but that + # fails when: # - # @return [Array<String>] + # 1. A Thor class is created with commands defined, then + # 2. That Thor class is subclassed, then + # 3. The *subclass* as added as a {.subcommand}. # - def subcommands - @subcommands ||= from_superclass(:subcommands, []) - end - alias_method :subtasks, :subcommands - - - # Map of subcommand names to Thor classes for *this* Thor class only. + # This is because then the commands that need {Command#ancestor_name} + # set are not part of {.commands}, only {.all_commands}. # - # @note - # `.subcommands` is not necessarily equal to `.subcommand_classes.keys` - # - it won't be when there are subcommands inherited from super classes. + # At the moment, this *seems* like an architectural problem, since + # the commands being mutated via {Thor::Command#ancestor_name=} do + # not *belong* to `self` - they are simply part of superclass, which + # could logically be re-used across through additional sub-classes + # by multiple {Thor} *unless* this is not party of the paradigm... + # but the paradigm fundametals and constraints are never really laid + # out anywhere. # - # @note - # I'm not really sure how this relates to {Thor::Group}... and I'm not - # going to take the time to find out now. + # Anyways, switching to {subcommand_classes.all_commands...} hacks + # the problem away for the moment. # - # @return [Hash<String, Class<Thor::Base>] - # - def subcommand_classes - @subcommand_classes ||= {} + subcommand_class.all_commands.each do |_meth, command| + command.ancestor_name ||= ui_name end - - - def subcommand(subcommand, subcommand_class) - subcommands << subcommand.to_s - subcommand_class.subcommand_help subcommand - subcommand_classes[subcommand.to_s] = subcommand_class - define_method(subcommand) do |*args| - args, opts = Thor::Arguments.split(args) - invoke_args = [ - args, - opts, - {:invoked_via_subcommand => true, :class_options => options} - ] - invoke_args.unshift "help" if opts.delete("--help") || opts.delete("-h") - invoke subcommand_class, *invoke_args - end - subcommand_class.commands.each do |_meth, command| - command.ancestor_name = subcommand - end - end - alias_method :subtask, :subcommand + nil + end - # Extend check unknown options to accept a hash of conditions. - # - # === Parameters - # options<Hash>: A hash containing :only and/or :except keys - def check_unknown_options!(options = {}) - @check_unknown_options ||= {} - options.each do |key, value| - if value - @check_unknown_options[key] = Array(value) - else - @check_unknown_options.delete(key) - end + singleton_class.send :alias_method, :subtask, :subcommand + + + # Extend check unknown options to accept a hash of conditions. + # + # === Parameters + # options<Hash>: A hash containing :only and/or :except keys + def self.check_unknown_options!(options = {}) + @check_unknown_options ||= {} + options.each do |key, value| + if value + @check_unknown_options[key] = Array(value) + else + @check_unknown_options.delete(key) end - @check_unknown_options end + @check_unknown_options + end - # Overwrite check_unknown_options? to take subcommands and options into - # account. - def check_unknown_options?(config) #:nodoc: - options = check_unknown_options - return false unless options - command = config[:current_command] - return true unless command + # Overwrite check_unknown_options? to take subcommands and options into + # account. + def self.check_unknown_options?(config) #:nodoc: + options = check_unknown_options + return false unless options - name = command.name + command = config[:current_command] + return true unless command - if subcommands.include?(name) - false - elsif options[:except] - !options[:except].include?(name.to_sym) - elsif options[:only] - options[:only].include?(name.to_sym) - else - true - end - end + name = command.name - # Stop parsing of options as soon as an unknown option or a regular - # argument is encountered. All remaining arguments are passed to the command. - # This is useful if you have a command that can receive arbitrary additional - # options, and where those additional options should not be handled by - # Thor. - # - # ==== Example - # - # To better understand how this is useful, let's consider a command that calls - # an external command. A user may want to pass arbitrary options and - # arguments to that command. The command itself also accepts some options, - # which should be handled by Thor. - # - # class_option "verbose", :type => :boolean - # stop_on_unknown_option! :exec - # check_unknown_options! :except => :exec - # - # desc "exec", "Run a shell command" - # def exec(*args) - # puts "diagnostic output" if options[:verbose] - # Kernel.exec(*args) - # end - # - # Here +exec+ can be called with +--verbose+ to get diagnostic output, - # e.g.: - # - # $ thor exec --verbose echo foo - # diagnostic output - # foo - # - # But if +--verbose+ is given after +echo+, it is passed to +echo+ instead: - # - # $ thor exec echo --verbose foo - # --verbose foo - # - # ==== Parameters - # Symbol ...:: A list of commands that should be affected. - def stop_on_unknown_option!(*command_names) - stop_on_unknown_option.merge(command_names) + if subcommands.include?(name) + false + elsif options[:except] + !options[:except].include?(name.to_sym) + elsif options[:only] + options[:only].include?(name.to_sym) + else + true end + end - def stop_on_unknown_option?(command) #:nodoc: - command && stop_on_unknown_option.include?(command.name.to_sym) - end - # Disable the check for required options for the given commands. - # This is useful if you have a command that does not need the required options - # to work, like help. - # - # ==== Parameters - # Symbol ...:: A list of commands that should be affected. - def disable_required_check!(*command_names) - disable_required_check.merge(command_names) + # Stop parsing of options as soon as an unknown option or a regular + # argument is encountered. All remaining arguments are passed to the command. + # This is useful if you have a command that can receive arbitrary additional + # options, and where those additional options should not be handled by + # Thor. + # + # ==== Example + # + # To better understand how this is useful, let's consider a command that calls + # an external command. A user may want to pass arbitrary options and + # arguments to that command. The command itself also accepts some options, + # which should be handled by Thor. + # + # class_option "verbose", :type => :boolean + # stop_on_unknown_option! :exec + # check_unknown_options! :except => :exec + # + # desc "exec", "Run a shell command" + # def exec(*args) + # puts "diagnostic output" if options[:verbose] + # Kernel.exec(*args) + # end + # + # Here +exec+ can be called with +--verbose+ to get diagnostic output, + # e.g.: + # + # $ thor exec --verbose echo foo + # diagnostic output + # foo + # + # But if +--verbose+ is given after +echo+, it is passed to +echo+ instead: + # + # $ thor exec echo --verbose foo + # --verbose foo + # + # ==== Parameters + # Symbol ...:: A list of commands that should be affected. + def self.stop_on_unknown_option!(*command_names) + stop_on_unknown_option.merge(command_names) + end + + + def self.stop_on_unknown_option?(command) #:nodoc: + command && stop_on_unknown_option.include?(command.name.to_sym) + end + + + # Disable the check for required options for the given commands. + # This is useful if you have a command that does not need the required options + # to work, like help. + # + # ==== Parameters + # Symbol ...:: A list of commands that should be affected. + def self.disable_required_check!(*command_names) + disable_required_check.merge(command_names) + end + + + def self.disable_required_check?(command) #:nodoc: + command && disable_required_check.include?(command.name.to_sym) + end + + + protected # Class Methods + # ============================================================================ + + def self.stop_on_unknown_option #:nodoc: + @stop_on_unknown_option ||= Set.new end - def disable_required_check?(command) #:nodoc: - command && disable_required_check.include?(command.name.to_sym) + + # help command has the required check disabled by default. + def self.disable_required_check #:nodoc: + @disable_required_check ||= Set.new([:help]) end - - # Atli Public Class Methods - # ======================================================================== - - # @return [Hash<Symbol, Thor::SharedOption] - # Get all shared options + + + # The method responsible for dispatching given the args. # - def shared_method_options(options = nil) - @shared_method_options ||= begin - # Reach up the inheritance chain, if there's anyone there - if superclass.respond_to? __method__ - superclass.send( __method__ ).dup - else - # Or just default to empty - {} + # @param [nil | String | Symbol] meth + # The method name of the command to run, which I *think* will be `nil` + # when running the "default command"? haven't messed with that yet... + # + # @param [Array<String>] given_args + # + def self.dispatch meth, given_args, given_opts, config # rubocop:disable MethodLength + logger.trace "START #{ self.safe_name }.#{ __method__ }", + meth: meth, + given_args: given_args, + given_opts: given_opts, + config: config + + meth ||= retrieve_command_name( given_args ).tap { |new_meth| + logger.trace "meth set via .retrieve_command_name", + meth: new_meth, + map: map + } + + normalized_name = normalize_command_name meth + command = all_commands[normalized_name] + + logger.trace "Fetched command", + command: command, + all_commands: all_commands.keys, + normalized_name: normalized_name + + if !command && config[:invoked_via_subcommand] + # We're a subcommand and our first argument didn't match any of our + # commands. So we put it back and call our default command. + given_args.unshift(meth) + command = all_commands[normalize_command_name(default_command)] + end + + if command + args, opts = Thor::Options.split(given_args) + if stop_on_unknown_option?(command) && !args.empty? + # given_args starts with a non-option, so we treat everything as + # ordinary arguments + args.concat opts + opts.clear end + else + args = given_args + opts = nil + command = dynamic_command_class.new(meth) end - - if options - # We don't support this (yet at least) - raise NotImplementedError, - "Bulk set not supported, use .shared_method_option" - # build_shared_options(options, @shared_method_options) - end - @shared_method_options + + opts = given_opts || opts || [] + config[:current_command] = command + config[:command_options] = command.options + + instance = new(args, opts, config) + yield instance if block_given? + args = instance.args + trailing = args[ arguments( command: command ).size..-1 ] + instance.invoke_command(command, trailing || []) end - alias_method :shared_options, :shared_method_options - # Find shared options given names and groups. + # The banner for this class. You can customize it if you are invoking the + # thor class by another ways which is not the Thor::Runner. It receives + # the command that is going to be invoked and a boolean which indicates if + # the namespace should be displayed as arguments. # - # @param [*<Symbol>] names - # Individual shared option names to include. + # @param [Thor::Command] command + # The command to render the banner for. # - # @param [nil | Symbol | Enumerable<Symbol>] groups: - # Single or list of shared option groups to include. + # @param [nil | ?] namespace + # *Atli*: this argument is _not_ _used_ _at_ _all_. I don't know what it + # could or should be, but it doesn't seem like it matters at all :/ + # + # @param [Boolean] subcommand + # Should be +true+ if the command was invoked as a sub-command; passed + # on to {Command#formatted_usage} so it can render correctly. # - # @return [Hash<Symbol, Thor::SharedOption>] - # Hash mapping option names (as {Symbol}) to instances. + # @return [String] + # The banner for the command. # - def find_shared_method_options *names, groups: nil - groups_set = Set[*groups] + def self.banner(command, namespace = nil, subcommand = false) + "#{basename} #{command.formatted_usage(self, $thor_runner, subcommand)}" + end + + + def self.baseclass #:nodoc: + Thor + end + + + def self.dynamic_command_class #:nodoc: + Thor::DynamicCommand + end + + + def self.create_command(meth) #:nodoc: + @usage ||= nil + @desc ||= nil + @long_desc ||= nil + @hide ||= nil - shared_method_options.each_with_object( {} ) do |(name, option), results| - match = {} + examples = @examples || [] + @examples = [] + + if @usage && @desc + base_class = @hide ? Thor::HiddenCommand : Thor::Command + commands[meth] = base_class.new \ + name: meth, + description: @desc, + long_description: @long_desc, + usage: @usage, + examples: examples, + options: method_options, + arguments: method_arguments - if names.include? name - match[:name] = true - end - - match_groups = option.groups & groups_set - - unless match_groups.empty? - match[:groups] = match_groups - end - - unless match.empty? - results[name] = { - option: option, - match: match, - } - end + @usage, + @desc, + @long_desc, + @method_options, + @hide, + @method_arguments = nil + + true + elsif all_commands[meth] || meth == "method_missing" + true + else + puts "[WARNING] Attempted to create command #{meth.inspect} without usage or description. " \ + "Call desc if you want this method to be available as command or declare it inside a " \ + "no_commands{} block. Invoked from #{caller[1].inspect}." + false end end - alias_method :find_shared_options, :find_shared_method_options - - - # Declare a shared method option with an optional groups that can then - # be added by name or group to commands. + + singleton_class.send :alias_method, :create_task, :create_command + + + def self.initialize_added #:nodoc: + class_options.merge!(method_options) + @method_options = nil + end + + + # Retrieve the command name from given args. # - # The shared options can then be added to methods individually by name and - # collectively as groups with {Thor.include_method_options}. + # @note + # Ugh... this *mutates* `args` # - # @example - # class MyCLI < Thor - # - # # Declare a shared option: - # shared_option :force, - # groups: :write, - # desc: "Force the operation", - # type: :boolean - # - # # ... - # - # desc "write [OPTIONS] path", - # "Write to a path" - # - # # Add the shared options to the method: - # include_options groups: :write - # - # def write path - # - # # Get a slice of `#options` with any of the `:write` group options - # # that were provided and use it in a method call: - # MyModule.write path, **option_kwds( groups: :write ) - # - # end - # end + # @param [Array<String>] args # - # @param [Symbol] name - # The name of the option. - # - # @param [**<Symbol, V>] options - # Keyword args used to initialize the {Thor::SharedOption}. + # @return [nil]` # - # All +**options+ are optional. # - # @option options [Symbol | Array<Symbol>] :groups - # One or more _shared_ _option_ _group_ that the new option will belong - # to. - # - # Examples: - # groups: :read - # groups: [:read, :write] - # - # *NOTE* The keyword is +groups+ with an +s+! {Thor::Option} already has - # a +group+ string attribute that, as far as I can tell, is only - # + def self.retrieve_command_name args + meth = args.first.to_s unless args.empty? + args.shift if meth && (map[meth] || meth !~ /^\-/) + end + + singleton_class.send :alias_method, :retrieve_task_name, + :retrieve_command_name + + + # Receives a (possibly nil) command name and returns a name that is in + # the commands hash. In addition to normalizing aliases, this logic + # will determine if a shortened command is an unambiguous substring of + # a command or alias. + # + # {.normalize_command_name} also converts names like `animal-prison` + # into `animal_prison`. # - # - # @option options [String] :desc - # Description for the option for help and feedback. - # - # @option options [Boolean] :required - # If the option is required or not. - # - # @option options [Object] :default - # Default value for this argument. + # @param [nil | ] meth # - # It cannot be +required+ and have default values. # - # @option options [String | Array<String>] :aliases - # Aliases for this option. - # - # Examples: - # aliases: '-s' - # aliases: '--other-name' - # aliases: ['-s', '--other-name'] - # - # @option options [:string | :hash | :array | :numeric | :boolean] :type - # Type of acceptable values, see - # {types for method options}[https://github.com/erikhuda/thor/wiki/Method-Options#types-for-method_options] - # in the Thor wiki. - # - # @option options [String] :banner - # String to show on usage notes. - # - # @option options [Boolean] :hide - # If you want to hide this option from the help. - # - # @return (see .build_shared_option) - # - def shared_method_option name, **options - # Don't think the `:for` option makes sense... that would just be a - # regular method option, right? I guess `:for` could be an array and - # apply the option to each command, but it seems like that would just - # be better as an extension to the {.method_option} behavior. - # - # So, we raise if we see it - if options.key? :for - raise ArgumentError, - ".shared_method_option does not accept the `:for` option" - end - - build_shared_option(name, options) - end # #shared_method_option - alias_method :shared_option, :shared_method_option - - - # Add the {Thor::SharedOption} instances with +names+ and in +groups+ to - # the next defined command method. - # - # @param (see .find_shared_method_options) - # @return (see .find_shared_method_options) - # - def include_method_options *names, groups: nil - find_shared_method_options( *names, groups: groups ). - each do |name, result| - method_options[name] = Thor::IncludedOption.new **result - end - end - - alias_method :include_options, :include_method_options - - # END Atli Public Class Methods ****************************************** - - - protected # Class Methods - # ============================================================================ + def self.normalize_command_name meth #:nodoc: + return default_command.to_s.tr("-", "_") unless meth - def stop_on_unknown_option #:nodoc: - @stop_on_unknown_option ||= Set.new + possibilities = find_command_possibilities(meth) + + if possibilities.size > 1 + raise AmbiguousTaskError, + "Ambiguous command #{meth} matches [#{possibilities.join(', ')}]" end - # help command has the required check disabled by default. - def disable_required_check #:nodoc: - @disable_required_check ||= Set.new([:help]) + if possibilities.empty? + meth ||= default_command + elsif map[meth] + meth = map[meth] + else + meth = possibilities.first end - # The method responsible for dispatching given the args. - def dispatch(meth, given_args, given_opts, config) #:nodoc: # rubocop:disable MethodLength - meth ||= retrieve_command_name(given_args) - command = all_commands[normalize_command_name(meth)] + meth.to_s.tr("-", "_") # treat foo-bar as foo_bar + end - if !command && config[:invoked_via_subcommand] - # We're a subcommand and our first argument didn't match any of our - # commands. So we put it back and call our default command. - given_args.unshift(meth) - command = all_commands[normalize_command_name(default_command)] - end + singleton_class.send :alias_method, :normalize_task_name, + :normalize_command_name - if command - args, opts = Thor::Options.split(given_args) - if stop_on_unknown_option?(command) && !args.empty? - # given_args starts with a non-option, so we treat everything as - # ordinary arguments - args.concat opts - opts.clear - end - else - args = given_args - opts = nil - command = dynamic_command_class.new(meth) - end - opts = given_opts || opts || [] - config[:current_command] = command - config[:command_options] = command.options + # This is the logic that takes the command name passed in by the user + # and determines whether it is an unambiguous prefix of a command or + # alias name. + # + # @param [#to_s] input + # Input to match to command names. + # + def self.find_command_possibilities input + input = input.to_s - instance = new(args, opts, config) - yield instance if block_given? - args = instance.args - trailing = args[Range.new(arguments.size, -1)] - instance.invoke_command(command, trailing || []) - end - - - # The banner for this class. You can customize it if you are invoking the - # thor class by another ways which is not the Thor::Runner. It receives - # the command that is going to be invoked and a boolean which indicates if - # the namespace should be displayed as arguments. - # - # @param [Thor::Command] command - # The command to render the banner for. - # - # @param [nil | ?] namespace - # *Atli*: this argument is _not_ _used_ _at_ _all_. I don't know what it - # could or should be, but it doesn't seem like it matters at all :/ - # - # @param [Boolean] subcommand - # Should be +true+ if the command was invoked as a sub-command; passed - # on to {Command#formatted_usage} so it can render correctly. - # - # @return [String] - # The banner for the command. - # - def banner(command, namespace = nil, subcommand = false) - "#{basename} #{command.formatted_usage(self, $thor_runner, subcommand)}" - end - - - def baseclass #:nodoc: - Thor - end + possibilities = all_commands.merge(map).keys.select { |name| + name.start_with? input + }.sort - def dynamic_command_class #:nodoc: - Thor::DynamicCommand - end + unique_possibilities = possibilities.map { |k| map[k] || k }.uniq - def create_command(meth) #:nodoc: - @usage ||= nil - @desc ||= nil - @long_desc ||= nil - @hide ||= nil - - examples = @examples || [] - @examples = [] - - if @usage && @desc - base_class = @hide ? Thor::HiddenCommand : Thor::Command - commands[meth] = base_class.new \ - name: meth, - description: @desc, - long_description: @long_desc, - usage: @usage, - examples: examples, - options: method_options - - @usage, @desc, @long_desc, @method_options, @hide = nil - true - elsif all_commands[meth] || meth == "method_missing" - true - else - puts "[WARNING] Attempted to create command #{meth.inspect} without usage or description. " \ - "Call desc if you want this method to be available as command or declare it inside a " \ - "no_commands{} block. Invoked from #{caller[1].inspect}." - false - end + results = if possibilities.include? input + [ input ] + elsif unique_possibilities.size == 1 + unique_possibilities + else + possibilities end - alias_method :create_task, :create_command - def initialize_added #:nodoc: - class_options.merge!(method_options) - @method_options = nil - end + logger.trace "Found #{ results.length } command possibilities", + results: results, + possibilities: possibilities, + unique_possibilities: unique_possibilities + + results + end - # Retrieve the command name from given args. - def retrieve_command_name(args) #:nodoc: - meth = args.first.to_s unless args.empty? - args.shift if meth && (map[meth] || meth !~ /^\-/) - end - alias_method :retrieve_task_name, :retrieve_command_name + singleton_class.send :alias_method, :find_task_possibilities, + :find_command_possibilities - # receives a (possibly nil) command name and returns a name that is in - # the commands hash. In addition to normalizing aliases, this logic - # will determine if a shortened command is an unambiguous substring of - # a command or alias. - # - # +normalize_command_name+ also converts names like +animal-prison+ - # into +animal_prison+. - def normalize_command_name(meth) #:nodoc: - return default_command.to_s.tr("-", "_") unless meth - possibilities = find_command_possibilities(meth) + def self.subcommand_help(cmd) + # logger.trace __method__.to_s, + # cmd: cmd, + # caller: caller + + desc "help [COMMAND]", "Describe subcommands or one specific subcommand" + + # Atli - This used to be {#class_eval} (maybe to support really old + # Rubies? Who knows...) but that made it really hard to find in + # stack traces, so I switched it to {#define_method}. + # + define_method :help do |*args| - if possibilities.size > 1 - raise AmbiguousTaskError, - "Ambiguous command #{meth} matches [#{possibilities.join(', ')}]" - end - - if possibilities.empty? - meth ||= default_command - elsif map[meth] - meth = map[meth] + # Add the `is_subcommand = true` trailing arg + case args[-1] + when true + # pass + when false + # Weird, `false` was explicitly passed... whatever, set it to `true` + args[-1] = true else - meth = possibilities.first + # "Normal" case, append it + args << true end - - meth.to_s.tr("-", "_") # treat foo-bar as foo_bar - end - alias_method :normalize_task_name, :normalize_command_name - - # this is the logic that takes the command name passed in by the user - # and determines whether it is an unambiguous substrings of a command or - # alias name. - def find_command_possibilities(meth) - len = meth.to_s.length - possibilities = all_commands.merge(map).keys.select { |n| - meth == n[0, len] - }.sort - unique_possibilities = possibilities.map { |k| map[k] || k }.uniq - - if possibilities.include?(meth) - [meth] - elsif unique_possibilities.size == 1 - unique_possibilities - else - possibilities - end - end - alias_method :find_task_possibilities, :find_command_possibilities - - def subcommand_help(cmd) - logger.trace __method__.to_s, - cmd: cmd, - caller: caller - desc "help [COMMAND]", "Describe subcommands or one specific subcommand" - - # Atli - This used to be {#class_eval} (maybe to support really old - # Rubies? Who knows...) but that made it really hard to find in - # stack traces, so I switched it to {#define_method}. - # - define_method :help do |*args| - - # Add the `is_subcommand = true` trailing arg - case args[-1] - when true - # pass - when false - # Weird, `false` was explicitly passed... whatever, set it to `true` - args[-1] = true - else - # "Normal" case, append it - args << true - end - - super( *args ) - end - + super( *args ) end - alias_method :subtask_help, :subcommand_help - # Atli Protected Class Methods - # ====================================================================== - - # Build a Thor::SharedOption and add it to Thor.shared_method_options. - # - # The Thor::SharedOption is returned. - # - # ==== Parameters - # name<Symbol>:: The name of the argument. - # options<Hash>:: Described in both class_option and method_option, - # with the additional `:groups` shared option keyword. - def build_shared_option(name, options) - shared_method_options[name] = Thor::SharedOption.new( - name, - options.merge(:check_default_type => check_default_type?) - ) - end # #build_shared_option + end + + singleton_class.send :alias_method, :subtask_help, :subcommand_help - # END protected Class Methods ******************************************** + + # Atli Protected Class Methods + # ====================================================================== + + # Build a Thor::SharedOption and add it to Thor.shared_method_options. + # + # The Thor::SharedOption is returned. + # + # ==== Parameters + # name<Symbol>:: The name of the argument. + # options<Hash>:: Described in both class_option and method_option, + # with the additional `:groups` shared option keyword. + def self.build_shared_option(name, options) + shared_method_options[name] = Thor::SharedOption.new( + name, + options.merge(:check_default_type => check_default_type?) + ) + end # .build_shared_option - end # class << self ******************************************************** + public # END protected Class Methods *************************************** protected # Instance Methods # ==========================================================================