boson 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,12 @@
1
1
  module Boson
2
2
  # This library loads a gem by the given name. Unlike FileLibrary or ModuleLibrary, this library
3
3
  # doesn't need a module to provide its functionality.
4
+ #
5
+ # Example:
6
+ # >> load_library 'httparty', :class_commands=>{'put'=>'HTTParty.put',
7
+ # 'delete'=>'HTTParty.delete' }
8
+ # => true
9
+ # >> put 'http://someurl.com'
4
10
  class GemLibrary < Library
5
11
  #:stopdoc:
6
12
  def self.is_a_gem?(name)
@@ -1,6 +1,22 @@
1
1
  module Boson
2
- # This library takes a module as a library's name. Reload for this library
3
- # subclass is disabled.
2
+ # This library takes a module or class as a library's name and loads its class methods
3
+ # as commands. If no commands are given it defaults to loading all of its class methods
4
+ # as commands. The only method callback (see Loader) this library calls on the
5
+ # original module/class is config().
6
+ #
7
+ # Example:
8
+ # >> load_library Math, :commands=>%w{sin cos tan}
9
+ # => true
10
+ #
11
+ # # Let's brush up on ol trig
12
+ # >> sin (Math::PI/2)
13
+ # => 1.0
14
+ # >> tan (Math::PI/4)
15
+ # => 1.0
16
+ # # Close enough :)
17
+ # >> cos (Math::PI/2)
18
+ # => 6.12323399573677e-17
19
+
4
20
  class ModuleLibrary < Library
5
21
  #:stopdoc:
6
22
  handles {|source| source.is_a?(Module) }
@@ -11,7 +27,11 @@ module Boson
11
27
  super Util.underscore(underscore_lib)
12
28
  end
13
29
 
14
- def reload; false; end
30
+ def initialize_library_module
31
+ @class_commands = {@module.to_s=>Array(@commands).empty? ? @module.methods(false) : @commands }
32
+ @module = nil
33
+ super
34
+ end
15
35
  #:startdoc:
16
36
  end
17
- end
37
+ end
@@ -1,5 +1,13 @@
1
1
  # This library requires the given name. This is useful for loading standard libraries,
2
2
  # non-gem libraries (i.e. rip packages) and anything else in $LOAD_PATH.
3
+ #
4
+ # Example:
5
+ # >> load_library 'fileutils', :class_commands=>{'cd'=>'FileUtils.cd', 'cp'=>'FileUtils.cp'}
6
+ # => true
7
+ # >> cd '/home'
8
+ # => 0
9
+ # >> Dir.pwd
10
+ # >> '/home'
3
11
  class Boson::RequireLibrary < Boson::GemLibrary
4
12
  handles {|source|
5
13
  begin
data/lib/boson/library.rb CHANGED
@@ -2,7 +2,33 @@ module Boson
2
2
  # A library is a group of commands (Command objects) usually grouped together by a module.
3
3
  # Libraries are loaded from different sources depending on the library subclass. Default library
4
4
  # subclasses are FileLibrary, GemLibrary, RequireLibrary and ModuleLibrary.
5
+ # See Loader for callbacks a library's module can have.
5
6
  #
7
+ # == Naming a Library Module
8
+ # Although you can name a library module almost anything, here's the fine print:
9
+ # * A module can have any name if it's the only module in a library.
10
+ # * If there are multiple modules in a file library, the module's name must be a camelized version
11
+ # of the file's basename i.e. ~/.boson/commands/ruby_core.rb -> RubyCore.
12
+ # * Although modules are evaluated under the Boson::Commands namespace, Boson will warn you about creating
13
+ # modules whose name is the same as a top level class/module. The warning is to encourage users to stay
14
+ # away from error-prone libraries. Once you introduce such a module, _all_ libraries assume the nested module
15
+ # over the top level module and the top level module has to be prefixed with '::' _everywhere_.
16
+ #
17
+ # == Configuration
18
+ # To configure a library, pass a hash of {library attributes}[link:classes/Boson/Library.html#M000077] under
19
+ # {the :libraries key}[link:classes/Boson/Repo.html#M000070] of a config file. When not using FileLibrary,
20
+ # you can give most commands the functionality FileLibrary naturally gives its commands by configuring
21
+ # the :commands key. Here's a config example of a GemLibrary that does that:
22
+ # :libraries:
23
+ # httparty:
24
+ # :class_commands:
25
+ # delete: HTTParty.delete
26
+ # :commands:
27
+ # delete:
28
+ # :alias: d
29
+ # :description: Http delete a given url
30
+ #
31
+ # === Creating Your Own Library
6
32
  # To create your own subclass you need to define what sources the subclass can handle with handles().
7
33
  # If handles() returns true then the subclass is chosen to load. See Loader to see what instance methods
8
34
  # to override for a subclass.
@@ -18,49 +44,49 @@ module Boson
18
44
  end
19
45
 
20
46
  # Public attributes for use outside of Boson.
21
- ATTRIBUTES = [:gems, :dependencies, :commands, :loaded, :module, :name, :namespace]
47
+ ATTRIBUTES = [:gems, :dependencies, :commands, :loaded, :module, :name, :namespace, :indexed_namespace]
22
48
  attr_reader *(ATTRIBUTES + [:commands_hash, :library_file, :object_namespace])
23
49
  # Private attribute for use within Boson.
24
- attr_reader :except, :no_alias_creation, :new_module, :new_commands
50
+ attr_reader :no_alias_creation, :new_module, :new_commands
25
51
  # Optional namespace name for a library. When enabled defaults to a library's name.
26
52
  attr_writer :namespace
27
53
 
28
54
  # Creates a library object with a hash of attributes which must include a :name attribute.
29
55
  # Each hash pair maps directly to an instance variable and value. Defaults for attributes
30
- # are read from config[:libraries][@library_name][@attribute].
56
+ # are read from config[:libraries][@library_name][@attribute]. When loading libraries, attributes
57
+ # can also be set via a library module's config() method (see Loader).
31
58
  #
32
59
  # Attributes that can be configured:
33
- # * *:dependencies*: An array of libraries that this library depends on. A library won't load
34
- # unless its dependencies are loaded first.
35
- # * *:commands*: A hash or array of commands that belong to this library. A hash configures command attributes
36
- # for the given commands with command names pointing to their configs. See Command.new for a
37
- # command's configurable attributes. If an array, the commands are set for the given library,
38
- # overidding default command detection.
39
- # Example:
40
- # :commands=>{'commands'=>{:description=>'Lists commands', :alias=>'com'}}
41
- # * *:class_commands*: A hash of commands to create. Hash should map command names to any string of ruby code
42
- # that ends with a method call.
43
- # Example:
44
- # :class_commands=>{'spy'=>'Bond.spy', 'create'=>'Alias.manager.create'}
45
- # * *:force*: Boolean which forces a library to ignore when a library's methods are overriding existing ones.
46
- # Use with caution. Default is false.
47
- # * *:object_methods*: Boolean which detects any Object/Kernel methods created when loading a library and automatically
48
- # adds them to a library's commands. Default is true.
49
- # * *:namespace*: Boolean or string which namespaces a library. When true, the library is automatically namespaced
50
- # to the library's name. When a string, the library is namespaced to the string. Default is nil. To control the
51
- # namespacing of all libraries see Boson::Repo.config.
60
+ # [*:dependencies*] An array of libraries that this library depends on. A library won't load
61
+ # unless its dependencies are loaded first.
62
+ # [*:commands*] A hash or array of commands that belong to this library. A hash configures command attributes
63
+ # for the given commands with command names pointing to their configs. See Command.new for a
64
+ # command's configurable attributes. If an array, the commands are set for the given library,
65
+ # overidding default command detection. Example:
66
+ # :commands=>{'commands'=>{:description=>'Lists commands', :alias=>'com'}}
67
+ # [*:class_commands*] A hash of commands to create. A hash key-pair can map command names to any string of ruby code
68
+ # that ends with a method call. Or a key-pair can map a class to an array of its class methods
69
+ # to create commands of the same name. Example:
70
+ # :class_commands=>{'spy'=>'Bond.spy', 'create'=>'Alias.manager.create',
71
+ # 'Boson::Util'=>['detect', 'any_const_get']}
72
+ # [*:force*] Boolean which forces a library to ignore when a library's methods are overriding existing ones.
73
+ # Use with caution. Default is false.
74
+ # [*:object_methods*] Boolean which detects any Object/Kernel methods created when loading a library and automatically
75
+ # adds them to a library's commands. Default is true.
76
+ # [*:namespace*] Boolean or string which namespaces a library. When true, the library is automatically namespaced
77
+ # to the library's name. When a string, the library is namespaced to the string. Default is nil.
78
+ # To control the namespacing of all libraries see Boson::Repo.config.
79
+ # [*:no_alias_creation*] Boolean which doesn't create aliases for a library. Useful for libraries that configure command
80
+ # aliases outside of Boson's control. Default is false.
52
81
  def initialize(hash)
53
- @name = set_name hash.delete(:name)
54
- @loaded = false
55
82
  repo = set_repo
56
83
  @repo_dir = repo.dir
84
+ @name = set_name hash.delete(:name)
85
+ @loaded = false
57
86
  @commands_hash = {}
58
87
  @commands = []
59
- set_config (repo.config[:libraries][@name] || {}).merge(hash)
88
+ set_config (repo.config[:libraries][@name] || {}).merge(hash), true
60
89
  set_command_aliases(repo.config[:command_aliases])
61
- @namespace = true if Boson.repo.config[:auto_namespace] && @namespace.nil? &&
62
- !Boson::Runner.default_libraries.include?(@module)
63
- @namespace = clean_name if @namespace
64
90
  end
65
91
 
66
92
  # A concise symbol version of a library type i.e. FileLibrary -> :file.
@@ -69,9 +95,19 @@ module Boson
69
95
  str.downcase.to_sym
70
96
  end
71
97
 
98
+ def namespace(orig=@namespace)
99
+ @namespace = [String,FalseClass].include?(orig.class) ? orig : begin
100
+ if (@namespace == true || (Boson.repo.config[:auto_namespace] && !@index))
101
+ @namespace = clean_name
102
+ else
103
+ @namespace = false
104
+ end
105
+ end
106
+ end
107
+
72
108
  # The object a library uses for executing its commands.
73
109
  def namespace_object
74
- @namespace_object ||= @namespace ? Boson.invoke(@namespace) : Boson.main_object
110
+ @namespace_object ||= namespace ? Boson.invoke(namespace) : Boson.main_object
75
111
  end
76
112
 
77
113
  #:stopdoc:
@@ -84,7 +120,7 @@ module Boson
84
120
  name.to_s or raise ArgumentError, "New library missing required key :name"
85
121
  end
86
122
 
87
- def set_config(config)
123
+ def set_config(config, force=false)
88
124
  if (commands = config.delete(:commands))
89
125
  if commands.is_a?(Array)
90
126
  @commands += commands
@@ -95,7 +131,7 @@ module Boson
95
131
  end
96
132
  end
97
133
  set_command_aliases config.delete(:command_aliases) if config[:command_aliases]
98
- set_attributes config, true
134
+ set_attributes config, force
99
135
  end
100
136
 
101
137
  def set_command_aliases(command_aliases)
@@ -113,16 +149,20 @@ module Boson
113
149
  hash.each {|k,v| instance_variable_set("@#{k}", v) if instance_variable_get("@#{k}").nil? || force }
114
150
  end
115
151
 
116
- def command_objects(names)
117
- Boson.commands.select {|e| names.include?(e.name) && e.lib == self.name }
152
+ def command_objects(names=self.commands, command_array=Boson.commands)
153
+ command_array.select {|e| names.include?(e.name) && e.lib == self.name }
154
+ end
155
+
156
+ def command_object(name)
157
+ command_objects([name])[0]
118
158
  end
119
159
 
120
160
  def marshal_dump
121
- [@name, @commands, @gems, @module.to_s, @repo_dir]
161
+ [@name, @commands, @gems, @module.to_s, @repo_dir, @indexed_namespace]
122
162
  end
123
163
 
124
164
  def marshal_load(ary)
125
- @name, @commands, @gems, @module, @repo_dir = ary
165
+ @name, @commands, @gems, @module, @repo_dir, @indexed_namespace = ary
126
166
  end
127
167
  #:startdoc:
128
168
  end
data/lib/boson/loader.rb CHANGED
@@ -5,6 +5,27 @@ module Boson
5
5
  # This module is mixed into Library to give it load() and reload() functionality.
6
6
  # When creating your own Library subclass, you should override load_source_and_set_module and
7
7
  # reload_source_and_set_module . You can override other methods in this module as needed.
8
+ #
9
+ # === Module Callbacks
10
+ # For libraries that have a module i.e. FileLibrary and GemLibrary, the following class methods
11
+ # are invoked in the order below when loading a library:
12
+ #
13
+ # [*:config*] This method returns a library's hash of attributes as explained by Library.new. This is useful
14
+ # for distributing libraries with a default configuration. The library attributes specified here
15
+ # are overridden by ones a user has in their config file except for the :commands attribute, which
16
+ # is recursively merged together.
17
+ # [*:append_features*] In addition to its normal behavior, this method's return value determines if a
18
+ # library is loaded in the current environment. This is useful for libraries that you
19
+ # want loaded by default but not in some environments i.e. different ruby versions or
20
+ # in irb but not in script/console. Remember to use super when returning true.
21
+ # [*:included*] In addition to its normal behavior, this method should be used to require external libraries.
22
+ # Although requiring dependencies could be done anywhere in a module, putting dependencies here
23
+ # are encouraged. By not having dependencies hardcoded in a module, it's possible to analyze
24
+ # and view a library's commands without having to install and load its dependencies.
25
+ # If creating commands here, note that conflicts with existing commands won't be detected.
26
+ # [*:after_included*] This method is called after included() to initialize functionality. This is useful for
27
+ # libraries that are primarily executing ruby code i.e. defining ruby extensions or
28
+ # setting irb features. This method isn't called when indexing a library.
8
29
  module Loader
9
30
  # Loads a library and its dependencies and returns true if library loads correctly.
10
31
  def load
@@ -12,9 +33,9 @@ module Boson
12
33
  load_source_and_set_module
13
34
  module_callbacks if @module
14
35
  yield if block_given?
15
- (@module || @class_commands) ? detect_additions { load_module_commands } : @namespace = nil
16
- @init_methods.each {|m| namespace_object.send(m) if namespace_object.respond_to?(m) } if @init_methods && !@index
36
+ (@module || @class_commands) ? detect_additions { load_module_commands } : @namespace = false
17
37
  set_library_commands
38
+ @indexed_namespace = (@namespace == false) ? nil : @namespace if @index
18
39
  loaded_correctly? && (@loaded = true)
19
40
  end
20
41
 
@@ -26,7 +47,8 @@ module Boson
26
47
  !!@module
27
48
  end
28
49
 
29
- # Reloads a library from its source and adds new commands.
50
+ # Reloads a library from its source and adds new commands. Only implemented
51
+ # for FileLibrary for now.
30
52
  def reload
31
53
  original_commands = @commands
32
54
  reload_source_and_set_module
@@ -49,12 +71,13 @@ module Boson
49
71
  end
50
72
 
51
73
  def load_module_commands
52
- initialize_library_module
74
+ initialize_library_module
53
75
  rescue MethodConflictError=>e
54
- if Boson.repo.config[:error_method_conflicts] || @namespace
76
+ if Boson.repo.config[:error_method_conflicts] || namespace
55
77
  raise MethodConflictError, e.message
56
78
  else
57
79
  @namespace = clean_name
80
+ @method_conflict = true
58
81
  $stderr.puts "#{e.message}. Attempting load into the namespace #{@namespace}..."
59
82
  initialize_library_module
60
83
  end
@@ -71,20 +94,27 @@ module Boson
71
94
  def initialize_library_module
72
95
  @module = @module ? Util.constantize(@module) : Util.create_module(Boson::Commands, clean_name)
73
96
  raise(LoaderError, "No module for library #{@name}") unless @module
74
- Manager.create_class_aliases(@module, @class_commands) unless @class_commands.to_s.empty?
97
+ if (conflict = Util.top_level_class_conflict(Boson::Commands, @module.to_s))
98
+ warn "Library module '#{@module}' may conflict with top level class/module '#{conflict}' references in"+
99
+ " your libraries. Rename your module to avoid this warning."
100
+ end
101
+
102
+ Manager.create_class_aliases(@module, @class_commands) unless @class_commands.to_s.empty? || @method_conflict
75
103
  check_for_method_conflicts unless @force
76
104
  @namespace = clean_name if @object_namespace
77
- @namespace ? Namespace.create(@namespace, self) : include_in_universe
105
+ namespace ? Namespace.create(namespace, self) : include_in_universe
78
106
  end
79
107
 
80
108
  def include_in_universe(lib_module=@module)
81
109
  Boson::Universe.send :include, lib_module
110
+ @module.after_included if lib_module.respond_to?(:after_included) && !@index
82
111
  Boson::Universe.send :extend_object, Boson.main_object
83
112
  end
84
113
 
85
114
  def check_for_method_conflicts
86
- conflicts = @namespace ? (Boson.can_invoke?(@namespace) ? [@namespace] : []) :
87
- Util.common_instance_methods(@module, Boson::Universe)
115
+ conflicts = namespace ? (Boson.can_invoke?(namespace) ? [namespace] : []) :
116
+ (@module.instance_methods + @module.private_instance_methods) & (Boson.main_object.methods +
117
+ Boson.main_object.private_methods)
88
118
  unless conflicts.empty?
89
119
  raise MethodConflictError,"The following commands conflict with existing commands: #{conflicts.join(', ')}"
90
120
  end
@@ -93,9 +123,8 @@ module Boson
93
123
  def set_library_commands
94
124
  aliases = @commands_hash.select {|k,v| @commands.include?(k) }.map {|k,v| v[:alias]}.compact
95
125
  @commands -= aliases
96
- @commands.delete(@namespace) if @namespace
97
- @commands += Boson.invoke(@namespace).boson_commands if @namespace && !@pre_defined_commands
98
- @commands -= @except if @except
126
+ @commands.delete(namespace) if namespace
127
+ @commands += Boson.invoke(namespace).boson_commands if namespace && !@pre_defined_commands
99
128
  @commands.uniq!
100
129
  end
101
130
  #:startdoc:
data/lib/boson/manager.rb CHANGED
@@ -7,17 +7,18 @@ module Boson
7
7
  # Handles loading and reloading of libraries and commands.
8
8
  class Manager
9
9
  class <<self
10
+ attr_accessor :failed_libraries
11
+
10
12
  # Loads a library or an array of libraries with options. Manager loads the first library subclass
11
13
  # to meet a library subclass' criteria in this order: ModuleLibrary, FileLibrary, GemLibrary, RequireLibrary.
12
14
  # ==== Examples:
13
15
  # Manager.load 'my_commands' -> Loads a FileLibrary object from ~/.boson/commands/my_commands.rb
14
16
  # Manager.load 'method_lister' -> Loads a GemLibrary object which requires the method_lister gem
17
+ # Any options that aren't listed here are passed as library attributes to the libraries (see Library.new)
15
18
  # ==== Options:
16
19
  # [:verbose] Boolean to print each library's loaded status along with more verbose errors. Default is false.
17
- # [:index] Boolean to load in index mode. Default is false.
18
20
  def load(libraries, options={})
19
- libraries = [libraries] unless libraries.is_a?(Array)
20
- libraries.map {|e|
21
+ Array(libraries).map {|e|
21
22
  (@library = load_once(e, options)) ? after_load : false
22
23
  }.all?
23
24
  end
@@ -45,6 +46,10 @@ module Boson
45
46
  end
46
47
 
47
48
  #:stopdoc:
49
+ def failed_libraries
50
+ @failed_libraries ||= []
51
+ end
52
+
48
53
  def add_library(lib)
49
54
  Boson.libraries.delete(Boson.library(lib.name))
50
55
  Boson.libraries << lib
@@ -59,19 +64,18 @@ module Boson
59
64
  rescue AppendFeaturesFalseError
60
65
  rescue LoaderError=>e
61
66
  FileLibrary.reset_file_cache(library.to_s)
62
- print_error_message "Unable to #{load_method} library #{library}. Reason: #{e.message}"
67
+ failed_libraries << library
68
+ $stderr.puts "Unable to #{load_method} library #{library}. Reason: #{e.message}"
63
69
  rescue Exception=>e
64
70
  FileLibrary.reset_file_cache(library.to_s)
65
- print_error_message "Unable to #{load_method} library #{library}. Reason: #{$!}" + "\n" +
66
- e.backtrace.slice(0,3).map {|e| " " + e }.join("\n")
71
+ failed_libraries << library
72
+ message = "Unable to #{load_method} library #{library}. Reason: #{$!}"
73
+ message += "\n" + e.backtrace.slice(0,3).map {|e| " " + e }.join("\n") if @options[:verbose]
74
+ $stderr.puts message
67
75
  ensure
68
76
  Inspector.disable if Inspector.enabled
69
77
  end
70
78
 
71
- def print_error_message(message)
72
- $stderr.puts message if !@options[:index] || (@options[:index] && @options[:verbose])
73
- end
74
-
75
79
  def load_once(source, options={})
76
80
  @options = options
77
81
  rescue_load_action(source, :load) do
@@ -129,10 +133,6 @@ module Boson
129
133
  end
130
134
 
131
135
  def create_commands(lib, commands=lib.commands)
132
- if lib.except
133
- commands -= lib.except
134
- lib.except.each {|e| lib.namespace_object.instance_eval("class<<self;self;end").send :undef_method, e }
135
- end
136
136
  before_create_commands(lib)
137
137
  commands.each {|e| Boson.commands << Command.create(e, lib)}
138
138
  create_command_aliases(lib, commands) if commands.size > 0 && !lib.no_alias_creation
@@ -170,6 +170,11 @@ module Boson
170
170
  end
171
171
 
172
172
  def create_class_aliases(mod, class_commands)
173
+ class_commands.each {|k,v|
174
+ if v.is_a?(Array)
175
+ class_commands.delete(k).each {|e| class_commands[e] = "#{k}.#{e}"}
176
+ end
177
+ }
173
178
  Alias.manager.create_aliases(:any_to_instance_method, mod.to_s=>class_commands.invert)
174
179
  end
175
180
 
@@ -1,6 +1,8 @@
1
1
  module Boson
2
- # Simple Hash with indifferent access. Used by OptionParser.
3
- class IndifferentAccessHash < ::Hash #:nodoc:
2
+ # Simple Hash with indifferent fetching and storing using symbol or string keys. Other actions such as
3
+ # merging should assume symbolic keys. Used by OptionParser.
4
+ class IndifferentAccessHash < ::Hash
5
+ #:stopdoc:
4
6
  def initialize(hash)
5
7
  super()
6
8
  hash.each {|k,v| self[k] = v }
@@ -19,22 +21,49 @@ module Boson
19
21
  end
20
22
 
21
23
  protected
22
- def convert_key(key)
23
- key.kind_of?(String) ? key.to_sym : key
24
- end
24
+ def convert_key(key)
25
+ key.kind_of?(String) ? key.to_sym : key
26
+ end
27
+ #:startdoc:
25
28
  end
26
29
 
27
- # This class provides option parsing for boolean, string, numeric and array
28
- # values given a simple hash of options. Setting option values should be straightforward for
29
- # *nix people. By option type:
30
- # * *:boolean*: These don't have values i.e. '--debug'. To toogle a boolean, prepend with --no- i.e. '--no-debug'.
31
- # Multiple booleans can be joined together i.e. '-d -f -t' == '-dft'.
32
- # * *:string*: Separate name from value with space or '=' i.e. '--color red' or '--color=red'.
33
- # * *:numeric*: Receives values as :string does or by appending number right after name i.e.
34
- # '-N3' == '-N=3'.
35
- # * *:array*: Receives values as :string does. Multiple values are split by ',' i.e.
36
- # '--fields 1,2,3' -> ['1','2','3']. The split character can be configured as explained at
37
- # OptionParser.new .
30
+ # This class concisely defines commandline options that when parsed produce a Hash of option keys and values.
31
+ # Additional points:
32
+ # * Setting option values should follow conventions in *nix environments. See examples below.
33
+ # * By default, there are 5 option types, each which produce different objects for option values.
34
+ # * The default option types can produce objects for one or more of the following Ruby classes:
35
+ # String, Integer, Float, Array, Hash, FalseClass, TrueClass.
36
+ # * Users can define their own option types which create objects for _any_ Ruby class. See Options.
37
+ # * Each option type can have attributes to enable more features (see OptionParser.new).
38
+ # * When options are parsed by OptionParser.parse, an IndifferentAccessHash hash is returned.
39
+ # * Options are also called switches, parameters, flags etc.
40
+ #
41
+ # Default option types:
42
+ # [*:boolean*] This option has no passed value. To toogle a boolean, prepend with '--no-'.
43
+ # Multiple booleans can be joined together.
44
+ # '--debug' -> {:debug=>true}
45
+ # '--no-debug' -> {:debug=>false}
46
+ # '--no-d' -> {:debug=>false}
47
+ # '-d -f -t' same as '-dft'
48
+ # [*:string*] Sets values by separating name from value with space or '='.
49
+ # '--color red' -> {:color=>'red'}
50
+ # '--color=red' -> {:color=>'red'}
51
+ # '--color "gotta love spaces"' -> {:color=>'gotta love spaces'}
52
+ # [*:numeric*] Sets values as :string does or by appending number right after aliased name. Shortened form
53
+ # can be appended to joined booleans.
54
+ # '-n3' -> {:num=>3}
55
+ # '-dn3' -> {:debug=>true, :num=>3}
56
+ # [*:array*] Sets values as :string does. Multiple values are split by a configurable character
57
+ # Default is ',' (see OptionParser.new). Passing '*' refers to all known :values.
58
+ # '--fields 1,2,3' -> {:fields=>['1','2','3']}
59
+ # '--fields *' -> {:fields=>['1','2','3']}
60
+ # [*:hash*] Sets values as :string does. Key-value pairs are split by ':' and pairs are split by
61
+ # a configurable character (default ','). Multiple keys can be joined to one value. Passing '*'
62
+ # as a key refers to all known :keys.
63
+ # '--fields a:b,c:d' -> {:fields=>{'a'=>'b', 'c'=>'d'} }
64
+ # '--fields a,b:d' -> {:fields=>{'a'=>'d', 'b'=>'d'} }
65
+ # '--fields *:d' -> {:fields=>{'a'=>'d', 'b'=>'d', 'c'=>'d'} }
66
+ #
38
67
  # This is a modified version of Yehuda Katz's Thor::Options class which is a modified version
39
68
  # of Daniel Berger's Getopt::Long class (licensed under Ruby's license).
40
69
  class OptionParser
@@ -84,19 +113,29 @@ module Boson
84
113
  # Boson::OptionParser.new :fields=>{:type=>:array, :values=>%w{f1 f2 f3},
85
114
  # :enum=>false}
86
115
  #
87
- # Here are the available option attributes:
88
- #
89
- # * *:type*: This or :default is required. Available types are :string, :boolean, :array, :numeric.
90
- # * *:default*: This or :type is required. This is the default value an option has when not passed.
91
- # * *:values*: An array of values an option can have. Available for :array and :string options. Values here
92
- # can be aliased by typing a unique string it starts with. For example:
93
- #
94
- # For values foo, odd, optional: f refers to foo, o to odd and op to optional.
116
+ # These attributes are available when an option is parsed via current_attributes().
117
+ # Here are the available option attributes for the default option types:
95
118
  #
96
- # * *:enum*: Boolean indicating if an option enforces values in :values. Default is true. Available for
97
- # :array and :string options.
98
- # * *:split*: Only for :array options. A string or regular expression on which an array value splits
99
- # to produce an array of values. Default is ','.
119
+ # [*:type*] This or :default is required. Available types are :string, :boolean, :array, :numeric, :hash.
120
+ # [*:default*] This or :type is required. This is the default value an option has when not passed.
121
+ # [*:bool_default*] This is the value an option has when passed as a boolean. However, by enabling this
122
+ # an option can only have explicit values with '=' i.e. '--index=alias' and no '--index alias'.
123
+ # If this value is a string, it is parsed as any option value would be. Otherwise, the value is
124
+ # passed directly without parsing.
125
+ # [*:required*] Boolean indicating if option is required. Option parses raises error if value not given.
126
+ # Default is false.
127
+ # [*:alias*] Alternative way to define option aliases with an option name or an array of them. Useful in yaml files.
128
+ # Setting to false will prevent creating an automatic alias.
129
+ # [*:values*] An array of values an option can have. Available for :array and :string options. Values here
130
+ # can be aliased by typing a unique string it starts with. For example, for values foo, odd, optional,
131
+ # f refers to foo, o to odd and op to optional.
132
+ # [*:enum*] Boolean indicating if an option enforces values in :values or :keys. Default is true. For
133
+ # :array, :hash and :string options.
134
+ # [*:split*] For :array and :hash options. A string or regular expression on which an array value splits
135
+ # to produce an array of values. Default is ','.
136
+ # [*:keys*] :hash option only. An array of values a hash option's keys can have. Keys can be aliased just like :values.
137
+ # [:default_keys] :hash option only. Default keys to assume when only a value is given. Multiple keys can be joined
138
+ # by the :split character. Defaults to first key of :keys if :keys given.
100
139
  def initialize(opts)
101
140
  @defaults = {}
102
141
  @opt_aliases = {}
@@ -120,19 +159,22 @@ module Boson
120
159
  if type.is_a?(Hash)
121
160
  @option_attributes ||= {}
122
161
  @option_attributes[nice_name] = type
162
+ @opt_aliases[nice_name] = Array(type[:alias]) if type.key?(:alias)
123
163
  @defaults[nice_name] = type[:default] if type[:default]
124
- @option_attributes[nice_name][:enum] = true if type.key?(:values) && !type.key?(:enum)
125
- type = determine_option_type(type[:default]) || type[:type] || :boolean
164
+ @option_attributes[nice_name][:enum] = true if (type.key?(:values) || type.key?(:keys)) &&
165
+ !type.key?(:enum)
166
+ @option_attributes[nice_name][:default_keys] ||= type[:keys][0] if type.key?(:keys)
167
+ type = type[:type] || (!type[:default].nil? ? determine_option_type(type[:default]) : :boolean)
126
168
  end
127
169
 
128
170
  # set defaults
129
171
  case type
130
- when TrueClass then @defaults[nice_name] = true
131
- when FalseClass then @defaults[nice_name] = false
132
- when String, Numeric, Array then @defaults[nice_name] = type
172
+ when TrueClass then @defaults[nice_name] = true
173
+ when FalseClass then @defaults[nice_name] = false
174
+ else
175
+ @defaults[nice_name] = type unless type.is_a?(Symbol)
133
176
  end
134
-
135
- mem[name] = determine_option_type(type) || type
177
+ mem[name] = !type.nil? ? determine_option_type(type) : type
136
178
  mem
137
179
  end
138
180
 
@@ -146,13 +188,13 @@ module Boson
146
188
  opt_alias = h.key?("-"+opt_alias) ? "-"+opt_alias.capitalize : "-"+opt_alias
147
189
  h[opt_alias] ||= name unless @opt_types.key?(opt_alias)
148
190
  else
149
- aliases.each { |e| h[e] = name unless @opt_types.key?(e) }
191
+ aliases.each {|e| h[e] = name if !@opt_types.key?(e) && e != false }
150
192
  end
151
193
  h
152
194
  }
153
195
  end
154
196
 
155
- # Parses an array of arguments for defined options to return a hash. Once the parser
197
+ # Parses an array of arguments for defined options to return an IndifferentAccessHash. Once the parser
156
198
  # recognizes a valid option, it continues to parse until an non option argument is detected.
157
199
  # Flags that can be passed to the parser:
158
200
  # * :opts_before_args: When true options must come before arguments. Default is false.
@@ -168,7 +210,7 @@ module Boson
168
210
  end
169
211
 
170
212
  while current_is_option?
171
- case shift
213
+ case @original_current_option = shift
172
214
  when SHORT_SQ_RE
173
215
  unshift $1.split('').map { |f| "-#{f}" }
174
216
  next
@@ -178,12 +220,12 @@ module Boson
178
220
  when LONG_RE, SHORT_RE
179
221
  option = $1
180
222
  end
181
-
223
+
182
224
  dashed_option = normalize_option(option)
183
225
  @current_option = undasherize(dashed_option)
184
226
  type = option_type(dashed_option)
185
227
  validate_option_value(type)
186
- value = get_option_value(type, dashed_option)
228
+ value = create_option_value(type)
187
229
  # set on different line since current_option may change
188
230
  hash[@current_option.to_sym] = value
189
231
  end
@@ -194,24 +236,18 @@ module Boson
194
236
  hash
195
237
  end
196
238
 
197
- # One-line option usage
239
+ # Helper method to generate usage. Takes a dashed option and a string value indicating
240
+ # an option value's format.
241
+ def default_usage(opt, val)
242
+ opt + "=" + (@defaults[undasherize(opt)] || val).to_s
243
+ end
244
+
245
+ # Generates one-line usage of all options.
198
246
  def formatted_usage
199
247
  return "" if @opt_types.empty?
200
248
  @opt_types.map do |opt, type|
201
- case type
202
- when :boolean
203
- "[#{opt}]"
204
- when :required
205
- opt + "=" + opt.gsub(/\-/, "").upcase
206
- else
207
- sample = @defaults[undasherize(opt)]
208
- sample ||= case type
209
- when :string then undasherize(opt).gsub(/\-/, "_").upcase
210
- when :numeric then "N"
211
- when :array then "A,B,C"
212
- end
213
- "[" + opt + "=" + sample.to_s + "]"
214
- end
249
+ val = respond_to?("usage_for_#{type}", true) ? send("usage_for_#{type}", opt) : "#{opt}=:#{type}"
250
+ "[" + val + "]"
215
251
  end.join(" ")
216
252
  end
217
253
 
@@ -221,51 +257,61 @@ module Boson
221
257
  def print_usage_table(render_options={})
222
258
  aliases = @opt_aliases.invert
223
259
  additional = [:desc, :values].select {|e| (@option_attributes || {}).values.any? {|f| f.key?(e) } }
260
+ additional_opts = {:desc=>[:desc], :values=>[:values, :keys]}
224
261
  opts = @opt_types.keys.sort.inject([]) {|t,e|
225
262
  h = {:name=>e, :aliases=>aliases[e], :type=>@opt_types[e]}
226
- additional.each {|f| h[f] = (@option_attributes[undasherize(e)] || {})[f] }
263
+ additional.each {|f|
264
+ h[f] = additional_opts[f].map {|g| (@option_attributes[undasherize(e)] || {})[g]}.flatten.compact
265
+ }
227
266
  t << h
228
267
  }
229
- render_options = {:headers=>{:name=>"Option", :aliases=>"Alias", :desc=>'Description', :values=>'Values'},
268
+ render_options = {:headers=>{:name=>"Option", :aliases=>"Alias", :desc=>'Description', :values=>'Values/Keys', :type=>'Type'},
230
269
  :fields=>[:name, :aliases, :type] + additional, :description=>false, :filters=>{:values=>lambda {|e| (e || []).join(',')} }
231
270
  }.merge(render_options)
232
271
  View.render opts, render_options
233
272
  end
234
273
 
274
+ # Hash of option attributes for the currently parsed option. _Any_ hash keys
275
+ # passed to an option are available here. This means that an option type can have any
276
+ # user-defined attributes available during option parsing and object creation.
277
+ def current_attributes
278
+ @option_attributes && @option_attributes[@current_option] || {}
279
+ end
280
+
281
+ # Removes dashes from a dashed option i.e. '--date' -> 'date' and '-d' -> 'd'.
282
+ def undasherize(str)
283
+ str.sub(/^-{1,2}/, '')
284
+ end
285
+
286
+ # Adds dashes to an option name i.e. 'date' -> '--date' and 'd' -> '-d'.
287
+ def dasherize(str)
288
+ (str.length > 1 ? "--" : "-") + str
289
+ end
290
+
235
291
  private
236
292
  def determine_option_type(value)
293
+ return value if value.is_a?(Symbol)
237
294
  case value
238
- when TrueClass, FalseClass then :boolean
239
- when String then :string
240
- when Numeric then :numeric
241
- when Array then :array
242
- else nil
295
+ when TrueClass, FalseClass then :boolean
296
+ when Numeric then :numeric
297
+ else
298
+ Util.underscore(value.class.to_s).to_sym
243
299
  end
244
300
  end
245
301
 
246
- def get_option_value(type, opt)
247
- case type
248
- when :required
249
- shift
250
- when :string
251
- value = shift
252
- if (values = @option_attributes[@current_option][:values].sort_by {|e| e.to_s} rescue nil)
253
- (val = auto_alias_value(values, value)) && value = val
254
- end
255
- value
256
- when :boolean
257
- (!@opt_types.key?(opt) && @current_option =~ /^no-(\w+)$/) ? (@current_option.replace($1) && false) : true
258
- when :numeric
259
- peek.index('.') ? shift.to_f : shift.to_i
260
- when :array
261
- splitter = (@option_attributes[@current_option][:split] rescue nil) || ','
262
- array = shift.split(splitter)
263
- if values = @option_attributes[@current_option][:values].sort_by {|e| e.to_s } rescue nil
264
- array.each_with_index {|e,i|
265
- (value = auto_alias_value(values, e)) && array[i] = value
266
- }
267
- end
268
- array
302
+ def value_shift
303
+ return shift if !current_attributes.key?(:bool_default)
304
+ return shift if @original_current_option =~ EQ_RE
305
+ current_attributes[:bool_default]
306
+ end
307
+
308
+ def create_option_value(type)
309
+ if current_attributes.key?(:bool_default) && (@original_current_option !~ EQ_RE) &&
310
+ !(bool_default = current_attributes[:bool_default]).is_a?(String)
311
+ bool_default
312
+ else
313
+ respond_to?("create_#{type}", true) ? send("create_#{type}", type != :boolean ? value_shift : nil) :
314
+ raise(Error, "Option '#{@current_option}' is invalid option type #{type.inspect}.")
269
315
  end
270
316
  end
271
317
 
@@ -275,18 +321,11 @@ module Boson
275
321
  end
276
322
 
277
323
  def validate_option_value(type)
324
+ return if current_attributes.key?(:bool_default)
278
325
  if type != :boolean && peek.nil?
279
326
  raise Error, "no value provided for option '#{@current_option}'"
280
327
  end
281
-
282
- case type
283
- when :required, :string
284
- raise Error, "cannot pass '#{peek}' as an argument to option '#{@current_option}'" if valid?(peek)
285
- when :numeric
286
- unless peek =~ NUMERIC and $& == peek
287
- raise Error, "expected numeric value for option '#{@current_option}'; got #{peek.inspect}"
288
- end
289
- end
328
+ send("validate_#{type}", peek) if respond_to?("validate_#{type}", true)
290
329
  end
291
330
 
292
331
  def delete_invalid_opts
@@ -299,14 +338,6 @@ module Boson
299
338
  end
300
339
  end
301
340
 
302
- def undasherize(str)
303
- str.sub(/^-{1,2}/, '')
304
- end
305
-
306
- def dasherize(str)
307
- (str.length > 1 ? "--" : "-") + str
308
- end
309
-
310
341
  def peek
311
342
  @args.first
312
343
  end
@@ -325,7 +356,8 @@ module Boson
325
356
 
326
357
  def valid?(arg)
327
358
  if arg.to_s =~ /^--no-(\w+)$/
328
- @opt_types.key?(arg) or (@opt_types["--#{$1}"] == :boolean)
359
+ @opt_types.key?(arg) or (@opt_types["--#{$1}"] == :boolean) or
360
+ (@opt_types[original_no_opt($1)] == :boolean)
329
361
  else
330
362
  @opt_types.key?(arg) or @opt_aliases.key?(arg)
331
363
  end
@@ -346,16 +378,21 @@ module Boson
346
378
 
347
379
  def option_type(opt)
348
380
  if opt =~ /^--no-(\w+)$/
349
- @opt_types[opt] || @opt_types["--#{$1}"]
381
+ @opt_types[opt] || @opt_types["--#{$1}"] || @opt_types[original_no_opt($1)]
350
382
  else
351
383
  @opt_types[opt]
352
384
  end
353
385
  end
354
-
386
+
387
+ def original_no_opt(opt)
388
+ @opt_aliases[dasherize(opt)]
389
+ end
390
+
355
391
  def check_required!(hash)
356
392
  for name, type in @opt_types
357
- if type == :required and !hash[undasherize(name)]
358
- raise Error, "no value provided for required option '#{undasherize(name)}'"
393
+ @current_option = undasherize(name)
394
+ if current_attributes[:required] && !hash.key?(@current_option.to_sym)
395
+ raise Error, "no value provided for required option '#{@current_option}'"
359
396
  end
360
397
  end
361
398
  end