mothership 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,154 @@
1
+ class Mothership
2
+ class Parser
3
+ def initialize(command)
4
+ @command = command
5
+ end
6
+
7
+ def inputs(argv)
8
+ inputs = {}
9
+
10
+ args = parse_flags(inputs, argv.dup)
11
+
12
+ parse_arguments(inputs, args)
13
+
14
+ inputs
15
+ end
16
+
17
+ def parse_flags(inputs, argv)
18
+ args = []
19
+
20
+ until argv.empty?
21
+ flag = normalize_flag(argv.shift, argv)
22
+
23
+ name = @command.flags[flag]
24
+ unless name
25
+ args << flag
26
+ next
27
+ end
28
+
29
+ input = @command.inputs[name]
30
+
31
+ case input[:type]
32
+ when :bool, :boolean
33
+ if argv.first == "false" || argv.first == "true"
34
+ inputs[name] = argv.shift == "true"
35
+ else
36
+ inputs[name] = true
37
+ end
38
+ when :float, :floating
39
+ if !argv.empty? && argv.first =~ /^[0-9]+(\.[0-9]*)?$/
40
+ inputs[name] = argv.shift.to_f
41
+ else
42
+ raise TypeMismatch.new(@command.name, name, "floating")
43
+ end
44
+ when :integer, :number, :numeric
45
+ if !argv.empty? && argv.first =~ /^[0-9]+$/
46
+ inputs[name] = argv.shift.to_i
47
+ else
48
+ raise TypeMismatch.new(@command.name, name, "numeric")
49
+ end
50
+ else
51
+ if argv.empty? || !argv.first.start_with?("-")
52
+ arg = argv.shift || ""
53
+
54
+ inputs[name] =
55
+ if input[:argument] == :splat
56
+ arg.split(",")
57
+ else
58
+ arg
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ args
65
+ end
66
+
67
+ # [FOO] [BAR] FIZZ BUZZ:
68
+ # 1 2 => :fizz => 1, :buzz => 2
69
+ # 1 2 3 => :foo => 1, :fizz => 2, :buzz => 3
70
+ # 1 2 3 4 => :foo => 1, :bar => 2, :fizz => 3, :buzz => 4
71
+ def parse_arguments(inputs, args)
72
+ total = @command.arguments.size
73
+ required = 0
74
+ optional = 0
75
+ @command.arguments.each do |arg|
76
+ case arg[:type]
77
+ when :optional
78
+ optional += 1
79
+ when :splat
80
+ break
81
+ else
82
+ required += 1
83
+ end
84
+ end
85
+
86
+ parse_optionals = args.size - required
87
+
88
+ @command.arguments.each do |arg|
89
+ name = arg[:name]
90
+ next if inputs.key? name
91
+
92
+ case arg[:type]
93
+ when :splat
94
+ inputs[name] = []
95
+
96
+ until args.empty?
97
+ inputs[name] << args.shift
98
+ end
99
+
100
+ when :optional
101
+ if parse_optionals > 0 && val = args.shift
102
+ inputs[name] = val
103
+ parse_optionals -= 1
104
+ end
105
+
106
+ else
107
+ if val = args.shift
108
+ inputs[name] = val
109
+ elsif !@command.inputs[name][:default]
110
+ raise MissingArgument.new(@command.name, name)
111
+ end
112
+ end
113
+ end
114
+
115
+ raise ExtraArguments.new(@command.name) unless args.empty?
116
+ end
117
+
118
+ private
119
+
120
+ # --no-foo => --foo false
121
+ # --no-foo true => --foo false
122
+ # --no-foo false => --foo true
123
+ #
124
+ # --foo=bar => --foo bar
125
+ def normalize_flag(flag, argv)
126
+ case flag
127
+ # boolean negation
128
+ when /^--no-(.+)/
129
+ case argv.first
130
+ when "true"
131
+ argv[0] = "false"
132
+ when "false"
133
+ argv[0] = "true"
134
+ else
135
+ argv.unshift "false"
136
+ end
137
+
138
+ "--#$1"
139
+
140
+ # --foo=bar form
141
+ when /^--([^=]+)=(.+)/
142
+ argv.unshift $2
143
+ "--#$1"
144
+
145
+ # normal flag name
146
+ when /^--([^ ]+)$/
147
+ "--#$1"
148
+
149
+ else
150
+ flag
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,82 @@
1
+ require "rbconfig"
2
+
3
+ # Mix in to your Mothership class to enable user-toggleable colors.
4
+ #
5
+ # Redefine color_enabled? to control color enabling/disabling. Colors will be
6
+ # auto-disabled if the platform is Windows or if $stdout is not a tty.
7
+ #
8
+ # Redefine user_colors to return a hash from tags to color, e.g. from a user's
9
+ # color config file.
10
+ module Mothership::Pretty
11
+ WINDOWS = !!(RbConfig::CONFIG['host_os'] =~ /mingw|mswin32|cygwin/)
12
+
13
+ COLOR_CODES = {
14
+ :black => 0,
15
+ :red => 1,
16
+ :green => 2,
17
+ :yellow => 3,
18
+ :blue => 4,
19
+ :magenta => 5,
20
+ :cyan => 6,
21
+ :white => 7
22
+ }
23
+
24
+ DEFAULT_COLORS = {
25
+ :name => :blue,
26
+ :neutral => :blue,
27
+ :good => :green,
28
+ :bad => :red,
29
+ :error => :magenta,
30
+ :unknown => :cyan,
31
+ :warning => :yellow,
32
+ :instance => :yellow,
33
+ :number => :green,
34
+ :prompt => :blue,
35
+ :yes => :green,
36
+ :no => :red,
37
+ :dim => :black,
38
+ :default => :black
39
+ }
40
+
41
+ private
42
+
43
+ # override with e.g. option(:color), or whatever toggle you use
44
+ def color_enabled?
45
+ true
46
+ end
47
+
48
+ # use colors?
49
+ def color?
50
+ color_enabled? && !WINDOWS && $stdout.tty?
51
+ end
52
+
53
+ # redefine to control the tag -> color settings
54
+ def user_colors
55
+ DEFAULT_COLORS
56
+ end
57
+
58
+ # colored text
59
+ #
60
+ # shouldn't use bright colors, as some color themes abuse
61
+ # the bright palette (I'm looking at you, Solarized)
62
+ def c(str, type)
63
+ return str unless color?
64
+
65
+ bright = false
66
+ color = user_colors[type]
67
+ if color =~ /bright-(.+)/
68
+ bright = true
69
+ color = $1.to_sym
70
+ end
71
+
72
+ return str unless color
73
+
74
+ "\e[#{bright ? 9 : 3}#{COLOR_CODES[color]}m#{str}\e[0m"
75
+ end
76
+
77
+ # bold text
78
+ def b(str)
79
+ return str unless color?
80
+ "\e[1m#{str}\e[0m"
81
+ end
82
+ end
@@ -0,0 +1,112 @@
1
+ require "mothership/pretty"
2
+
3
+ module Mothership::Progress
4
+ include Mothership::Pretty
5
+
6
+ module Dots
7
+ class << self
8
+ DOT_COUNT = 3
9
+ DOT_TICK = 0.15
10
+
11
+ def start!
12
+ @dots ||=
13
+ Thread.new do
14
+ before_sync = $stdout.sync
15
+
16
+ $stdout.sync = true
17
+
18
+ printed = false
19
+ i = 1
20
+ until @stop_dots
21
+ if printed
22
+ print "\b" * DOT_COUNT
23
+ end
24
+
25
+ print ("." * i).ljust(DOT_COUNT)
26
+ printed = true
27
+
28
+ if i == DOT_COUNT
29
+ i = 0
30
+ else
31
+ i += 1
32
+ end
33
+
34
+ sleep DOT_TICK
35
+ end
36
+
37
+ if printed
38
+ print "\b" * DOT_COUNT
39
+ print " " * DOT_COUNT
40
+ print "\b" * DOT_COUNT
41
+ end
42
+
43
+ $stdout.sync = before_sync
44
+ @stop_dots = nil
45
+ end
46
+ end
47
+
48
+ def stop!
49
+ return unless @dots
50
+ return if @stop_dots
51
+ @stop_dots = true
52
+ @dots.join
53
+ @dots = nil
54
+ end
55
+ end
56
+ end
57
+
58
+ class Skipper
59
+ def initialize(&ret)
60
+ @return = ret
61
+ end
62
+
63
+ def skip(&callback)
64
+ @return.call("SKIPPED", :warning, callback)
65
+ end
66
+
67
+ def give_up(&callback)
68
+ @return.call("GAVE UP", :bad, callback)
69
+ end
70
+
71
+ def fail(&callback)
72
+ @return.call("FAILED", :error, callback)
73
+ end
74
+ end
75
+
76
+ # override to determine whether to show progress
77
+ def quiet?
78
+ false
79
+ end
80
+
81
+ def with_progress(message)
82
+ unless quiet?
83
+ print message
84
+ Dots.start!
85
+ end
86
+
87
+ skipper = Skipper.new do |status, color, callback|
88
+ unless quiet?
89
+ Dots.stop!
90
+ puts "... #{c(status, color)}"
91
+ end
92
+
93
+ return callback && callback.call
94
+ end
95
+
96
+ begin
97
+ res = yield skipper
98
+ unless quiet?
99
+ Dots.stop!
100
+ puts "... #{c("OK", :good)}"
101
+ end
102
+ res
103
+ rescue
104
+ unless quiet?
105
+ Dots.stop!
106
+ puts "... #{c("FAILED", :error)}"
107
+ end
108
+
109
+ raise
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,3 @@
1
+ class Mothership
2
+ VERSION = "0.0.1"
3
+ end
data/lib/mothership.rb ADDED
@@ -0,0 +1,66 @@
1
+ require "mothership/base"
2
+ require "mothership/callbacks"
3
+ require "mothership/command"
4
+ require "mothership/parser"
5
+ require "mothership/help"
6
+ require "mothership/errors"
7
+
8
+ class Mothership
9
+ # [Mothership::Command] global options
10
+ @@global = Command.new(self, "(global options)")
11
+
12
+ # [Mothershp::Inputs] inputs from global options
13
+ @@inputs = nil
14
+
15
+ # [Fixnum] exit status; reassign as appropriate error code (e.g. 1)
16
+ @@exit_status = 0
17
+
18
+ class << self
19
+ # define a global option
20
+ def option(name, options = {}, &default)
21
+ @@global.add_input(name, options, &default)
22
+ end
23
+
24
+ # parse argv, by taking the first arg as the command, and the rest as
25
+ # arguments and flags
26
+ #
27
+ # arguments and flags can be in any order; all flags will be parsed out
28
+ # first, and the bits left over will be treated as arguments
29
+ def start(argv)
30
+ @@inputs = Inputs.new(@@global, self, {})
31
+
32
+ name, *argv =
33
+ Parser.new(@@global).parse_flags(
34
+ @@inputs.inputs,
35
+ argv)
36
+
37
+ app = new
38
+
39
+ return app.default_action unless name
40
+
41
+ cmdname = name.gsub("-", "_").to_sym
42
+
43
+ cmd = @@commands[cmdname]
44
+ return app.unknown_command(cmdname) unless cmd
45
+
46
+ app.execute(cmd, argv)
47
+
48
+ exit @@exit_status
49
+ end
50
+ end
51
+
52
+ # set the exit status
53
+ def exit_status(num)
54
+ @@exit_status = num
55
+ end
56
+
57
+ # get value of global option
58
+ def option(name, *args)
59
+ @@inputs[name, *args]
60
+ end
61
+
62
+ # test if an option was explicitly provided
63
+ def option_given?(name)
64
+ @@inputs.given? name
65
+ end
66
+ end
data/spec/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ Bundler.require(:default, :development)
4
+
5
+ require 'rake/dsl_definition'
6
+ require 'rake'
7
+ require 'rspec'
8
+ require 'rspec/core/rake_task'
9
+
10
+ RSpec::Core::RakeTask.new do |t|
11
+ t.pattern = "**/*_spec.rb"
12
+ t.rspec_opts = ["--format", "documentation", "--colour"]
13
+ end
14
+
@@ -0,0 +1,164 @@
1
+ require "mothership"
2
+ require "./helpers"
3
+
4
+ describe Mothership::Parser do
5
+ describe "arguments" do
6
+ describe "normal" do
7
+ it "is declared as an input" do
8
+ command(:foo => { :argument => true }) do |c|
9
+ inputs(c, "bar").should == { :foo => "bar" }
10
+ end
11
+ end
12
+
13
+ it "can be passed as a flag" do
14
+ command(:foo => { :argument => true }) do |c|
15
+ inputs(c, "--foo", "bar").should == { :foo => "bar" }
16
+ end
17
+ end
18
+
19
+ it "is parsed in the order of definition" do
20
+ command([
21
+ [:foo, { :argument => true }],
22
+ [:bar, { :argument => true }]]) do |c|
23
+ inputs(c, "fizz", "buzz").should ==
24
+ { :foo => "fizz", :bar => "buzz" }
25
+ end
26
+
27
+ command([
28
+ [:bar, { :argument => true }],
29
+ [:foo, { :argument => true }]]) do |c|
30
+ inputs(c, "fizz", "buzz").should ==
31
+ { :foo => "buzz", :bar => "fizz" }
32
+ end
33
+ end
34
+ end
35
+
36
+ describe "optional & required ordering" do
37
+ it "parses required arguments positioned before optionals first" do
38
+ command([
39
+ [:foo, { :argument => true }],
40
+ [:foo2, { :argument => true }],
41
+ [:bar, { :argument => :optional }],
42
+ [:bar2, { :argument => :optional }]]) do |c|
43
+ inputs(c, "a", "b").should == { :foo => "a", :foo2 => "b" }
44
+
45
+ inputs(c, "a", "b", "c").should ==
46
+ { :foo => "a", :foo2 => "b", :bar => "c" }
47
+
48
+ inputs(c, "a", "b", "c", "d").should ==
49
+ { :foo => "a", :foo2 => "b", :bar => "c", :bar2 => "d" }
50
+ end
51
+ end
52
+
53
+ it "parses required arguments positioned after optionals first " do
54
+ command([
55
+ [:foo, { :argument => :optional }],
56
+ [:foo2, { :argument => :optional }],
57
+ [:bar, { :argument => true }],
58
+ [:bar2, { :argument => true }]]) do |c|
59
+ inputs(c, "a", "b").should == { :bar => "a", :bar2 => "b" }
60
+
61
+ inputs(c, "a", "b", "c").should ==
62
+ { :bar => "b", :bar2 => "c", :foo => "a" }
63
+
64
+ inputs(c, "a", "b", "c", "d").should ==
65
+ { :bar => "c", :bar2 => "d", :foo => "a", :foo2 => "b" }
66
+ end
67
+ end
68
+
69
+ it "parses required arguments positioned around optionals first " do
70
+ command([
71
+ [:foo, { :argument => true }],
72
+ [:foo2, { :argument => :optional }],
73
+ [:bar, { :argument => :optional }],
74
+ [:bar2, { :argument => true }]]) do |c|
75
+ inputs(c, "a", "b").should == { :foo => "a", :bar2 => "b" }
76
+
77
+ inputs(c, "a", "b", "c").should ==
78
+ { :foo => "a", :foo2 => "b", :bar2 => "c" }
79
+
80
+ inputs(c, "a", "b", "c", "d").should ==
81
+ { :foo => "a", :foo2 => "b", :bar => "c", :bar2 => "d" }
82
+ end
83
+ end
84
+
85
+ it "parses required arguments positioned between optionals first " do
86
+ command([
87
+ [:foo, { :argument => :optional }],
88
+ [:foo2, { :argument => true }],
89
+ [:bar, { :argument => true }],
90
+ [:bar2, { :argument => :optional }]]) do |c|
91
+ inputs(c, "a", "b").should == { :foo2 => "a", :bar => "b" }
92
+
93
+ inputs(c, "a", "b", "c").should ==
94
+ { :foo => "a", :foo2 => "b", :bar => "c" }
95
+
96
+ inputs(c, "a", "b", "c", "d").should ==
97
+ { :foo => "a", :foo2 => "b", :bar => "c", :bar2 => "d" }
98
+ end
99
+ end
100
+ end
101
+
102
+ describe "as flags" do
103
+ it "assigns as the value if given" do
104
+ command(:foo => { :argument => true }) do |c|
105
+ inputs(c, "--foo", "bar").should == { :foo => "bar" }
106
+ end
107
+ end
108
+
109
+ it "assigns as an empty string if just name is given" do
110
+ command(:foo => { :argument => true }) do |c|
111
+ inputs(c, "--foo").should == { :foo => "" }
112
+ end
113
+ end
114
+ end
115
+
116
+ describe "splats" do
117
+ it "is declared as an input" do
118
+ command(:foo => { :argument => :splat }) do |c|
119
+ inputs(c, "bar").should == { :foo => ["bar"] }
120
+ end
121
+ end
122
+
123
+ it "assigns as an empty array if no arguments given" do
124
+ command(:foo => { :argument => :splat }) do |c|
125
+ inputs(c).should == { :foo => [] }
126
+ end
127
+ end
128
+
129
+ it "assigns as an array if one argument given" do
130
+ command(:foo => { :argument => :splat }) do |c|
131
+ inputs(c, "foo").should == { :foo => ["foo"] }
132
+ end
133
+ end
134
+
135
+ it "assigns as an array if two or more arguments given" do
136
+ command(:foo => { :argument => :splat }) do |c|
137
+ inputs(c, "foo", "bar").should == { :foo => ["foo", "bar"] }
138
+ inputs(c, "foo", "bar", "baz").should ==
139
+ { :foo => ["foo", "bar", "baz"] }
140
+ end
141
+ end
142
+
143
+ describe "as flags" do
144
+ it "assigns as an empty array if just name is given" do
145
+ command(:foo => { :argument => :splat }) do |c|
146
+ inputs(c, "--foo").should == { :foo => [] }
147
+ end
148
+ end
149
+
150
+ it "assigns as an array if one value given" do
151
+ command(:foo => { :argument => :splat }) do |c|
152
+ inputs(c, "--foo", "bar").should == { :foo => ["bar"] }
153
+ end
154
+ end
155
+
156
+ it "accepts comma-separated values" do
157
+ command(:foo => { :argument => :splat }) do |c|
158
+ inputs(c, "--foo", "bar,baz").should == { :foo => ["bar", "baz"] }
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,105 @@
1
+ require "mothership"
2
+ require "./helpers"
3
+
4
+ describe Mothership::Parser do
5
+ describe "combinations" do
6
+ describe "arguments & flags" do
7
+ it "parses flags placed after arguments" do
8
+ command(:flag => {}, :arg => { :argument => true }) do |c|
9
+ inputs(c, "foo", "--flag", "bar").should ==
10
+ { :arg => "foo", :flag => "bar" }
11
+ end
12
+ end
13
+
14
+ it "parses flags placed before arguments" do
15
+ command(:flag => {}, :arg => { :argument => true }) do |c|
16
+ inputs(c, "--flag", "bar", "foo").should ==
17
+ { :arg => "foo", :flag => "bar" }
18
+ end
19
+ end
20
+
21
+ it "parses flags placed between arguments" do
22
+ command([
23
+ [:flag, {}],
24
+ [:arg1, { :argument => true }],
25
+ [:arg2, { :argument => true }]]) do |c|
26
+ inputs(c, "foo", "--flag", "bar", "baz").should ==
27
+ { :arg1 => "foo", :flag => "bar", :arg2 => "baz" }
28
+ end
29
+ end
30
+
31
+ it "skips parsing arguments that were passed as flags" do
32
+ command([
33
+ [:arg1, { :argument => true }],
34
+ [:arg2, { :argument => true }]]) do |c|
35
+ inputs(c, "baz", "--arg1", "foo").should ==
36
+ { :arg1 => "foo", :arg2 => "baz" }
37
+ end
38
+ end
39
+ end
40
+
41
+ describe "arguments & splats" do
42
+ it "consumes the rest of the arguments" do
43
+ command([
44
+ [:foo, { :argument => :splat }],
45
+ [:bar, { :argument => true }]]) do |c|
46
+ proc {
47
+ inputs(c, "fizz", "buzz")
48
+ }.should raise_error(Mothership::MissingArgument)
49
+ end
50
+ end
51
+
52
+ it "consumes arguments after normal arguments" do
53
+ command([
54
+ [:foo, { :argument => true }],
55
+ [:bar, { :argument => :splat }]]) do |c|
56
+ inputs(c, "fizz", "buzz").should ==
57
+ { :foo => "fizz", :bar => ["buzz"] }
58
+ end
59
+ end
60
+
61
+ it "appears empty when there are no arguments left after normal" do
62
+ command([
63
+ [:foo, { :argument => true }],
64
+ [:bar, { :argument => :splat }]]) do |c|
65
+ inputs(c, "fizz").should ==
66
+ { :foo => "fizz", :bar => [] }
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "splats & flags" do
72
+ it "parses flags placed after splats" do
73
+ command(:flag => {}, :arg => { :argument => :splat }) do |c|
74
+ inputs(c, "foo", "--flag", "bar").should ==
75
+ { :arg => ["foo"], :flag => "bar" }
76
+ end
77
+ end
78
+
79
+ it "parses flags placed before splats" do
80
+ command(:flag => {}, :arg => { :argument => :splat }) do |c|
81
+ inputs(c, "--flag", "bar", "foo").should ==
82
+ { :arg => ["foo"], :flag => "bar" }
83
+ end
84
+ end
85
+
86
+ it "parses flags placed between splat arguments" do
87
+ command([
88
+ [:flag, {}],
89
+ [:arg, { :argument => :splat }]]) do |c|
90
+ inputs(c, "foo", "--flag", "bar", "baz").should ==
91
+ { :arg => ["foo", "baz"], :flag => "bar" }
92
+ end
93
+ end
94
+
95
+ it "skips parsing splats that were passed as flags" do
96
+ command([
97
+ [:arg1, { :argument => :splat }],
98
+ [:arg2, { :argument => true }]]) do |c|
99
+ inputs(c, "baz", "--arg1", "foo").should ==
100
+ { :arg1 => ["foo"], :arg2 => "baz" }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end