boson 0.2.0 → 0.2.1

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.
data/lib/boson/pipe.rb ADDED
@@ -0,0 +1,157 @@
1
+ module Boson
2
+ # This module passes a command's return value through methods/commands specified as pipe options. Pipe options
3
+ # are processed in this order:
4
+ # * A :query option searches an array of objects or hashes using Pipe.search_object.
5
+ # * A :sort option sorts an array of objects or hashes using Pipe.sort_object.
6
+ # * All user-defined pipe options (:pipe_options key in Repo.config) are processed in random order.
7
+ #
8
+ # Some points:
9
+ # * User-defined pipes call a command (the option's name by default). It's the user's responsibility to have this
10
+ # command loaded when used. The easiest way to do this is by adding the pipe command's library to :defaults in main config.
11
+ # * By default, pipe commands do not modify the value their given. This means you can activate multiple pipes using
12
+ # a method's original return value.
13
+ # * If you want a pipe command to modify the value its given, set its pipe option's :filter attribute to true.
14
+ # * A pipe command expects a command's return value as its first argument. If the pipe option takes an argument, it's passed
15
+ # on as a second argument.
16
+ # * When piping occurs in relation to rendering depends on the Hirb view. With the default Hirb view, piping occurs
17
+ # occurs in the middle of the rendering, after Hirb has converted the return value into an array of hashes.
18
+ # If using a custom Hirb view, piping occurs before rendering.
19
+ #
20
+ # === Default Pipes: Search and Sort
21
+ # The default pipe options, :query, :sort and :reverse_sort, are quite useful for searching and sorting arrays:
22
+ # Some examples using default commands:
23
+ # # Searches commands in the full_name field for 'lib' and sorts results by that field.
24
+ # bash> boson commands -q=f:lib -s=f # or commands --query=full_name:lib --sort=full_name
25
+ #
26
+ # # Multiple fields can be searched if separated by a ','. This searches the full_name and description fields.
27
+ # bash> boson commands -q=f,d:web # or commands --query=full_name,description:web
28
+ #
29
+ # # All fields can be queried using a '*'.
30
+ # # Searches all library fields and then reverse sorts on name field
31
+ # bash> boson libraries -q=*:core -s=n -R # or libraries --query=*:core --sort=name --reverse_sort
32
+ #
33
+ # # Multiple searches can be joined together by ','
34
+ # # Searches for libraries that have the name matching core or a library_type matching gem
35
+ # bash> boson libraries -q=n:core,l:gem # or libraries --query=name:core,library_type:gem
36
+ #
37
+ # In these examples, we queried commands and examples with an explicit --query. However, -q or --query isn't necessary
38
+ # for these commands because they already default to it when not present. This behavior comes from the default_option
39
+ # attribute a command can have.
40
+ #
41
+ # === User-defined Pipes
42
+ # Let's say you want to have two commands, browser and copy, you want to make available as pipe options:
43
+ # # Opens url in browser. This command already ships with Boson.
44
+ # def browser(url)
45
+ # system('open', url)
46
+ # end
47
+ #
48
+ # # Copy to clipboard
49
+ # def copy(str)
50
+ # IO.popen('pbcopy', 'w+') {|clipboard| clipboard.write(str)}
51
+ # end
52
+ #
53
+ # To configure them, drop the following config in ~/.boson/config/boson.yml:
54
+ # :pipe_options:
55
+ # :browser:
56
+ # :type: :boolean
57
+ # :desc: Open in browser
58
+ # :copy:
59
+ # :type: :boolean
60
+ # :desc: Copy to clipboard
61
+ #
62
+ # Now for any command that returns a url string, these pipe options can be turned on to execute the url.
63
+ #
64
+ # Some examples of these options using commands from {my libraries}[http://github.com/cldwalker/irbfiles]:
65
+ # # Creates a gist and then opens url in browser and copies it.
66
+ # bash> cat some_file | boson gist -bC # or cat some_file | boson gist --browser --copy
67
+ #
68
+ # # Generates rdoc in current directory and then opens it in browser
69
+ # irb>> rdoc '-b' # or rdoc '--browser'
70
+ module Pipe
71
+ extend self
72
+
73
+ # Main method which processes all pipe commands, both default and user-defined ones.
74
+ def process(object, options)
75
+ if object.is_a?(Array)
76
+ object = search_object(object, options[:query]) if options[:query]
77
+ object = sort_object(object, options[:sort], options[:reverse_sort]) if options[:sort]
78
+ end
79
+ process_user_pipes(object, options)
80
+ end
81
+
82
+ # Searches an array of objects or hashes using the :query option.
83
+ # This option is a hash of fields mapped to their search terms. Searches are OR-ed.
84
+ def search_object(object, query_hash)
85
+ if object[0].is_a?(Hash)
86
+ TableCallbacks.search_callback(object, :query=>query_hash)
87
+ else
88
+ query_hash.map {|field,query| object.select {|e| e.send(field).to_s =~ /#{query}/i } }.flatten.uniq
89
+ end
90
+ rescue NoMethodError
91
+ $stderr.puts "Query failed with nonexistant method '#{$!.message[/`(.*)'/,1]}'"
92
+ end
93
+
94
+ # Sorts an array of objects or hashes using a sort field. Sort is reversed with reverse_sort set to true.
95
+ def sort_object(object, sort, reverse_sort=false)
96
+ if object[0].is_a?(Hash)
97
+ TableCallbacks.sort_callback(object, :sort=>sort, :reverse_sort=>reverse_sort)
98
+ else
99
+ sort_lambda = object.all? {|e| e.send(sort).respond_to?(:<=>) } ? lambda {|e| e.send(sort) || ''} :
100
+ lambda {|e| e.send(sort).to_s }
101
+ object = object.sort_by &sort_lambda
102
+ object = object.reverse if reverse_sort
103
+ object
104
+ end
105
+ rescue NoMethodError, ArgumentError
106
+ $stderr.puts "Sort failed with nonexistant method '#{sort}'"
107
+ end
108
+
109
+ #:stopdoc:
110
+ def pipe_options
111
+ @pipe_options ||= Boson.repo.config[:pipe_options] || {}
112
+ end
113
+
114
+ def process_user_pipes(result, options)
115
+ (options.keys & pipe_options.keys).each {|e|
116
+ command = pipe_options[e][:pipe] ||= e
117
+ pipe_result = pipe_options[e][:type] == :boolean ? Boson.invoke(command, result) :
118
+ Boson.invoke(command, result, options[e])
119
+ result = pipe_result if pipe_options[e][:filter]
120
+ }
121
+ result
122
+ end
123
+ #:startdoc:
124
+
125
+ # Callbacks used by Hirb::Helpers::Table to search,sort and run custom pipe commands on arrays of hashes.
126
+ module TableCallbacks
127
+ extend self
128
+ # Case-insensitive searches an array of hashes using the option :query. Numerical string keys
129
+ # in :query are converted to actual numbers to interface with Hirb. See Pipe.search_object for more
130
+ # about :query.
131
+ def search_callback(obj, options)
132
+ !options[:query] ? obj : begin
133
+ options[:query].map {|field,query|
134
+ field = field.to_i if field.to_s[/^\d+$/]
135
+ obj.select {|e| e[field].to_s =~ /#{query}/i }
136
+ }.flatten.uniq
137
+ end
138
+ end
139
+
140
+ # Sorts an array of hashes using :sort option and reverses the sort with :reverse_sort option.
141
+ def sort_callback(obj, options)
142
+ return obj unless options[:sort]
143
+ sort = options[:sort].to_s[/^\d+$/] ? options[:sort].to_i : options[:sort]
144
+ sort_lambda = (obj.all? {|e| e[sort].respond_to?(:<=>) } ? lambda {|e| e[sort] } : lambda {|e| e[sort].to_s })
145
+ obj = obj.sort_by &sort_lambda
146
+ obj = obj.reverse if options[:reverse_sort]
147
+ obj
148
+ end
149
+
150
+ # Processes user-defined pipes in random order.
151
+ def z_user_pipes_callback(obj, options)
152
+ Pipe.process_user_pipes(obj, options)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ Hirb::Helpers::Table.send :include, Boson::Pipe::TableCallbacks
data/lib/boson/repo.rb CHANGED
@@ -35,7 +35,7 @@ module Boson
35
35
  # A hash read from the YAML config file at config/boson.yml.
36
36
  # {See here}[http://github.com/cldwalker/irbfiles/blob/master/boson/config/boson.yml] for an example config file.
37
37
  # Top level config keys, library attributes and config attributes need to be symbols.
38
- # ==== Valid config keys:
38
+ # ==== Config keys for all repositories:
39
39
  # [:libraries] Hash of libraries mapping their name to attribute hashes. See Library.new for configurable attributes.
40
40
  # Example:
41
41
  # :libraries=>{'completion'=>{:namespace=>true}}
@@ -53,12 +53,23 @@ module Boson
53
53
  # [:add_load_path] Boolean specifying whether to add a load path pointing to the lib subdirectory/. This is useful in sharing
54
54
  # classes between libraries without resorting to packaging them as gems. Defaults to false if the lib
55
55
  # subdirectory doesn't exist in the boson directory.
56
+ #
57
+ # ==== Config keys specific to the main repo config ~/.boson/config/boson.yml
58
+ # [:pipe_options] Hash of options available to all option commands for piping (see Pipe). A pipe option has the
59
+ # {normal option attributes}[link:classes/Boson/OptionParser.html#M000081] and these:
60
+ # * :pipe: Specifies the command to call when piping. Defaults to the pipe's option name.
61
+ # * :filter: Boolean which indicates that the pipe command will modify its input with what it returns.
62
+ # Default is false.
63
+ # [:render_options] Hash of render options available to all option commands to be passed to a Hirb view (see View). Since
64
+ # this merges with default render options, it's possible to override default render options.
56
65
  # [:error_method_conflicts] Boolean specifying library loading behavior when its methods conflicts with existing methods in
57
66
  # the global namespace. When set to false, Boson automatically puts the library in its own namespace.
58
67
  # When set to true, the library fails to load explicitly. Default is false.
59
68
  # [:console] Console to load when using --console from commandline. Default is irb.
60
69
  # [:auto_namespace] Boolean which automatically namespaces all user-defined libraries. Be aware this can break libraries which
61
70
  # depend on commands from other libraries. Default is false.
71
+ # [:ignore_directories] Array of directories to ignore when detecting local repositories for Boson.local_repo.
72
+ # [:no_auto_render] When set, turns off commandline auto-rendering of a command's output. Default is false.
62
73
  def config(reload=false)
63
74
  if reload || @config.nil?
64
75
  begin
@@ -72,5 +83,13 @@ module Boson
72
83
  end
73
84
  @config
74
85
  end
86
+
87
+ def detected_libraries #:nodoc:
88
+ Dir[File.join(commands_dir, '**/*.rb')].map {|e| e.gsub(/^#{commands_dir}\/|\.rb$/, '') }
89
+ end
90
+
91
+ def all_libraries #:nodoc:
92
+ (detected_libraries + config[:libraries].keys).uniq
93
+ end
75
94
  end
76
95
  end
@@ -0,0 +1,123 @@
1
+ require 'digest/md5'
2
+ module Boson
3
+ # This class provides an index for commands and libraries of a given a Repo.
4
+ # When this index updates, it detects library files whose md5 hash have changed and reindexes them.
5
+ # The index is stored with Marshal at config/index.marshal (relative to a Repo's root directory).
6
+ # Since the index is marshaled, putting lambdas/procs in it will break it.If an index gets corrupted,
7
+ # simply delete it and next time Boson needs it, the index will be recreated.
8
+
9
+ class RepoIndex
10
+ attr_reader :libraries, :commands, :repo
11
+ def initialize(repo)
12
+ @repo = repo
13
+ end
14
+
15
+ # Updates the index.
16
+ def update(options={})
17
+ libraries_to_update = !exists? ? repo.all_libraries : options[:libraries] || changed_libraries
18
+ read_and_transfer(libraries_to_update)
19
+ if options[:verbose]
20
+ puts !exists? ? "Generating index for all #{libraries_to_update.size} libraries. Patience ... is a bitch" :
21
+ (libraries_to_update.empty? ? "No libraries indexed" :
22
+ "Indexing the following libraries: #{libraries_to_update.join(', ')}")
23
+ end
24
+ Manager.failed_libraries = []
25
+ unless libraries_to_update.empty?
26
+ Manager.load(libraries_to_update, options.merge(:index=>true))
27
+ unless Manager.failed_libraries.empty?
28
+ $stderr.puts("Error: These libraries failed to load while indexing: #{Manager.failed_libraries.join(', ')}")
29
+ end
30
+ end
31
+ write(Manager.failed_libraries)
32
+ end
33
+
34
+ # Reads and initializes index.
35
+ def read
36
+ return if @read
37
+ @libraries, @commands, @lib_hashes = exists? ? Marshal.load(File.read(marshal_file)) : [[], [], {}]
38
+ delete_stale_libraries_and_commands
39
+ set_command_namespaces
40
+ @read = true
41
+ end
42
+
43
+ # Writes/saves current index to config/index.marshal.
44
+ def write(failed_libraries=[])
45
+ latest = latest_hashes
46
+ failed_libraries.each {|e| latest.delete(e) }
47
+ save_marshal_index Marshal.dump([Boson.libraries, Boson.commands, latest])
48
+ end
49
+
50
+ #:stopdoc:
51
+ def read_and_transfer(ignored_libraries=[])
52
+ read
53
+ existing_libraries = (Boson.libraries.map {|e| e.name} + ignored_libraries).uniq
54
+ libraries_to_add = @libraries.select {|e| !existing_libraries.include?(e.name)}
55
+ Boson.libraries += libraries_to_add
56
+ # depends on saved commands being correctly associated with saved libraries
57
+ Boson.commands += libraries_to_add.map {|e| e.command_objects(e.commands, @commands) }.flatten
58
+ end
59
+
60
+ def exists?
61
+ File.exists? marshal_file
62
+ end
63
+
64
+ def save_marshal_index(marshal_string)
65
+ File.open(marshal_file, 'w') {|f| f.write marshal_string }
66
+ end
67
+
68
+ def delete_stale_libraries_and_commands
69
+ cached_libraries = @lib_hashes.keys
70
+ libs_to_delete = @libraries.select {|e| !cached_libraries.include?(e.name) && e.is_a?(FileLibrary) }
71
+ names_to_delete = libs_to_delete.map {|e| e.name }
72
+ libs_to_delete.each {|e| @libraries.delete(e) }
73
+ @commands.delete_if {|e| names_to_delete.include? e.lib }
74
+ end
75
+
76
+ # set namespaces for commands
77
+ def set_command_namespaces
78
+ lib_commands = @commands.inject({}) {|t,e| (t[e.lib] ||= []) << e; t }
79
+ namespace_libs = @libraries.select {|e| e.namespace(e.indexed_namespace) }
80
+ namespace_libs.each {|lib|
81
+ (lib_commands[lib.name] || []).each {|e| e.namespace = lib.namespace }
82
+ }
83
+ end
84
+
85
+ def namespaces
86
+ nsps = @libraries.map {|e| e.namespace }.compact
87
+ nsps.delete(false)
88
+ nsps
89
+ end
90
+
91
+ def all_main_methods
92
+ @commands.reject {|e| e.namespace }.map {|e| [e.name, e.alias]}.flatten.compact + namespaces
93
+ end
94
+
95
+ def marshal_file
96
+ File.join(repo.config_dir, 'index.marshal')
97
+ end
98
+
99
+ def find_library(command)
100
+ read
101
+ namespace_command = command.split('.')[0]
102
+ if (lib = @libraries.find {|e| e.namespace == namespace_command })
103
+ lib.name
104
+ elsif (cmd = Command.find(command, @commands))
105
+ cmd.lib
106
+ end
107
+ end
108
+
109
+ def changed_libraries
110
+ read
111
+ latest_hashes.select {|lib, hash| @lib_hashes[lib] != hash}.map {|e| e[0]}
112
+ end
113
+
114
+ def latest_hashes
115
+ repo.all_libraries.inject({}) {|h, e|
116
+ lib_file = FileLibrary.library_file(e, repo.dir)
117
+ h[e] = Digest::MD5.hexdigest(File.read(lib_file)) if File.exists?(lib_file)
118
+ h
119
+ }
120
+ end
121
+ #:startdoc:
122
+ end
123
+ end
data/lib/boson/runner.rb CHANGED
@@ -11,18 +11,17 @@ module Boson
11
11
 
12
12
  # Libraries that come with Boson
13
13
  def default_libraries
14
- [Boson::Commands::Core, Boson::Commands::WebCore] + (Boson.repo.config[:defaults] || [])
14
+ [Boson::Commands::Core, Boson::Commands::WebCore] + Boson.repos.map {|e| e.config[:defaults] || [] }.flatten
15
15
  end
16
16
 
17
17
  # Libraries detected in repositories
18
18
  def detected_libraries
19
- Boson.repos.map {|repo| Dir[File.join(repo.commands_dir, '**/*.rb')].
20
- map {|e| e.gsub(/^#{repo.commands_dir}\/|\.rb$/, '')} }.flatten
19
+ Boson.repos.map {|e| e.detected_libraries }.flatten.uniq
21
20
  end
22
21
 
23
22
  # Libraries specified in config files and detected_libraries
24
23
  def all_libraries
25
- (detected_libraries + Boson.repos.map {|e| e.config[:libraries].keys}.flatten).uniq
24
+ Boson.repos.map {|e| e.all_libraries }.flatten.uniq
26
25
  end
27
26
 
28
27
  #:stopdoc:
@@ -2,7 +2,7 @@ module Boson
2
2
  # This class handles the boson executable (boson command execution from the commandline). Any changes
3
3
  # to your commands are immediately available from the commandline except for changes to the main config file.
4
4
  # For those changes to take effect you need to explicitly load and index the libraries with --index.
5
- # See Index to understand how Boson can immediately detect the latest commands.
5
+ # See RepoIndex to understand how Boson can immediately detect the latest commands.
6
6
  #
7
7
  # Usage for the boson shell command looks like this:
8
8
  # boson [GLOBAL OPTIONS] [COMMAND] [ARGS] [COMMAND OPTIONS]
@@ -20,9 +20,22 @@ module Boson
20
20
  # [:index] Updates index for given libraries allowing you to use them. This is useful if Boson's autodetection of
21
21
  # changed libraries isn't picking up your changes. Since this option has a :bool_default attribute, arguments
22
22
  # passed to this option need to be passed with '=' i.e. '--index=my_lib'.
23
- # [:render] Pretty formats the results of commands without options. Handy for commands that return arrays.
23
+ # [:render] Toggles the auto-rendering done for commands that don't have views. Doesn't affect commands that already have views.
24
+ # Default is false. Also see Auto Rendering section below.
24
25
  # [:pager_toggle] Toggles Hirb's pager in case you'd like to pipe to another command.
26
+ #
27
+ # ==== Auto Rendering
28
+ # Commands that don't have views (defined via render_options) have their return value auto-rendered as a view as follows:
29
+ # * nil,false and true aren't rendered
30
+ # * arrays are rendered with Hirb's tables
31
+ # * non-arrays are printed with inspect()
32
+ # * Any of these cases can be toggled to render/not render with the global option :render
33
+ # To turn off auto-rendering by default, add a :no_auto_render: true entry to the main config.
25
34
  class BinRunner < Runner
35
+ def self.all_libraries #:nodoc:
36
+ @all_libraries ||= ((libs = super) + libs.map {|e| File.basename(e) }).uniq
37
+ end
38
+
26
39
  GLOBAL_OPTIONS = {
27
40
  :verbose=>{:type=>:boolean, :desc=>"Verbose description of loading libraries or help"},
28
41
  :index=>{:type=>:array, :desc=>"Libraries to index. Libraries must be passed with '='.",
@@ -30,7 +43,8 @@ module Boson
30
43
  :execute=>{:type=>:string, :desc=>"Executes given arguments as a one line script"},
31
44
  :console=>{:type=>:boolean, :desc=>"Drops into irb with default and explicit libraries loaded"},
32
45
  :help=>{:type=>:boolean, :desc=>"Displays this help message or a command's help if given a command"},
33
- :load=>{:type=>:array, :values=>all_libraries, :enum=>false, :desc=>"A comma delimited array of libraries to load"},
46
+ :load=>{:type=>:array, :values=>all_libraries,
47
+ :enum=>false, :desc=>"A comma delimited array of libraries to load"},
34
48
  :render=>{:type=>:boolean, :desc=>"Renders a Hirb view from result of command without options"},
35
49
  :pager_toggle=>{:type=>:boolean, :desc=>"Toggles Hirb's pager"}
36
50
  } #:nodoc:
@@ -87,7 +101,7 @@ module Boson
87
101
  end
88
102
 
89
103
  def default_libraries
90
- super + (Boson.repo.config[:bin_defaults] || [])
104
+ super + Boson.repos.map {|e| e.config[:bin_defaults] || [] }.flatten + Dir.glob('Bosonfile')
91
105
  end
92
106
 
93
107
  def execute_command
@@ -107,10 +121,11 @@ module Boson
107
121
  end
108
122
 
109
123
  def render_output(output)
110
- if Scientist.global_options && !Scientist.rendered && !View.silent_object?(output)
111
- puts output.inspect
112
- elsif !Scientist.global_options && @options[:render]
113
- View.render(output, :silence_booleans=>true)
124
+ if (!Scientist.rendered && !View.silent_object?(output)) ^ @options[:render] ^
125
+ Boson.repo.config[:no_auto_render]
126
+ opts = output.is_a?(String) ? {:method=>'puts'} :
127
+ {:inspect=>!output.is_a?(Array) || (Scientist.global_options || {})[:render] }
128
+ View.render output, opts
114
129
  end
115
130
  end
116
131
 
@@ -5,8 +5,7 @@ module Boson
5
5
  class ConsoleRunner < Runner
6
6
  class <<self
7
7
  # Starts Boson by loading configured libraries. If no default libraries are specified in the config,
8
- # it will load up all detected libraries.
9
- # ==== Options
8
+ # it will load up all detected libraries. Options:
10
9
  # [:libraries] Array of libraries to load.
11
10
  # [:verbose] Boolean to be verbose about libraries loading. Default is true.
12
11
  # [:no_defaults] Boolean which turns off loading any default libraries. Default is false.
@@ -1,109 +1,54 @@
1
- require 'shellwords'
2
1
  module Boson
3
- # Scientist redefines _any_ object's methods to act like shell commands while still receiving ruby arguments normally.
4
- # It also let's your method have an optional view generated from a method's return value.
5
- # Boson::Scientist.create_option_command redefines an object's method with a Boson::Command while
6
- # Boson::Scientist.commandify redefines with just a hash. For an object's method to be redefined correctly,
2
+ # Scientist wraps around and redefines an object's method to give it the following features:
3
+ # * Methods can take shell command input with options or receive its normal arguments. See the Commandification
4
+ # section.
5
+ # * Methods have a slew of global options available. See OptionCommand for an explanation of basic global options.
6
+ # * Before a method returns its value, it pipes its return value through pipe commands if pipe options are specified. See Pipe.
7
+ # * Methods can have any number of optional views associated with them via global render options (see View). Views can be toggled
8
+ # on/off with the global option --render (see OptionCommand).
9
+ #
10
+ # The main methods Scientist provides are redefine_command() for redefining an object's method with a Command object
11
+ # and commandify() for redefining with a hash of method attributes. Note that for an object's method to be redefined correctly,
7
12
  # its last argument _must_ expect a hash.
8
13
  #
9
- # === Examples
14
+ # === Commandification
10
15
  # Take for example this basic method/command with an options definition:
11
16
  # options :level=>:numeric, :verbose=>:boolean
12
- # def foo(arg='', options={})
13
- # [arg, options]
17
+ # def foo(*args)
18
+ # args
14
19
  # end
15
20
  #
16
- # When Scientist wraps around foo(), argument defaults are respected:
17
- # foo '', :verbose=>true # normal call
18
- # foo '-v' # commandline call
19
- #
20
- # Both calls return: ['', {:verbose=>true}]
21
- #
22
- # Non-string arguments can be passed in:
23
- # foo Object, :level=>1
24
- # foo Object, 'l1'
21
+ # When Scientist wraps around foo(), it can take arguments normally or as a shell command:
22
+ # foo 'one', 'two', :verbose=>true # normal call
23
+ # foo 'one two -v' # commandline call
25
24
  #
26
- # Both calls return: [Object, {:level=>1}]
25
+ # Both calls return: ['one', 'two', {:verbose=>true}]
27
26
  #
28
- # === Global Options
29
- # Any command with options comes with default global options. For example '-hv' on such a command
30
- # prints a help summarizing a command's options as well as the global options.
31
- # When using global options along with command options, global options _must_ precede command options.
32
- # Take for example using the global --pretend option with the method above:
33
- # irb>> foo '-p -l=1'
34
- # Arguments: ["", {:level=>1}]
35
- # Global options: {:pretend=>true}
27
+ # Non-string arguments can be passed as well:
28
+ # foo Object, 'two', :level=>1
29
+ # foo Object, 'two -l1'
36
30
  #
37
- # If a global option conflicts with a command's option, the command's option takes precedence. You can get around
38
- # this by passing a --global option which takes a string of options without their dashes. For example:
39
- # foo '-p --fields=f1,f2 -l=1'
40
- # # is the same as
41
- # foo ' -g "p fields=f1,f2" -l=1 '
42
- #
43
- # === Rendering Views With Global Options
44
- # Perhaps the most important global option is --render. This option toggles the rendering of your command's output
45
- # with Hirb[http://github.com/cldwalker/hirb]. Since Hirb can be customized to generate any view, this option allows
46
- # you toggle a predefined view for a command without embedding view code in your command!
47
- #
48
- # Here's a simple example, toggling Hirb's table view:
49
- # # Defined in a library file:
50
- # #@options {}
51
- # def list(options={})
52
- # [1,2,3]
53
- # end
54
- #
55
- # Using it in irb:
56
- # >> list
57
- # => [1,2,3]
58
- # >> list '-r'
59
- # +-------+
60
- # | value |
61
- # +-------+
62
- # | 1 |
63
- # | 2 |
64
- # | 3 |
65
- # +-------+
66
- # 3 rows in set
67
- # => true
68
- #
69
- # To default to rendering a view for a command, add a render_options {method attribute}[link:classes/Boson/MethodInspector.html]
70
- # above list() along with any options you want to pass to your Hirb helper class. In this case, using '-r' gives you the
71
- # command's returned object instead of a formatted view!
31
+ # Both calls return: [Object, 'two', {:level=>1}]
72
32
  module Scientist
73
33
  extend self
74
34
  # Handles all Scientist errors.
75
35
  class Error < StandardError; end
76
36
  class EscapeGlobalOption < StandardError; end
77
37
 
78
- attr_reader :global_options, :rendered, :option_parsers, :command_options
38
+ attr_accessor :global_options, :rendered
79
39
  @no_option_commands ||= []
80
- GLOBAL_OPTIONS = {
81
- :help=>{:type=>:boolean, :desc=>"Display a command's help"},
82
- :render=>{:type=>:boolean, :desc=>"Toggle a command's default rendering behavior"},
83
- :verbose=>{:type=>:boolean, :desc=>"Increase verbosity for help, errors, etc."},
84
- :global=>{:type=>:string, :desc=>"Pass a string of global options without the dashes"},
85
- :pretend=>{:type=>:boolean, :desc=>"Display what a command would execute without executing it"},
86
- } #:nodoc:
87
- RENDER_OPTIONS = {
88
- :fields=>{:type=>:array, :desc=>"Displays fields in the order given"},
89
- :class=>{:type=>:string, :desc=>"Hirb helper class which renders"},
90
- :max_width=>{:type=>:numeric, :desc=>"Max width of a table"},
91
- :vertical=>{:type=>:boolean, :desc=>"Display a vertical table"},
92
- :sort=>{:type=>:string, :desc=>"Sort by given field"},
93
- :reverse_sort=>{:type=>:boolean, :desc=>"Reverse a given sort"},
94
- :query=>{:type=>:hash, :desc=>"Queries fields given field:search pairs"},
95
- } #:nodoc:
40
+ @option_commands ||= {}
96
41
 
97
42
  # Redefines an object's method with a Command of the same name.
98
- def create_option_command(obj, command)
99
- cmd_block = create_option_command_block(obj, command)
43
+ def redefine_command(obj, command)
44
+ cmd_block = redefine_command_block(obj, command)
100
45
  @no_option_commands << command if command.options.nil?
101
46
  [command.name, command.alias].compact.each {|e|
102
47
  obj.instance_eval("class<<self;self;end").send(:define_method, e, cmd_block)
103
48
  }
104
49
  end
105
50
 
106
- # A wrapper around create_option_command that doesn't depend on a Command object. Rather you
51
+ # A wrapper around redefine_command that doesn't depend on a Command object. Rather you
107
52
  # simply pass a hash of command attributes (see Command.new) or command methods and let OpenStruct mock a command.
108
53
  # The only required attribute is :name, though to get any real use you should define :options and
109
54
  # :arg_size (default is '*'). Example:
@@ -123,19 +68,23 @@ module Boson
123
68
  hash[:has_splat_args?] = true if hash[:arg_size] == '*'
124
69
  fake_cmd = OpenStruct.new(hash)
125
70
  fake_cmd.option_parser ||= OptionParser.new(fake_cmd.options || {})
126
- create_option_command(obj, fake_cmd)
71
+ redefine_command(obj, fake_cmd)
127
72
  end
128
73
 
129
- # The actual method which replaces a command's original method
130
- def create_option_command_block(obj, command)
74
+ # The actual method which redefines a command's original method
75
+ def redefine_command_block(obj, command)
131
76
  lambda {|*args|
132
77
  Boson::Scientist.translate_and_render(obj, command, args) {|args| super(*args) }
133
78
  }
134
79
  end
135
80
 
136
81
  #:stopdoc:
82
+ def option_command(cmd=@command)
83
+ @option_commands[cmd] ||= OptionCommand.new(cmd)
84
+ end
85
+
137
86
  def translate_and_render(obj, command, args)
138
- @global_options = {}
87
+ @global_options, @command = {}, command
139
88
  args = translate_args(obj, command, args)
140
89
  if @global_options[:verbose] || @global_options[:pretend]
141
90
  puts "Arguments: #{args.inspect}", "Global options: #{@global_options.inspect}"
@@ -149,24 +98,16 @@ module Boson
149
98
  end
150
99
 
151
100
  def translate_args(obj, command, args)
152
- @obj, @command, @args = obj, command, args
153
- # prepends default option
154
- if @command.default_option && @command.arg_size == 1 && !@command.has_splat_args? && @args[0].to_s[/./] != '-'
155
- @args[0] = "--#{@command.default_option}=#{@args[0]}" unless @args.join.empty? || @args[0].is_a?(Hash)
156
- end
157
- @command.options ||= {}
158
-
159
- if parsed_options = parse_command_options
160
- add_default_args(@args)
161
- return @args if @no_option_commands.include?(@command)
162
- @args << parsed_options
163
- if @args.size != command.arg_size && !command.has_splat_args?
164
- command_size, args_size = @args.size > command.arg_size ? [command.arg_size, @args.size] :
165
- [command.arg_size - 1, @args.size - 1]
166
- raise ArgumentError, "wrong number of arguments (#{args_size} for #{command_size})"
167
- end
101
+ option_command.prepend_default_option(args)
102
+ @global_options, parsed_options, args = option_command.parse(args)
103
+ raise EscapeGlobalOption if @global_options[:help]
104
+ if parsed_options
105
+ option_command.add_default_args(args, obj)
106
+ return args if @no_option_commands.include?(command)
107
+ args << parsed_options
108
+ option_command.check_argument_size(args)
168
109
  end
169
- @args
110
+ args
170
111
  rescue Error, ArgumentError, EscapeGlobalOption
171
112
  raise
172
113
  rescue Exception
@@ -176,137 +117,19 @@ module Boson
176
117
 
177
118
  def render_or_raw(result)
178
119
  if (@rendered = render?)
179
- result = run_pipe_commands(result)
180
- render_global_opts = @global_options.dup.delete_if {|k,v| default_global_options.keys.include?(k) }
181
- View.render(result, render_global_opts, false)
120
+ result = Pipe.process(result, @global_options) if @global_options.key?(:class) ||
121
+ @global_options.key?(:method)
122
+ View.render(result, OptionCommand.delete_non_render_options(@global_options.dup), false)
182
123
  else
183
- result = View.search_and_sort(result, @global_options) if !(@global_options.keys & [:sort, :reverse_sort, :query]).empty?
184
- run_pipe_commands(result)
124
+ Pipe.process(result, @global_options)
185
125
  end
186
126
  rescue Exception
187
127
  message = @global_options[:verbose] ? "#{$!}\n#{$!.backtrace.inspect}" : $!.message
188
128
  raise Error, message
189
129
  end
190
130
 
191
- def pipe_options
192
- @pipe_options ||= Hash[*default_global_options.select {|k,v| v[:pipe] }.flatten]
193
- end
194
-
195
- def run_pipe_commands(result)
196
- (global_options.keys & pipe_options.keys).each {|e|
197
- command = pipe_options[e][:pipe] != true ? pipe_options[e][:pipe] : e
198
- pipe_result = pipe_options[e][:type] == :boolean ? Boson.invoke(command, result) :
199
- Boson.invoke(command, result, global_options[e])
200
- result = pipe_result if pipe_options[e][:filter]
201
- }
202
- result
203
- end
204
-
205
- # choose current parser
206
- def option_parser
207
- @command.render_options ? command_option_parser : default_option_parser
208
- end
209
-
210
- # current command parser
211
- def command_option_parser
212
- (@option_parsers ||= {})[@command] ||= OptionParser.new current_command_options
213
- end
214
-
215
- # set cmd and use its parser
216
- def render_option_parser(cmd)
217
- @command = cmd
218
- option_parser
219
- end
220
-
221
- def default_option_parser
222
- @default_option_parser ||= OptionParser.new default_render_options.merge(default_global_options)
223
- end
224
-
225
- def default_global_options
226
- @default_global_options ||= GLOBAL_OPTIONS.merge Boson.repo.config[:global_options] || {}
227
- end
228
-
229
- def default_render_options
230
- @default_render_options ||= RENDER_OPTIONS.merge Boson.repo.config[:render_options] || {}
231
- end
232
-
233
- def current_command_options
234
- (@command_options ||= {})[@command] ||= begin
235
- @command.render_options.each {|k,v|
236
- if !v.is_a?(Hash) && !v.is_a?(Symbol)
237
- @command.render_options[k] = {:default=>v}
238
- end
239
- }
240
- render_opts = Util.recursive_hash_merge(@command.render_options, Util.deep_copy(default_render_options))
241
- opts = Util.recursive_hash_merge render_opts, Util.deep_copy(default_global_options)
242
- if !opts[:fields].key?(:values)
243
- if opts[:fields][:default]
244
- opts[:fields][:values] = opts[:fields][:default]
245
- else
246
- opts[:fields][:values] = opts[:change_fields][:default].values if opts[:change_fields] && opts[:change_fields][:default]
247
- opts[:fields][:values] ||= opts[:headers][:default].keys if opts[:headers] && opts[:headers][:default]
248
- end
249
- opts[:fields][:enum] = false if opts[:fields][:values] && !opts[:fields].key?(:enum)
250
- end
251
- if opts[:fields][:values]
252
- opts[:sort][:values] ||= opts[:fields][:values]
253
- opts[:query][:keys] ||= opts[:fields][:values]
254
- opts[:query][:default_keys] ||= "*"
255
- end
256
- opts
257
- end
258
- end
259
-
260
131
  def render?
261
- (@command.render_options && !@global_options[:render]) || (!@command.render_options && @global_options[:render])
262
- end
263
-
264
- def parse_command_options
265
- if @args.size == 1 && @args[0].is_a?(String)
266
- parsed_options, @args = parse_options Shellwords.shellwords(@args[0])
267
- # last string argument interpreted as args + options
268
- elsif @args.size > 1 && @args[-1].is_a?(String)
269
- args = Boson.const_defined?(:BinRunner) ? @args : Shellwords.shellwords(@args.pop)
270
- parsed_options, new_args = parse_options args
271
- @args += new_args
272
- # add default options
273
- elsif @command.options.empty? || (!@command.has_splat_args? &&
274
- @args.size <= (@command.arg_size - 1).abs) || (@command.has_splat_args? && !@args[-1].is_a?(Hash))
275
- parsed_options = parse_options([])[0]
276
- # merge default options with given hash of options
277
- elsif (@command.has_splat_args? || (@args.size == @command.arg_size)) && @args[-1].is_a?(Hash)
278
- parsed_options = parse_options([])[0]
279
- parsed_options.merge!(@args.pop)
280
- end
281
- parsed_options
282
- end
283
-
284
- def parse_options(args)
285
- parsed_options = @command.option_parser.parse(args, :delete_invalid_opts=>true)
286
- @global_options = option_parser.parse @command.option_parser.leading_non_opts
287
- new_args = option_parser.non_opts.dup + @command.option_parser.trailing_non_opts
288
- if @global_options[:global]
289
- global_opts = Shellwords.shellwords(@global_options[:global]).map {|str|
290
- ((str[/^(.*?)=/,1] || str).length > 1 ? "--" : "-") + str }
291
- @global_options.merge! option_parser.parse(global_opts)
292
- end
293
- raise EscapeGlobalOption if @global_options[:help]
294
- [parsed_options, new_args]
295
- end
296
-
297
- def add_default_args(args)
298
- if @command.args && args.size < @command.args.size - 1
299
- # leave off last arg since its an option
300
- @command.args.slice(0..-2).each_with_index {|arr,i|
301
- next if args.size >= i + 1 # only fill in once args run out
302
- break if arr.size != 2 # a default arg value must exist
303
- begin
304
- args[i] = @command.file_parsed_args? ? @obj.instance_eval(arr[1]) : arr[1]
305
- rescue Exception
306
- raise Error, "Unable to set default argument at position #{i+1}.\nReason: #{$!.message}"
307
- end
308
- }
309
- end
132
+ !!@command.render_options ^ @global_options[:render]
310
133
  end
311
134
  #:startdoc:
312
135
  end