boson 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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