bales 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a449ca597147ec6bc515f40ff54bd657662f9e80
4
+ data.tar.gz: af9b1836f3d639cb968cdfba34daccd60e95ef0c
5
+ SHA512:
6
+ metadata.gz: 3dd8e473c927c35175c378f49ba43f5cd62e639ab929b50dc7f5dfe6f3f5af08a78dee252a72904071d1d737154ff94008a6348428b6857ee0073320c1e26f45
7
+ data.tar.gz: eb9675b63e316b5a316991d4066b054c2698ec799ede8bf966a721de528789296b0b62643acfab94916a5a959d5f28182a5f68d89726c3a2f30bef1342233ee3
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # Ruby on Bales
2
+
3
+ ![bales](https://upload.wikimedia.org/wikipedia/commons/2/2c/DavidBrown-Verdon.jpg)
4
+
5
+ ## What is it?
6
+
7
+ It's a framework for writing command-line applications.
8
+
9
+ ## What does it look like?
10
+
11
+ Why, like this!
12
+
13
+ ```ruby
14
+ #!/usr/bin/env ruby
15
+ # /usr/local/bin/simple-app
16
+ require 'bales'
17
+
18
+ module SimpleApp
19
+ class Application < Bales::Application
20
+ version "0.0.1"
21
+ end
22
+
23
+ class Command < Bales::Command
24
+ action do
25
+ Bales::Command::Help.run
26
+ end
27
+
28
+ class Smack < Command
29
+ option :weapon,
30
+ type: String,
31
+ description: "Thing to smack with",
32
+ short_form: '-w',
33
+ long_form: '--with'
34
+
35
+ action do |victims, opts|
36
+ suffix = opts[:weapon] ? " with a #{opts[:weapon]}" : ""
37
+
38
+ if victims.none?
39
+ puts "You have been smacked#{suffix}."
40
+ else
41
+ victims.each do |victim|
42
+ puts "#{victim} has been smacked#{suffix}."
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ SimpleApp::Application.parse_and_run
51
+ ```
52
+
53
+ And like this (assuming the above script lives in `/usr/local/bin/simple-app`)!
54
+
55
+ ```
56
+ $ simple-app smack
57
+ You have been smacked.
58
+ $ simple-app smack foo
59
+ foo has been smacked.
60
+ $ simple-app Fred Wilma
61
+ Fred has been smacked.
62
+ Wilma has been smacked.
63
+ $ simple-app smack John -w fish
64
+ John has been smacked with a fish.
65
+ $ simple-app smack John --with fish
66
+ John has been smacked with a fish.
67
+ $ simple-app smack John --with=fish
68
+ John has been smacked with a fish.
69
+ ```
70
+
71
+ ## So how does it work?
72
+
73
+ A Bales app is basically a collection of classes: one class representing the application itself (`SimpleApp::Application` in the above example) and one or more classes representing the application's commands (`SimpleApp::Command` and its children in the above example).
74
+
75
+ The application has (or *will* have, more precisely; I don't have a whole lot for you on this front just yet) a few available DSL-ish functions for you to play with.
76
+
77
+ * `version`: sets your app's version number. If you use semantic versioning, you can query this with the `major_version`, `minor_version`, and `patch_level` class methods.
78
+
79
+ ## What kind of a silly names is "Bales", anyway?
80
+
81
+ It's shamelessly stolen^H^H^H^H^H^Hborrowed from Jason R. Clark's "Testing the Multiverse" talk at Ruby on Ales 2015 (which, if you haven't watched, you [totally should](http://confreaks.tv/videos/roa2015-testing-the-multiverse)). Sorry, Jason. Hope you don't mind.
82
+
83
+ ## What's the license?
84
+
85
+ MIT License
86
+
87
+ Copyright (c) 2015 Ryan S. Northrup
88
+
89
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
90
+
91
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
92
+
93
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/lib/bales.rb ADDED
@@ -0,0 +1,27 @@
1
+ # :main: README.md
2
+
3
+ require 'bales/application'
4
+ require 'bales/command'
5
+
6
+ ##
7
+ # Ruby on Bales (or just "Bales" for short) is to command-line apps what
8
+ # Ruby on Rails (or just "Rails" for short) is to websites/webapps.
9
+ #
10
+ # The name (and concept) was shamelessly stolen from Jason R. Clark's
11
+ # "Testing the Multiverse" talk at Ruby on Ales 2015. Here's to hoping that
12
+ # we, as a Ruby programming community, can get a headstart on a command-line
13
+ # app framework *before* the Puma-Unicorn Wars ravage the Earth.
14
+ module Bales
15
+ end
16
+
17
+ # Helper stuff; please ignore
18
+ class String
19
+ def underscore
20
+ self.
21
+ gsub(/::/, '/').
22
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
23
+ gsub(/([a-z\d])([A-Z])/, '\1_\2').
24
+ tr('-', '_').
25
+ downcase
26
+ end
27
+ end
data/lib/bales.rb~ ADDED
@@ -0,0 +1,236 @@
1
+ # :main: README.md
2
+
3
+ ##
4
+ # Ruby on Bales (or just "Bales" for short) is to command-line apps what
5
+ # Ruby on Rails (or just "Rails" for short) is to websites/webapps.
6
+ #
7
+ # The name (and concept) was shamelessly stolen from Jason R. Clark's
8
+ # "Testing the Multiverse" talk at Ruby on Ales 2015. Here's to hoping that
9
+ # we, as a Ruby programming community, can get a headstart on a command-line
10
+ # app framework *before* the Puma-Unicorn Wars ravage the Earth.
11
+ module Bales
12
+ ##
13
+ # Base class for Bales apps. Your command-line program should create a
14
+ # subclass of this, then call said subclass' #parse_and_run instance
15
+ # method, like so:
16
+ #
17
+ # ```ruby
18
+ # class MyApp::Application < Bales::Application
19
+ # # insert customizations here
20
+ # end
21
+ #
22
+ # MyApp::Application.parse_and_run
23
+ # ```
24
+ class Application
25
+ ##
26
+ # Runs the specified command (should be a valid class; preferably, should
27
+ # be a subclass of Bales::Command). Takes a list of positional args
28
+ # followed by named options.
29
+ def self.run(command=Bales::Command::Help, *args, **opts)
30
+ command.run *args, **opts
31
+ end
32
+
33
+ ##
34
+ # Parses ARGV (or some other array if you specify one), returning the
35
+ # class of the identified command, a hash containing the passed-in
36
+ # options, and a list of any remaining arguments
37
+ def self.parse(argv=ARGV)
38
+ command, result = parse_command_name argv.dup
39
+ opts, args = command.parse_opts result
40
+ return command, args, opts
41
+ end
42
+
43
+ ##
44
+ # Parses ARGV (or some other array if you specify one) for a command to
45
+ # run and its arguments/options, then runs the command.
46
+ def self.parse_and_run(argv=ARGV)
47
+ command, args, opts = parse argv
48
+ run command, *args, **opts
49
+ end
50
+
51
+ private
52
+
53
+ def parse_command_name(argv)
54
+ command_name_parts = []
55
+ argv.each do |arg|
56
+ last if arg.match(/^-/)
57
+ test = args_to_constant [*command_name_parts, arg]
58
+ if eval("defined? #{test}") == "constant"
59
+ command_name_parts.push argv.shift
60
+ else
61
+ last
62
+ end
63
+ end
64
+ command = args_to_constant [*command_name_parts, arg]
65
+ command, argv
66
+ end
67
+
68
+ def args_to_constant(argv)
69
+ result = argv.dup
70
+ result.map! do |arg|
71
+ arg.capitalize
72
+ arg.gsub('-','_').split('_').map { |e| e.capitalize}.join
73
+ end
74
+ result.join('::')
75
+ end
76
+ end
77
+
78
+ ##
79
+ # Base class for all Bales commands. Subclass this class to create your
80
+ # own command, like so:
81
+ #
82
+ # ```ruby
83
+ # class MyApp::Command::Hello < Bales::Command
84
+ # def self.run(*args, **opts)
85
+ # puts "Hello, world!"
86
+ # end
87
+ # end # produces a `my-app hello` command that prints "Hello, world!"
88
+ # ```
89
+ #
90
+ # Note that the above will accept any number of arguments (including none
91
+ # at all!). If you want to change this behavior, change `self.run`'s
92
+ # signature, like so:
93
+ #
94
+ # ```ruby
95
+ # class MyApp::Command::Smack < Bales::Command
96
+ # def self.run(target, **opts)
97
+ # puts "#{target} has been smacked with a large trout"
98
+ # end
99
+ # end
100
+ # ```
101
+ #
102
+ # Subcommands are automatically derived from namespacing, like so:
103
+ #
104
+ # ```ruby
105
+ # class MyApp::Command::Foo::Bar < Bales::Command
106
+ # def self.run(*args, **opts)
107
+ # # ...
108
+ # end
109
+ # end # produces `my-app foo bar`
110
+ # ```
111
+ #
112
+ # Camel-cased command classes can be accessed using either hyphenation or
113
+ # underscores, like so:
114
+ #
115
+ # ```ruby
116
+ # class MyApp::Command::FooBarBaz < Bales::Command
117
+ # # ...
118
+ # end
119
+ # # valid result: "my-app foo-bar-baz"
120
+ # # also valid: "my-app foo_bar_baz"
121
+ # ```
122
+ class Command
123
+ @options = {}
124
+
125
+ ##
126
+ # Runs the command with the provided list of arguments and named options.
127
+ # Your command should override this method (which by default does
128
+ # nothing), since this is the method that `Bales::Application.run` calls
129
+ # in order to actually run your command.
130
+ #
131
+ # For example:
132
+ #
133
+ # ```ruby
134
+ # class MyApp::Command::Hello < Bales::Command
135
+ # def self.run(*args, **opts)
136
+ # puts "Hello, world!"
137
+ # end
138
+ # end
139
+ # ```
140
+ def self.run(*args, **opts)
141
+ end
142
+
143
+ ##
144
+ # Defines a named option that the command will accept, along with some
145
+ # named arguments:
146
+ #
147
+ # `:short_form` (optional)
148
+ # : A shorthand flag to use for the option (like `-v`). This should be a
149
+ # string, like `"-v"`.
150
+ #
151
+ # `:long_form` (optional)
152
+ # : A longhand flag to use for the option (like `--verbose`). This is
153
+ # derived from the name of the option if not specified. This should be
154
+ # a string, like `"--verbose"`
155
+ #
156
+ # `:type` (optional)
157
+ # : The type that this option represents. Defaults to `TrueClass` if
158
+ # `:arg` is not specified; else, defaults to `String`. This must be a
159
+ # valid class name.
160
+ #
161
+ # A special note on boolean options: if you want your boolean to
162
+ # default to `true`, set `:type` to `TrueClass`. Likewise, if you want
163
+ # it to default to `false`, set `:type` to `FalseClass`.
164
+ #
165
+ # `:arg` (required unless `:type` is `TrueClass` or `FalseClass`)
166
+ # : The name of the argument this option accepts. This must not be
167
+ # defined if `:type` is either `TrueClass` or `FalseClass`; for all
168
+ # other types, this must be specified. As mentioned above, though, if
169
+ # `:type` is unspecified, the existence of `:arg` then determines
170
+ # whether the option's `:type` should default to `TrueClass` or
171
+ # `String`. This should be a symbol, like `:level`.
172
+ #
173
+ # Aside from the hash of option-options, `option` takes a single `name`
174
+ # argument, which should be a symbol representing the name of the option
175
+ # to be set, like `:verbose`.
176
+ def self.option(name, **opts)
177
+ name = name.to_sym
178
+ opts[:long_form] ||= "--#{name.to_s}".gsub("_","-")
179
+
180
+ unless opts[:type].class == Class
181
+ raise ArgumentError, ":type option should be a valid class"
182
+ end
183
+
184
+ if opts[:type] == TrueClass or opts[:type] == FalseClass
185
+ raise ArgumentError, ":arg in boolean opt" unless opts[:arg].nil?
186
+ else
187
+ raise ArgumentError, "missing :arg" if opts[:arg].nil?
188
+ end
189
+
190
+ @options[name] = opts
191
+ end
192
+
193
+ ##
194
+ # Takes an ARGV-like array and returns a hash of options and what's left
195
+ # of the original array. This is rarely needed for normal use, but is
196
+ # an integral part of how a Bales::Application parses the ARGV it
197
+ # receives.
198
+ #
199
+ # Normally, this should be perfectly fine to leave alone, but if you
200
+ # prefer to define your own parsing method (e.g. if you want to specify
201
+ # an alternative format for command-line options, or you are otherwise
202
+ # dissatisfied with the default approach of wrapping OptionParser), this
203
+ # is the method you'd want to override.
204
+ def self.parse_opts(argv)
205
+ optparser = OptionParser.new
206
+ result = {}
207
+ @options.each do |name, opts|
208
+ result[name] = opts[:default]
209
+ parser_args = []
210
+ parser_args.push opts[:short_form]
211
+ parser_args.push opts[:long_form]
212
+ unless opts[:type] == TrueClass or opts[:type] == FalseClass
213
+ parser_args.push opts[:type]
214
+ end
215
+ parser_args.push opts[:description]
216
+
217
+ if opts[:type] == FalseClass
218
+ optparser.on(*parser_args) do |value|
219
+ result[name] = !value
220
+ end
221
+ else
222
+ optparser.on(*parser_args) do |value|
223
+ result[name] = value
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ class Command::Help < Command
231
+ def self.run(*args, **opts)
232
+ puts "This will someday output some help text"
233
+
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,196 @@
1
+ require 'optparse'
2
+
3
+ ##
4
+ # Base class for all Bales commands. Subclass this class to create your
5
+ # own command, like so:
6
+ #
7
+ # ```ruby
8
+ # class MyApp::Command::Hello < Bales::Command
9
+ # def self.run(*args, **opts)
10
+ # puts "Hello, world!"
11
+ # end
12
+ # end # produces a `my-app hello` command that prints "Hello, world!"
13
+ # ```
14
+ #
15
+ # Note that the above will accept any number of arguments (including none
16
+ # at all!). If you want to change this behavior, change `self.run`'s
17
+ # signature, like so:
18
+ #
19
+ # ```ruby
20
+ # class MyApp::Command::Smack < Bales::Command
21
+ # def self.run(target, **opts)
22
+ # puts "#{target} has been smacked with a large trout"
23
+ # end
24
+ # end
25
+ # ```
26
+ #
27
+ # Subcommands are automatically derived from namespacing, like so:
28
+ #
29
+ # ```ruby
30
+ # class MyApp::Command::Foo::Bar < Bales::Command
31
+ # def self.run(*args, **opts)
32
+ # # ...
33
+ # end
34
+ # end # produces `my-app foo bar`
35
+ # ```
36
+ #
37
+ # Camel-cased command classes can be accessed using either hyphenation or
38
+ # underscores, like so:
39
+ #
40
+ # ```ruby
41
+ # class MyApp::Command::FooBarBaz < Bales::Command
42
+ # # ...
43
+ # end
44
+ # # valid result: "my-app foo-bar-baz"
45
+ # # also valid: "my-app foo_bar_baz"
46
+ # ```
47
+ module Bales
48
+ class Command
49
+ def self.options
50
+ @options ||= {}
51
+ @options
52
+ end
53
+ def self.options=(new)
54
+ @options = new
55
+ end
56
+
57
+ ##
58
+ # Assigns an action to this command. Said action is represented as a
59
+ # block, which should accept an array of arguments and a hash of options.
60
+ # For example:
61
+ #
62
+ # ```ruby
63
+ # class MyApp::Hello < Bales::Command
64
+ # action do |args, opts|
65
+ # puts "Hello, world!"
66
+ # end
67
+ # end
68
+ # ```
69
+ def self.action(&code)
70
+ @action = code
71
+ end
72
+
73
+ def self.run(*args, **opts)
74
+ @action.call(args, opts) unless @action.nil?
75
+ end
76
+
77
+ ##
78
+ # Defines a named option that the command will accept, along with some
79
+ # named arguments:
80
+ #
81
+ # `:short_form` (optional)
82
+ # : A shorthand flag to use for the option (like `-v`). This should be a
83
+ # string, like `"-v"`.
84
+ #
85
+ # `:long_form` (optional)
86
+ # : A longhand flag to use for the option (like `--verbose`). This is
87
+ # derived from the name of the option if not specified. This should be
88
+ # a string, like `"--verbose"`
89
+ #
90
+ # `:type` (optional)
91
+ # : The type that this option represents. Defaults to `TrueClass`.
92
+ # Should be a valid class name, like `String` or `Integer`
93
+ #
94
+ # A special note on boolean options: if you want your boolean to
95
+ # default to `true`, set `:type` to `TrueClass`. Likewise, if you want
96
+ # it to default to `false`, set `:type` to `FalseClass`.
97
+ #
98
+ # `:arg` (optional)
99
+ # : The name of the argument this option accepts. This should be a
100
+ # symbol (like :level) or `false` (if the option is a boolean flag).
101
+ # Defaults to the name of the option or (if the option's `:type` is
102
+ # `TrueClass` or `FalseClass`) `false`.
103
+ #
104
+ # If this is an array, and `:type` is set to `Enumerable` or some
105
+ # subclass thereof, this will instead be interpreted as a list of
106
+ # sample arguments during option parsing. It's recommended you set
107
+ # this accordingly if `:type` is `Enumerable` or any of its subclasses.
108
+ #
109
+ # `:required` (optional)
110
+ # : Whether or not the option is required. This should be a boolean
111
+ # (`true` or `false`). Default is `false`.
112
+ #
113
+ # Aside from the hash of option-options, `option` takes a single `name`
114
+ # argument, which should be a symbol representing the name of the option
115
+ # to be set, like `:verbose`.
116
+ def self.option(name, **opts)
117
+ name = name.to_sym
118
+ opts[:long_form] ||= "--#{name.to_s}".gsub("_","-")
119
+
120
+ opts[:type] = String if opts[:type].nil?
121
+
122
+ unless opts[:type].is_a? Class
123
+ raise ArgumentError, ":type option should be a valid class"
124
+ end
125
+
126
+ if (opts[:type].ancestors & [TrueClass, FalseClass]).empty?
127
+ opts[:arg] ||= name
128
+ end
129
+
130
+ opts[:default] = false if opts[:type].ancestors.include? TrueClass
131
+ opts[:default] = true if opts[:type].ancestors.include? FalseClass
132
+
133
+ result = options
134
+ result[name] = opts
135
+ options = result
136
+ end
137
+
138
+ ##
139
+ # Takes an ARGV-like array and returns a hash of options and what's left
140
+ # of the original array. This is rarely needed for normal use, but is
141
+ # an integral part of how a Bales::Application parses the ARGV it
142
+ # receives.
143
+ #
144
+ # Normally, this should be perfectly fine to leave alone, but if you
145
+ # prefer to define your own parsing method (e.g. if you want to specify
146
+ # an alternative format for command-line options, or you are otherwise
147
+ # dissatisfied with the default approach of wrapping OptionParser), this
148
+ # is the method you'd want to override.
149
+ def self.parse_opts(argv)
150
+ optparser = OptionParser.new
151
+ result = {}
152
+ options.each do |name, opts|
153
+ result[name] = opts[:default]
154
+ parser_args = []
155
+ parser_args.push opts[:short_form] if opts[:short_form]
156
+ if (opts[:type].ancestors & [TrueClass,FalseClass]).empty?
157
+ argstring = opts[:arg].to_s.upcase
158
+ if opts[:required]
159
+ parser_args.push "#{opts[:long_form]} #{argstring}"
160
+ else
161
+ parser_args.push "#{opts[:long_form]} [#{argstring}]"
162
+ end
163
+ parser_args.push opts[:type]
164
+ else
165
+ parser_args.push opts[:long_form]
166
+ end
167
+ parser_args.push opts[:description]
168
+
169
+ if opts[:type].ancestors.include? FalseClass
170
+ optparser.on(*parser_args) do
171
+ result[name] = false
172
+ end
173
+ elsif opts[:type].ancestors.include? TrueClass
174
+ optparser.on(*parser_args) do
175
+ result[name] = true
176
+ end
177
+ else
178
+ optparser.on(*parser_args) do |value|
179
+ result[name] = value
180
+ end
181
+ end
182
+ end
183
+
184
+ optparser.parse! argv
185
+ return result, argv
186
+ end
187
+ end
188
+ end
189
+
190
+ ##
191
+ # Default help command. You'll probably use your own...
192
+ class Bales::Command::Help < Bales::Command
193
+ action do |args, opts|
194
+ puts "This will someday output some help text"
195
+ end
196
+ end
@@ -0,0 +1,149 @@
1
+ require 'bales/command'
2
+
3
+ ##
4
+ # Base class for Bales apps. Your command-line program should create a
5
+ # subclass of this, then call said subclass' #parse_and_run instance
6
+ # method, like so:
7
+ #
8
+ # ```ruby
9
+ # class MyApp::Application < Bales::Application
10
+ # # insert customizations here
11
+ # end
12
+ #
13
+ # MyApp::Application.parse_and_run
14
+ # ```
15
+ module Bales
16
+ class Application
17
+ def self.default_command
18
+ @default_command ||= Bales::Command::Help
19
+ @default_command
20
+ end
21
+ def self.default_command=(command)
22
+ @default_command = command
23
+ end
24
+
25
+ ##
26
+ # Set or retrieve the application's version number. Defaults to "0.0.0".
27
+ def self.version(v="0.0.0")
28
+ @version ||= v
29
+ @version
30
+ end
31
+
32
+ ##
33
+ # Major version number. Assumes semantic versioning, but will work with
34
+ # any versioning scheme with at least major and minor version numbers.
35
+ def self.major_version
36
+ version.split('.')[0]
37
+ end
38
+
39
+ ##
40
+ # Minor version number. Assumes semantic versioning, but will work with
41
+ # any versioning scheme with at least major and minor version numbers.
42
+ def self.minor_version
43
+ version.split('.')[1]
44
+ end
45
+
46
+ ##
47
+ # Patch level. Assumes semantic versioning.
48
+ def self.patch_level
49
+ version.split('.')[2]
50
+ end
51
+
52
+ ##
53
+ # Runs the specified command (should be a valid class; preferably, should
54
+ # be a subclass of Bales::Command). Takes a list of positional args
55
+ # followed by named options.
56
+ def self.run(command, *args, **opts)
57
+ command.run *args, **opts
58
+ end
59
+
60
+ ##
61
+ # Parses ARGV (or some other array if you specify one), returning the
62
+ # class of the identified command, a hash containing the passed-in
63
+ # options, and a list of any remaining arguments
64
+ def self.parse(argv=ARGV)
65
+ command, result = parse_command_name argv.dup
66
+ command ||= default_command
67
+ opts, args = command.parse_opts result
68
+ return command, args, opts
69
+ end
70
+
71
+ ##
72
+ # Parses ARGV (or some other array if you specify one) for a command to
73
+ # run and its arguments/options, then runs the command.
74
+ def self.parse_and_run(argv=ARGV)
75
+ command, args, opts = parse argv
76
+ run command, *args, **opts
77
+ end
78
+
79
+ private
80
+
81
+ # def self.parse_command_name(argv)
82
+ # command_name_parts = [*constant_to_args(base_name), "command"]
83
+ # puts command_name_parts
84
+ # argv.each do |arg|
85
+ # break if arg.match(/^-/)
86
+ # begin
87
+ # test = args_to_constant [*command_name_parts, arg]
88
+ # rescue NameError
89
+ # break
90
+ # end
91
+ # if eval("defined? #{test}") == "constant"
92
+ # command_name_parts.push argv.shift
93
+ # else
94
+ # break
95
+ # end
96
+ # end
97
+ # command = args_to_constant [*command_name_parts]
98
+ # return command, argv
99
+ # end
100
+
101
+ def self.parse_command_name(argv)
102
+ command_name_parts = [*constant_to_args(base_name), "command"]
103
+ depth = 0
104
+ catch(:end) do
105
+ argv.each_with_index do |arg, i|
106
+ throw(:end) if arg.match(/^-/)
107
+ begin
108
+ test = args_to_constant [*command_name_parts, arg]
109
+ rescue NameError
110
+ throw(:end)
111
+ end
112
+
113
+ if eval("defined? #{test}") == "constant"
114
+ command_name_parts.push arg
115
+ depth += 1
116
+ else
117
+ throw(:end)
118
+ end
119
+ end
120
+ end
121
+ command = args_to_constant [*command_name_parts]
122
+ argv.shift depth
123
+ return command, argv
124
+ end
125
+
126
+ def self.base_name
127
+ result = self.name.split('::') - ["Application"]
128
+ eval result.join('::')
129
+ end
130
+
131
+ def self.constant_to_args(constant)
132
+ constant.name.split('::').map { |e| e.gsub!(/(.)([A-Z])/,'\1_\2') }
133
+ end
134
+
135
+ def self.args_to_constant(argv)
136
+ result = argv.dup
137
+ result.map! do |arg|
138
+ arg
139
+ .capitalize
140
+ .gsub('-','_')
141
+ .gsub(/\W/,'')
142
+ .split('_')
143
+ .map { |e| e.capitalize}
144
+ .join
145
+ end
146
+ eval result.join('::')
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,71 @@
1
+ ##
2
+ # Base class for Bales apps. Your command-line program should create a
3
+ # subclass of this, then call said subclass' #parse_and_run instance
4
+ # method, like so:
5
+ #
6
+ # ```ruby
7
+ # class MyApp::Application < Bales::Application
8
+ # # insert customizations here
9
+ # end
10
+ #
11
+ # MyApp::Application.parse_and_run
12
+ # ```
13
+ class Bales::Application
14
+ @default_command = Bales::Command::Help
15
+ def self.default(command=Bales::Command::Help)
16
+ @default_command = command
17
+ end
18
+
19
+ ##
20
+ # Runs the specified command (should be a valid class; preferably, should
21
+ # be a subclass of Bales::Command). Takes a list of positional args
22
+ # followed by named options.
23
+ def self.run(command, *args, **opts)
24
+ command.run *args, **opts
25
+ end
26
+
27
+ ##
28
+ # Parses ARGV (or some other array if you specify one), returning the
29
+ # class of the identified command, a hash containing the passed-in
30
+ # options, and a list of any remaining arguments
31
+ def self.parse(argv=ARGV)
32
+ command, result = parse_command_name argv.dup
33
+ command ||= @default_command
34
+ opts, args = command.parse_opts result
35
+ return command, args, opts
36
+ end
37
+
38
+ ##
39
+ # Parses ARGV (or some other array if you specify one) for a command to
40
+ # run and its arguments/options, then runs the command.
41
+ def self.parse_and_run(argv=ARGV)
42
+ command, args, opts = parse argv
43
+ run command, *args, **opts
44
+ end
45
+
46
+ private
47
+
48
+ def self.parse_command_name(argv)
49
+ command_name_parts = []
50
+ argv.each do |arg|
51
+ last if arg.match(/^-/)
52
+ test = args_to_constant [*command_name_parts, arg]
53
+ if eval("defined? #{test}") == "constant"
54
+ command_name_parts.push argv.shift
55
+ else
56
+ last
57
+ end
58
+ end
59
+ command = args_to_constant [*command_name_parts]
60
+ return command, argv
61
+ end
62
+
63
+ def self.args_to_constant(argv)
64
+ result = argv.dup
65
+ result.map! do |arg|
66
+ arg.capitalize
67
+ arg.gsub('-','_').split('_').map { |e| e.capitalize}.join
68
+ end
69
+ eval result.join('::')
70
+ end
71
+ end
@@ -0,0 +1,196 @@
1
+ require 'optparse'
2
+
3
+ ##
4
+ # Base class for all Bales commands. Subclass this class to create your
5
+ # own command, like so:
6
+ #
7
+ # ```ruby
8
+ # class MyApp::Command::Hello < Bales::Command
9
+ # def self.run(*args, **opts)
10
+ # puts "Hello, world!"
11
+ # end
12
+ # end # produces a `my-app hello` command that prints "Hello, world!"
13
+ # ```
14
+ #
15
+ # Note that the above will accept any number of arguments (including none
16
+ # at all!). If you want to change this behavior, change `self.run`'s
17
+ # signature, like so:
18
+ #
19
+ # ```ruby
20
+ # class MyApp::Command::Smack < Bales::Command
21
+ # def self.run(target, **opts)
22
+ # puts "#{target} has been smacked with a large trout"
23
+ # end
24
+ # end
25
+ # ```
26
+ #
27
+ # Subcommands are automatically derived from namespacing, like so:
28
+ #
29
+ # ```ruby
30
+ # class MyApp::Command::Foo::Bar < Bales::Command
31
+ # def self.run(*args, **opts)
32
+ # # ...
33
+ # end
34
+ # end # produces `my-app foo bar`
35
+ # ```
36
+ #
37
+ # Camel-cased command classes can be accessed using either hyphenation or
38
+ # underscores, like so:
39
+ #
40
+ # ```ruby
41
+ # class MyApp::Command::FooBarBaz < Bales::Command
42
+ # # ...
43
+ # end
44
+ # # valid result: "my-app foo-bar-baz"
45
+ # # also valid: "my-app foo_bar_baz"
46
+ # ```
47
+ module Bales
48
+ class Command
49
+ def self.options
50
+ @options ||= {}
51
+ @options
52
+ end
53
+ def self.options=(new)
54
+ @options = new
55
+ end
56
+
57
+ ##
58
+ # Assigns an action to this command. Said action is represented as a
59
+ # block, which should accept an array of arguments and a hash of options.
60
+ # For example:
61
+ #
62
+ # ```ruby
63
+ # class MyApp::Hello < Bales::Command
64
+ # action do |args, opts|
65
+ # puts "Hello, world!"
66
+ # end
67
+ # end
68
+ # ```
69
+ def self.action(&code)
70
+ @action = code
71
+ end
72
+
73
+ def self.run(*args, **opts)
74
+ @action.call(args, opts) unless @action.nil?
75
+ end
76
+
77
+ ##
78
+ # Defines a named option that the command will accept, along with some
79
+ # named arguments:
80
+ #
81
+ # `:short_form` (optional)
82
+ # : A shorthand flag to use for the option (like `-v`). This should be a
83
+ # string, like `"-v"`.
84
+ #
85
+ # `:long_form` (optional)
86
+ # : A longhand flag to use for the option (like `--verbose`). This is
87
+ # derived from the name of the option if not specified. This should be
88
+ # a string, like `"--verbose"`
89
+ #
90
+ # `:type` (optional)
91
+ # : The type that this option represents. Defaults to `TrueClass`.
92
+ # Should be a valid class name, like `String` or `Integer`
93
+ #
94
+ # A special note on boolean options: if you want your boolean to
95
+ # default to `true`, set `:type` to `TrueClass`. Likewise, if you want
96
+ # it to default to `false`, set `:type` to `FalseClass`.
97
+ #
98
+ # `:arg` (optional)
99
+ # : The name of the argument this option accepts. This should be a
100
+ # symbol (like :level) or `false` (if the option is a boolean flag).
101
+ # Defaults to the name of the option or (if the option's `:type` is
102
+ # `TrueClass` or `FalseClass`) `false`.
103
+ #
104
+ # If this is an array, and `:type` is set to `Enumerable` or some
105
+ # subclass thereof, this will instead be interpreted as a list of
106
+ # sample arguments during option parsing. It's recommended you set
107
+ # this accordingly if `:type` is `Enumerable` or any of its subclasses.
108
+ #
109
+ # `:required` (optional)
110
+ # : Whether or not the option is required. This should be a boolean
111
+ # (`true` or `false`). Default is `false`.
112
+ #
113
+ # Aside from the hash of option-options, `option` takes a single `name`
114
+ # argument, which should be a symbol representing the name of the option
115
+ # to be set, like `:verbose`.
116
+ def self.option(name, **opts)
117
+ name = name.to_sym
118
+ opts[:long_form] ||= "--#{name.to_s}".gsub("_","-")
119
+
120
+ opts[:type] = String if opts[:type].nil?
121
+
122
+ unless opts[:type].is_a? Class
123
+ raise ArgumentError, ":type option should be a valid class"
124
+ end
125
+
126
+ if (opts[:type].ancestors & [TrueClass, FalseClass]).empty?
127
+ opts[:arg] ||= name
128
+ end
129
+
130
+ opts[:default] = false if opts[:type].ancestors.include? TrueClass
131
+ opts[:default] = true if opts[:type].ancestors.include? FalseClass
132
+
133
+ result = options
134
+ result[name] = opts
135
+ options = result
136
+ end
137
+
138
+ ##
139
+ # Takes an ARGV-like array and returns a hash of options and what's left
140
+ # of the original array. This is rarely needed for normal use, but is
141
+ # an integral part of how a Bales::Application parses the ARGV it
142
+ # receives.
143
+ #
144
+ # Normally, this should be perfectly fine to leave alone, but if you
145
+ # prefer to define your own parsing method (e.g. if you want to specify
146
+ # an alternative format for command-line options, or you are otherwise
147
+ # dissatisfied with the default approach of wrapping OptionParser), this
148
+ # is the method you'd want to override.
149
+ def self.parse_opts(argv)
150
+ optparser = OptionParser.new
151
+ result = {}
152
+ options.each do |name, opts|
153
+ result[name] = opts[:default]
154
+ parser_args = []
155
+ parser_args.push opts[:short_form] if opts[:short_form]
156
+ if (opts[:type].ancestors & [TrueClass,FalseClass]).empty?
157
+ argstring = opts[:arg].to_s.upcase
158
+ if opts[:required]
159
+ parser_args.push "#{opts[:long_form]} #{argstring}"
160
+ else
161
+ parser_args.push "#{opts[:long_form]} [#{argstring}]"
162
+ end
163
+ parser_args.push opts[:type]
164
+ else
165
+ parser_args.push opts[:long_form]
166
+ end
167
+ parser_args.push opts[:description]
168
+
169
+ if opts[:type].ancestors.include? FalseClass
170
+ optparser.on(*parser_args) do
171
+ result[name] = false
172
+ end
173
+ elsif opts[:type].ancestors.include? TrueClass
174
+ optparser.on(*parser_args) do
175
+ result[name] = true
176
+ end
177
+ else
178
+ optparser.on(*parser_args) do |value|
179
+ result[name] = value
180
+ end
181
+ end
182
+ end
183
+
184
+ optparser.parse! argv
185
+ return result, argv
186
+ end
187
+ end
188
+ end
189
+
190
+ ##
191
+ # Default help command. You'll probably use your own...
192
+ class Bales::Command::Help < Bales::Command
193
+ action do |args, opts|
194
+ puts "This will someday output some help text"
195
+ end
196
+ end
@@ -0,0 +1,189 @@
1
+ ##
2
+ # Base class for all Bales commands. Subclass this class to create your
3
+ # own command, like so:
4
+ #
5
+ # ```ruby
6
+ # class MyApp::Command::Hello < Bales::Command
7
+ # def self.run(*args, **opts)
8
+ # puts "Hello, world!"
9
+ # end
10
+ # end # produces a `my-app hello` command that prints "Hello, world!"
11
+ # ```
12
+ #
13
+ # Note that the above will accept any number of arguments (including none
14
+ # at all!). If you want to change this behavior, change `self.run`'s
15
+ # signature, like so:
16
+ #
17
+ # ```ruby
18
+ # class MyApp::Command::Smack < Bales::Command
19
+ # def self.run(target, **opts)
20
+ # puts "#{target} has been smacked with a large trout"
21
+ # end
22
+ # end
23
+ # ```
24
+ #
25
+ # Subcommands are automatically derived from namespacing, like so:
26
+ #
27
+ # ```ruby
28
+ # class MyApp::Command::Foo::Bar < Bales::Command
29
+ # def self.run(*args, **opts)
30
+ # # ...
31
+ # end
32
+ # end # produces `my-app foo bar`
33
+ # ```
34
+ #
35
+ # Camel-cased command classes can be accessed using either hyphenation or
36
+ # underscores, like so:
37
+ #
38
+ # ```ruby
39
+ # class MyApp::Command::FooBarBaz < Bales::Command
40
+ # # ...
41
+ # end
42
+ # # valid result: "my-app foo-bar-baz"
43
+ # # also valid: "my-app foo_bar_baz"
44
+ # ```
45
+ class Bales::Command
46
+ @options = {}
47
+ def self.options
48
+ @options
49
+ end
50
+ def self.options=(new)
51
+ @options = new
52
+ end
53
+
54
+ ##
55
+ # Assigns an action to this command. Said action is represented as a
56
+ # block, which should accept an array of arguments and a hash of options.
57
+ # For example:
58
+ #
59
+ # ```ruby
60
+ # class MyApp::Hello < Bales::Command
61
+ # action do |args, opts|
62
+ # puts "Hello, world!"
63
+ # end
64
+ # end
65
+ # ```
66
+ def self.action(&code)
67
+ @action = code
68
+ end
69
+
70
+ def self.run(*args, **opts)
71
+ @action.call(args, opts)
72
+ end
73
+
74
+ ##
75
+ # Defines a named option that the command will accept, along with some
76
+ # named arguments:
77
+ #
78
+ # `:short_form` (optional)
79
+ # : A shorthand flag to use for the option (like `-v`). This should be a
80
+ # string, like `"-v"`.
81
+ #
82
+ # `:long_form` (optional)
83
+ # : A longhand flag to use for the option (like `--verbose`). This is
84
+ # derived from the name of the option if not specified. This should be
85
+ # a string, like `"--verbose"`
86
+ #
87
+ # `:type` (optional)
88
+ # : The type that this option represents. Defaults to `TrueClass`.
89
+ # Should be a valid class name, like `String` or `Integer`
90
+ #
91
+ # A special note on boolean options: if you want your boolean to
92
+ # default to `true`, set `:type` to `TrueClass`. Likewise, if you want
93
+ # it to default to `false`, set `:type` to `FalseClass`.
94
+ #
95
+ # `:arg` (optional)
96
+ # : The name of the argument this option accepts. This should be a
97
+ # symbol (like :level) or `false` (if the option is a boolean flag).
98
+ # Defaults to the name of the option or (if the option's `:type` is
99
+ # `TrueClass` or `FalseClass`) `false`.
100
+ #
101
+ # If this is an array, and `:type` is set to `Enumerable` or some
102
+ # subclass thereof, this will instead be interpreted as a list of
103
+ # sample arguments during option parsing. It's recommended you set
104
+ # this accordingly if `:type` is `Enumerable` or any of its subclasses.
105
+ #
106
+ # `:required` (optional)
107
+ # : Whether or not the option is required. This should be a boolean
108
+ # (`true` or `false`). Default is `false`.
109
+ #
110
+ # Aside from the hash of option-options, `option` takes a single `name`
111
+ # argument, which should be a symbol representing the name of the option
112
+ # to be set, like `:verbose`.
113
+ def self.option(name, **opts)
114
+ name = name.to_sym
115
+ opts[:long_form] ||= "--#{name.to_s}".gsub("_","-")
116
+
117
+ unless opts[:type].is_a? Class
118
+ raise ArgumentError, ":type option should be a valid class"
119
+ end
120
+
121
+ unless opts[:type].is_a?(TrueClass) or opts[:type].is_a?(FalseClass)
122
+ opts[:arg] ||= name
123
+ end
124
+
125
+ # if opts[:type] == TrueClass or opts[:type] == FalseClass
126
+ # raise ArgumentError, ":arg in boolean opt" unless opts[:arg].nil?
127
+ # else
128
+ # raise ArgumentError, "missing :arg" if opts[:arg].nil?
129
+ # end
130
+
131
+ result = {}
132
+ result[name] = opts
133
+ self.options = result
134
+ end
135
+
136
+ ##
137
+ # Takes an ARGV-like array and returns a hash of options and what's left
138
+ # of the original array. This is rarely needed for normal use, but is
139
+ # an integral part of how a Bales::Application parses the ARGV it
140
+ # receives.
141
+ #
142
+ # Normally, this should be perfectly fine to leave alone, but if you
143
+ # prefer to define your own parsing method (e.g. if you want to specify
144
+ # an alternative format for command-line options, or you are otherwise
145
+ # dissatisfied with the default approach of wrapping OptionParser), this
146
+ # is the method you'd want to override.
147
+ def self.parse_opts(argv)
148
+ optparser = OptionParser.new
149
+ result = {}
150
+ @options.each do |name, opts|
151
+ result[name] = opts[:default]
152
+ parser_args = []
153
+ parser_args.push opts[:short_form]
154
+ if opts[:type].is_a?(TrueClass) or opts[:type].is_a?(FalseClass)
155
+ parser_args.push opts[:long_form]
156
+ else
157
+ argstring = opts[:arg].to_s.upcase
158
+ if opts[:required]
159
+ parser_args.push "#{opts[:long_form]} #{argstring}"
160
+ else
161
+ parser_args.push "#{opts[:long_form]} [#{argstring}]"
162
+ parser_args.push opts[:type]
163
+ end
164
+ parser_args.push opts[:description]
165
+
166
+ if opts[:type].is_a? FalseClass
167
+ optparser.on(*parser_args) do |value|
168
+ result[name] = !value
169
+ end
170
+ else
171
+ optparser.on(*parser_args) do |value|
172
+ result[name] = value
173
+ end
174
+ end
175
+ end
176
+
177
+ opt_parser.parse! argv
178
+ return result, argv
179
+ end
180
+ end
181
+ end
182
+
183
+ ##
184
+ # Default help command. You'll probably use your own...
185
+ class Bales::Command::Help < Bales::Command
186
+ action do |args, opts|
187
+ puts "This will someday output some help text"
188
+ end
189
+ end
@@ -0,0 +1,3 @@
1
+ module Bales
2
+ VERSION="0.0.1"
3
+ end
@@ -0,0 +1,3 @@
1
+ module Bales
2
+ VERSION="0.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bales
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ryan S. Northrup
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-07-08 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A framework for building command-line applications
14
+ email:
15
+ - rnorthrup@newleaders.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - lib/bales.rb
22
+ - lib/bales.rb~
23
+ - lib/bales/#command.rb#
24
+ - lib/bales/application.rb
25
+ - lib/bales/application.rb~
26
+ - lib/bales/command.rb
27
+ - lib/bales/command.rb~
28
+ - lib/bales/version.rb
29
+ - lib/bales/version.rb~
30
+ homepage: http://github.com/YellowApple/bales
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 2.4.6
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: Ruby on Bales
54
+ test_files: []