mothership 0.0.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/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ Copyright (c)2012, Alex Suraci
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+
11
+ * Redistributions in binary form must reproduce the above
12
+ copyright notice, this list of conditions and the following
13
+ disclaimer in the documentation and/or other materials provided
14
+ with the distribution.
15
+
16
+ * Neither the name of Alex Suraci nor the names of other
17
+ contributors may be used to endorse or promote products derived
18
+ from this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require "rubygems"
2
+ require "bundler"
3
+
4
+ if Gem::Version.new(Bundler::VERSION) > Gem::Version.new("1.0.12")
5
+ require "bundler/gem_tasks"
6
+ end
7
+
8
+ task :default => "spec"
9
+
10
+ desc "Run specs"
11
+ task "spec" => ["bundler:install", "test:spec"]
12
+
13
+ namespace "bundler" do
14
+ desc "Install gems"
15
+ task "install" do
16
+ sh("bundle install")
17
+ end
18
+ end
19
+
20
+ namespace "test" do
21
+ task "spec" do |t|
22
+ sh("cd spec && bundle exec rake spec")
23
+ end
24
+ end
@@ -0,0 +1,62 @@
1
+ require "mothership/command"
2
+ require "mothership/inputs"
3
+
4
+ class Mothership
5
+ # all commands
6
+ @@commands = {}
7
+
8
+ # parsed global input set
9
+ @@inputs = nil
10
+
11
+ # Initialize with the command being executed.
12
+ def initialize(command = nil)
13
+ @command = command
14
+ end
15
+
16
+ class << self
17
+ # start defining a new command with the given description
18
+ def desc(description)
19
+ @command = Command.new(self, description)
20
+ end
21
+
22
+ # define an input for the current command or the global command
23
+ def input(name, options = {}, &default)
24
+ raise "no current command" unless @command
25
+
26
+ @command.add_input(name, options, &default)
27
+ end
28
+
29
+ # register a command
30
+ def method_added(name)
31
+ return unless @command
32
+
33
+ @command.name = name
34
+ @@commands[name] = @command
35
+
36
+ @command = nil
37
+ end
38
+
39
+ def alias_command(orig, new)
40
+ @@commands[new] = @@commands[orig]
41
+ end
42
+ end
43
+
44
+ def execute(cmd, argv)
45
+ cmd.invoke(Parser.new(cmd).inputs(argv))
46
+ rescue Mothership::Error => e
47
+ puts e
48
+ puts ""
49
+ Mothership::Help.command_usage(cmd)
50
+
51
+ @@exit_status = 1
52
+ end
53
+
54
+ # invoke a command with the given inputs
55
+ def invoke(name, inputs = {})
56
+ if cmd = @@commands[name]
57
+ cmd.invoke(inputs)
58
+ else
59
+ unknown_command(name)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,75 @@
1
+ class Mothership
2
+ # temporary filters via #with_filters
3
+ #
4
+ # command => { tag => [callbacks] }
5
+ @@filters = Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = [] } }
6
+
7
+ class << self
8
+ # register a callback that's evaluated before a command is run
9
+ def before(name, &callback)
10
+ @@commands[name].before << [callback, self]
11
+ end
12
+
13
+ # register a callback that's evaluated after a command is run
14
+ def after(name, &callback)
15
+ @@commands[name].after << [callback, self]
16
+ end
17
+
18
+ # register a callback that's evaluated around a command, controlling its
19
+ # evaluation (i.e. inputs)
20
+ def around(name, &callback)
21
+ @@commands[name].around << [callback, self]
22
+ end
23
+
24
+ # register a callback that's evaluated when a command uses the given
25
+ # filter
26
+ def filter(name, tag, &callback)
27
+ @@commands[name].filters[tag] << [callback, self]
28
+ end
29
+
30
+ # change an argument's status, i.e. optional, splat, or required
31
+ def change_argument(name, arg, to)
32
+ @@commands[name].arguments.each do |a|
33
+ if a[:name] == arg
34
+ a[:type] = to
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ # filter a value through any plugins
41
+ def filter(tag, val)
42
+ if @@filters.key?(@command.name) &&
43
+ @@filters[@command.name].key?(tag)
44
+ @@filters[@command.name][tag].each do |f, ctx|
45
+ val = ctx.instance_exec(val, &f)
46
+ end
47
+ end
48
+
49
+ @command.filters[tag].each do |f, c|
50
+ val = c.new.instance_exec(val, &f)
51
+ end
52
+
53
+ val
54
+ end
55
+
56
+ # temporary dynamically-scoped filters
57
+ def with_filters(filters)
58
+ filters.each do |cmd, callbacks|
59
+ callbacks.each do |tag, callback|
60
+ @@filters[cmd][tag] << [callback, self]
61
+ end
62
+ end
63
+
64
+ yield
65
+ ensure
66
+ filters.each do |cmd, callbacks|
67
+ callbacks.each do |tag, callback|
68
+ @@filters[cmd][tag].pop
69
+ @@filters[cmd].delete(tag) if @@filters[cmd][tag].empty?
70
+ end
71
+
72
+ @@filters.delete(cmd) if @@filters[cmd].empty?
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,120 @@
1
+ require "mothership/inputs"
2
+
3
+ class Mothership
4
+ class Command
5
+ attr_accessor :name, :description
6
+
7
+ attr_reader :inputs, :arguments, :flags
8
+
9
+ attr_reader :before, :after, :around, :filters
10
+
11
+ def initialize(context, description = nil)
12
+ @context = context
13
+ @description = description
14
+ @aliases = []
15
+
16
+ # inputs accepted by command
17
+ @inputs = {}
18
+
19
+ # inputs that act as arguments
20
+ @arguments = []
21
+
22
+ # flag -> input (e.g. --name -> :name)
23
+ @flags = {}
24
+
25
+ # various callbacks
26
+ @before = []
27
+ @after = []
28
+ @around = []
29
+ @filters = Hash.new { |h, k| h[k] = [] }
30
+ end
31
+
32
+ def inspect
33
+ "\#<Command '#{@name}'>"
34
+ end
35
+
36
+ def usage
37
+ str = @name.to_s.gsub("_", "-")
38
+
39
+ @arguments.each do |a|
40
+ name = a[:name].to_s.upcase
41
+
42
+ case a[:type]
43
+ when :splat
44
+ str << " #{name}..."
45
+ when :optional
46
+ str << " [#{name}]"
47
+ else
48
+ str << " #{name}"
49
+ end
50
+ end
51
+
52
+ str
53
+ end
54
+
55
+ def invoke(inputs)
56
+ @before.each { |f, c| c.new.instance_exec(&f) }
57
+
58
+ name = @name
59
+ ctx = @context.new(self)
60
+ input = Inputs.new(self, ctx, inputs)
61
+
62
+ action = proc do |*given_inputs|
63
+ ctx.send(name, given_inputs.first || input)
64
+ end
65
+
66
+ cmd = self
67
+ @around.each do |a, c|
68
+ before = action
69
+
70
+ sub = c.new(cmd)
71
+ action = proc do |*given_inputs|
72
+ sub.instance_exec(before, given_inputs.first || input, &a)
73
+ end
74
+ end
75
+
76
+ res = ctx.instance_exec(input, &action)
77
+
78
+ @after.each { |f, c| c.new.instance_exec(&f) }
79
+
80
+ res
81
+ end
82
+
83
+ def add_input(name, options = {}, &default)
84
+ options[:default] = default if default
85
+ options[:description] = options.delete(:desc) if options.key?(:desc)
86
+
87
+ @flags["--#{name.to_s.gsub("_", "-")}"] = name
88
+
89
+ if options[:singular]
90
+ @flags["--#{options[:singular]}"] = name
91
+ end
92
+
93
+ if aliases = options[:aliases] || options[:alias]
94
+ Array(aliases).each do |a|
95
+ @flags[a] = name
96
+ end
97
+ end
98
+
99
+ # :argument => true means accept as single argument
100
+ # :argument => :foo is shorthand for :argument => {:type => :foo}
101
+ if opts = options[:argument]
102
+ type =
103
+ case opts
104
+ when true
105
+ :normal
106
+ when Symbol
107
+ opts
108
+ when Hash
109
+ opts[:type]
110
+ end
111
+
112
+ options[:argument] = type
113
+
114
+ @arguments << { :name => name, :type => type }
115
+ end
116
+
117
+ @inputs[name] = options
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,37 @@
1
+ class Mothership
2
+ class Error < RuntimeError
3
+ end
4
+
5
+ class MissingArgument < Error
6
+ def initialize(cmd, arg)
7
+ @command = cmd
8
+ @argument = arg
9
+ end
10
+
11
+ def to_s
12
+ "#{@command}: missing input '#{@argument}'"
13
+ end
14
+ end
15
+
16
+ class ExtraArguments < Error
17
+ def initialize(cmd)
18
+ @command = cmd
19
+ end
20
+
21
+ def to_s
22
+ "#{@command}: too many arguments"
23
+ end
24
+ end
25
+
26
+ class TypeMismatch < Error
27
+ def initialize(cmd, input, type)
28
+ @command = cmd
29
+ @input = input
30
+ @type = type
31
+ end
32
+
33
+ def to_s
34
+ "#{@command}: expected #{@type} value for #{@input}"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,237 @@
1
+ require "mothership/base"
2
+
3
+ module Mothership::Help
4
+ @@groups = []
5
+ @@tree = {}
6
+
7
+ class << self
8
+ def has_groups?
9
+ !@@groups.empty?
10
+ end
11
+
12
+ def print_help_groups(global = nil, all = false)
13
+ @@groups.each do |commands|
14
+ print_help_group(commands, all)
15
+ end
16
+
17
+ command_options(global)
18
+ end
19
+
20
+ def print_help_group(group, all = false, indent = 0)
21
+ return if nothing_printable?(group, all)
22
+
23
+ members = group[:members]
24
+
25
+ unless all
26
+ members = members.reject do |_, opts|
27
+ opts[:hidden]
28
+ end
29
+ end
30
+
31
+ commands = members.collect(&:first)
32
+
33
+ i = " " * indent
34
+
35
+ print i
36
+ puts group[:description]
37
+
38
+ width = 0
39
+ commands.each do |cmd|
40
+ len = cmd.usage.size
41
+ if len > width
42
+ width = len
43
+ end
44
+ end
45
+
46
+ commands.each do |cmd|
47
+ puts "#{i} #{cmd.usage.ljust(width)}\t#{cmd.description}"
48
+ end
49
+
50
+ puts "" unless commands.empty?
51
+
52
+ group[:children].each do |group|
53
+ print_help_group(group, all, indent + 1)
54
+ end
55
+ end
56
+
57
+ # define help groups
58
+ def groups(*tree)
59
+ tree.each do |*args|
60
+ add_group(@@groups, @@tree, *args.first)
61
+ end
62
+ end
63
+
64
+ def add_to_group(command, names, options)
65
+ where = @@tree
66
+ top = true
67
+ names.each do |n|
68
+ where = where[:children] unless top
69
+ break unless where
70
+
71
+ where = where[n]
72
+ break unless where
73
+
74
+ top = false
75
+ end
76
+
77
+ unless where
78
+ raise "unknown help group: #{names.join("/")}"
79
+ end
80
+
81
+ where[:members] << [command, options]
82
+ end
83
+
84
+ def basic_help(commands, global)
85
+ puts "Commands:"
86
+
87
+ width = 0
88
+ commands.each do |_, c|
89
+ len = c.usage.size
90
+ width = len if len > width
91
+ end
92
+
93
+ commands.each do |_, c|
94
+ puts " #{c.usage.ljust(width)}\t#{c.description}"
95
+ end
96
+
97
+ unless global.flags.empty?
98
+ puts ""
99
+ command_options(global)
100
+ end
101
+ end
102
+
103
+ def command_help(cmd)
104
+ puts cmd.description
105
+ puts ""
106
+ command_usage(cmd)
107
+ end
108
+
109
+ def command_usage(cmd)
110
+ puts "Usage: #{cmd.usage}"
111
+
112
+ unless cmd.flags.empty?
113
+ puts ""
114
+ command_options(cmd)
115
+ end
116
+ end
117
+
118
+ def command_options(cmd)
119
+ puts "Options:"
120
+
121
+ rev_flags = Hash.new { |h, k| h[k] = [] }
122
+
123
+ cmd.flags.each do |f, n|
124
+ rev_flags[n] << f
125
+ end
126
+
127
+ usages = []
128
+
129
+ max_bool = 0
130
+ rev_flags.collect do |name, fs|
131
+ info = cmd.inputs[name]
132
+
133
+ usage =
134
+ case info[:type]
135
+ when :boolean
136
+ fs.sort.join(", ")
137
+ else
138
+ fs.sort.collect { |f| "#{f} #{name.to_s.upcase}" }.join(", ")
139
+ end
140
+
141
+ say_no =
142
+ if info[:type] == :boolean
143
+ max_bool = usage.size if usage.size > max_bool
144
+ "--no-#{name.to_s.gsub("_", "-")}"
145
+ end
146
+
147
+ usages << [usage, info[:description], say_no]
148
+ end
149
+
150
+ max_width = 0
151
+ usages.collect! do |usage, desc, bool_no|
152
+ if bool_no
153
+ usage = usage.ljust(max_bool) + " #{bool_no}"
154
+ end
155
+
156
+ max_width = usage.size if usage.size > max_width
157
+
158
+ [usage, desc]
159
+ end
160
+
161
+ usages.sort! { |a, b| a.first <=> b.first }
162
+
163
+ usages.each do |u, d|
164
+ if d
165
+ puts " #{u.ljust(max_width)} #{d}"
166
+ else
167
+ puts " #{u}"
168
+ end
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ def nothing_printable?(group, all = false)
175
+ group[:members].reject { |_, opts| !all && opts[:hidden] }.empty? &&
176
+ group[:children].all? { |g| nothing_printable?(g) }
177
+ end
178
+
179
+ def add_group(groups, tree, name, desc, *subs)
180
+ members = []
181
+
182
+ meta = { :members => members, :children => [] }
183
+ groups << meta
184
+
185
+ tree[name] = { :members => members, :children => {} }
186
+
187
+ meta[:description] = desc
188
+
189
+ subs.each do |*args|
190
+ add_group(meta[:children], tree[name][:children], *args.first)
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ class Mothership
197
+ class << self
198
+ # add command to help group
199
+ def group(*names)
200
+ options =
201
+ if names.last.is_a? Hash
202
+ names.pop
203
+ else
204
+ {}
205
+ end
206
+
207
+ Mothership::Help.add_to_group(@command, names, options)
208
+ end
209
+ end
210
+
211
+ def default_action
212
+ invoke :help
213
+ end
214
+
215
+ def unknown_command(name)
216
+ puts "Unknown command '#{name}'. See 'help' for available commands."
217
+ exit_status 1
218
+ end
219
+
220
+ desc "Help!"
221
+ input :command, :argument => :optional
222
+ input :all, :type => :boolean
223
+ def help(input)
224
+ if name = input[:command]
225
+ Mothership::Help.command_help(@@commands[name.gsub("-", "_").to_sym])
226
+ elsif Help.has_groups?
227
+ unless input[:all]
228
+ puts "Showing basic command set. Pass --all to list all commands."
229
+ puts ""
230
+ end
231
+
232
+ Mothership::Help.print_help_groups(@@global, input[:all])
233
+ else
234
+ Mothership::Help.basic_help(@@commands, @@global)
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,58 @@
1
+ class Mothership
2
+ class Inputs
3
+ attr_reader :inputs
4
+
5
+ def initialize(command, context, inputs = {})
6
+ @command = command
7
+ @context = context
8
+ @inputs = inputs
9
+ @cache = {}
10
+ end
11
+
12
+ def given?(name)
13
+ @inputs.key?(name)
14
+ end
15
+
16
+ def given(name)
17
+ @inputs[name]
18
+ end
19
+
20
+ def merge(inputs)
21
+ self.class.new(@command, @context, @inputs.merge(inputs))
22
+ end
23
+
24
+ def [](name, *args)
25
+ return @inputs[name] if @inputs.key?(name) && @inputs[name] != []
26
+ return @cache[name] if @cache.key? name
27
+
28
+ meta = @command.inputs[name]
29
+
30
+ return unless meta
31
+
32
+ val =
33
+ if meta[:default].respond_to? :to_proc
34
+ @context.instance_exec(*args, &meta[:default])
35
+ elsif meta[:default]
36
+ meta[:default]
37
+ elsif meta[:type] == :boolean
38
+ false
39
+ elsif meta[:argument] == :splat
40
+ if meta[:singular] && single = @inputs[meta[:singular]]
41
+ [single]
42
+ else
43
+ []
44
+ end
45
+ end
46
+
47
+ unless meta[:forget]
48
+ @cache[name] = val
49
+ end
50
+
51
+ val
52
+ end
53
+
54
+ def forget(name)
55
+ @cache.delete(name)
56
+ end
57
+ end
58
+ end