mothership 0.0.1

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