require 'shellwords' module Boson # A class used by Scientist to wrap around Command objects. It's main purpose is to parse # a command's global options (basic, render and pipe types) and local options. # As the names imply, global options are available to all commands while local options are specific to a command. # When passing options to commands, global ones _must_ be passed first, then local ones. # Also, options _must_ all be passed either before or after arguments. # For more about pipe and render options see Pipe and View respectively. # # === Basic Global Options # Any command with options comes with basic global options. For example '-hv' on an option command # prints a help summarizing global and local options. Another basic global option is --pretend. This # option displays what global options have been parsed and the actual arguments to be passed to a # command if executed. For example: # # # Define this command in a library # options :level=>:numeric, :verbose=>:boolean # def foo(*args) # args # end # # irb>> foo 'testin -p -l=1' # Arguments: ["testin", {:level=>1}] # Global options: {:pretend=>true} # # If a global option conflicts with a local option, the local option takes precedence. You can get around # this by passing global options after a '-'. For example, if the global option -f (--fields) conflicts with # a local -f (--force): # foo 'arg1 -v -f - -f=f1,f2' # # is the same as # foo 'arg1 -v --fields=f1,f2 -f' # # === Toggling Views With the Basic Global Option --render # One of the more important global options is --render. This option toggles the rendering of a command's # output done with View and Hirb[http://github.com/cldwalker/hirb]. # # Here's a simple example of toggling Hirb's table view: # # Defined in a library file: # #@options {} # def list(options={}) # [1,2,3] # end # # Using it in irb: # >> list # => [1,2,3] # >> list '-r' # or list --render # +-------+ # | value | # +-------+ # | 1 | # | 2 | # | 3 | # +-------+ # 3 rows in set # => true class OptionCommand # ArgumentError specific to @command's arguments class CommandArgumentError < ::ArgumentError; end BASIC_OPTIONS = { :help=>{:type=>:boolean, :desc=>"Display a command's help"}, :render=>{:type=>:boolean, :desc=>"Toggle a command's default rendering behavior"}, :verbose=>{:type=>:boolean, :desc=>"Increase verbosity for help, errors, etc."}, :usage_options=>{:type=>:string, :desc=>"Render options to pass to usage/help"}, :pretend=>{:type=>:boolean, :desc=>"Display what a command would execute without executing it"}, :delete_options=>{:type=>:array, :desc=>'Deletes global options starting with given strings' } } #:nodoc: RENDER_OPTIONS = { :fields=>{:type=>:array, :desc=>"Displays fields in the order given"}, :class=>{:type=>:string, :desc=>"Hirb helper class which renders"}, :max_width=>{:type=>:numeric, :desc=>"Max width of a table"}, :vertical=>{:type=>:boolean, :desc=>"Display a vertical table"}, } #:nodoc: PIPE_OPTIONS = { :sort=>{:type=>:string, :desc=>"Sort by given field"}, :reverse_sort=>{:type=>:boolean, :desc=>"Reverse a given sort"}, :query=>{:type=>:hash, :desc=>"Queries fields given field:search pairs"}, :pipes=>{:alias=>'P', :type=>:array, :desc=>"Pipe to commands sequentially"} } #:nodoc: class < 1 && args[-1].is_a?(String) temp_args = Runner.in_shell? ? args : Shellwords.shellwords(args.pop) global_opt, parsed_options, new_args = parse_options temp_args Runner.in_shell? ? args = new_args : args += new_args # add default options elsif @command.options.to_s.empty? || (!@command.has_splat_args? && args.size <= (@command.arg_size - 1).abs) || (@command.has_splat_args? && !args[-1].is_a?(Hash)) global_opt, parsed_options = parse_options([])[0,2] # merge default options with given hash of options elsif (@command.has_splat_args? || (args.size == @command.arg_size)) && args[-1].is_a?(Hash) global_opt, parsed_options = parse_options([])[0,2] parsed_options.merge!(args.pop) end [global_opt || {}, parsed_options, args] end #:stopdoc: def parse_options(args) parsed_options = @command.option_parser.parse(args, :delete_invalid_opts=>true) trailing, unparseable = split_trailing global_options = parse_global_options @command.option_parser.leading_non_opts + trailing new_args = option_parser.non_opts.dup + unparseable [global_options, parsed_options, new_args] rescue OptionParser::Error global_options = parse_global_options @command.option_parser.leading_non_opts + split_trailing[0] global_options[:help] ? [global_options, nil, []] : raise end def split_trailing trailing = @command.option_parser.trailing_non_opts if trailing[0] == '--' trailing.shift [ [], trailing ] else trailing.shift if trailing[0] == '-' [ trailing, [] ] end end def parse_global_options(args) option_parser.parse args end def option_parser @option_parser ||= @command.render_options ? OptionParser.new(all_global_options) : self.class.default_option_parser end def all_global_options OptionParser.make_mergeable! @command.render_options render_opts = Util.recursive_hash_merge(@command.render_options, Util.deep_copy(self.class.default_render_options)) merged_opts = Util.recursive_hash_merge Util.deep_copy(self.class.default_pipe_options), render_opts opts = Util.recursive_hash_merge merged_opts, Util.deep_copy(BASIC_OPTIONS) set_global_option_defaults opts end def set_global_option_defaults(opts) if !opts[:fields].key?(:values) if opts[:fields][:default] opts[:fields][:values] = opts[:fields][:default] else if opts[:change_fields] && (changed = opts[:change_fields][:default]) opts[:fields][:values] = changed.is_a?(Array) ? changed : changed.values end opts[:fields][:values] ||= opts[:headers][:default].keys if opts[:headers] && opts[:headers][:default] end opts[:fields][:enum] = false if opts[:fields][:values] && !opts[:fields].key?(:enum) end if opts[:fields][:values] opts[:sort][:values] ||= opts[:fields][:values] opts[:query][:keys] ||= opts[:fields][:values] opts[:query][:default_keys] ||= "*" end opts end def modify_args(args) if @command.default_option && @command.arg_size <= 1 && !@command.has_splat_args? && args[0].to_s[/./] != '-' args[0] = "--#{@command.default_option}=#{args[0]}" unless args.join.empty? || args[0].is_a?(Hash) end end def check_argument_size(args) if args.size != @command.arg_size && !@command.has_splat_args? command_size, args_size = args.size > @command.arg_size ? [@command.arg_size, args.size] : [@command.arg_size - 1, args.size - 1] raise CommandArgumentError, "wrong number of arguments (#{args_size} for #{command_size})" end end def add_default_args(args, obj) if @command.args && args.size < @command.args.size - 1 # leave off last arg since its an option @command.args.slice(0..-2).each_with_index {|arr,i| next if args.size >= i + 1 # only fill in once args run out break if arr.size != 2 # a default arg value must exist begin args[i] = @command.file_parsed_args? ? obj.instance_eval(arr[1]) : arr[1] rescue Exception raise Scientist::Error, "Unable to set default argument at position #{i+1}.\nReason: #{$!.message}" end } end end #:startdoc: end end