boson 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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