bales 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +93 -0
- data/lib/bales.rb +27 -0
- data/lib/bales.rb~ +236 -0
- data/lib/bales/#command.rb# +196 -0
- data/lib/bales/application.rb +149 -0
- data/lib/bales/application.rb~ +71 -0
- data/lib/bales/command.rb +196 -0
- data/lib/bales/command.rb~ +189 -0
- data/lib/bales/version.rb +3 -0
- data/lib/bales/version.rb~ +3 -0
- metadata +54 -0
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
|
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: []
|