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.
@@ -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