bales 0.0.4 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +84 -34
- data/lib/bales/application.rb +31 -13
- data/lib/bales/command.rb +62 -0
- data/lib/bales/command/help.rb +12 -2
- data/lib/bales/version.rb +1 -1
- metadata +7 -13
- data/lib/bales.rb~ +0 -236
- data/lib/bales/application.rb~ +0 -71
- data/lib/bales/command.rb~ +0 -189
- data/lib/bales/command/help.rb~ +0 -37
- data/lib/bales/version.rb~ +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b6ada6a857baba9d741859d2ec38d4992e6b9e4b409dd3bb7d0aacce7a6b129b
|
4
|
+
data.tar.gz: 1cbc31b944a5bd783aa563d36477bc15bcabaa5a44d7d6a7f03a586d7d02fd3e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c4b688015b19d0b11a110729ec6b955e6c11e2ca3cf01f2bec922a9aa03203798bd34fe1efb84f599c89e9cc235939d19e812057348b508b6abf1ff230d7b93
|
7
|
+
data.tar.gz: 1c52425d3d50315b2fcf93d78a603bc47df4a2afa083a4646c92edfad7289fb0e7f11e4e6b421035719cae1a2b81267035581404ebfda511241e2145b8b65a84
|
data/README.md
CHANGED
@@ -26,9 +26,8 @@ module SimpleApp
|
|
26
26
|
short_form: '-2',
|
27
27
|
type: String
|
28
28
|
|
29
|
-
action do |
|
30
|
-
|
31
|
-
puts "Hello, #{opts[:recipient]}!"
|
29
|
+
action do |recipient: "world"|
|
30
|
+
puts "Hello, #{recipient}!"
|
32
31
|
end
|
33
32
|
|
34
33
|
# Subcommand
|
@@ -39,13 +38,13 @@ module SimpleApp
|
|
39
38
|
short_form: '-w',
|
40
39
|
long_form: '--with'
|
41
40
|
|
42
|
-
action do
|
43
|
-
suffix =
|
41
|
+
action do |*victims, weapon: nil|
|
42
|
+
suffix = weapon ? " with a #{weapon}" : ""
|
44
43
|
|
45
44
|
if victims.none?
|
46
45
|
puts "You have been smacked#{suffix}."
|
47
46
|
else
|
48
|
-
puts "#{
|
47
|
+
victims.each {|v| puts "#{v} has been smacked#{suffix}."}
|
49
48
|
end
|
50
49
|
end
|
51
50
|
end
|
@@ -53,6 +52,13 @@ module SimpleApp
|
|
53
52
|
# Specify subcommand's parent class
|
54
53
|
command "help", parent: Bales::Command::Help
|
55
54
|
|
55
|
+
# Subsubcommands!
|
56
|
+
command "smack with" do
|
57
|
+
action do |weapon, *victims|
|
58
|
+
SimpleApp::Command::Smack.run(*victims, weapon: weapon)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
56
62
|
# This is what makes the app actually run!
|
57
63
|
parse_and_run
|
58
64
|
end
|
@@ -61,7 +67,8 @@ end
|
|
61
67
|
SimpleApp::Application.parse_and_run
|
62
68
|
```
|
63
69
|
|
64
|
-
And like this (assuming the above script lives in
|
70
|
+
And like this (assuming the above script lives in
|
71
|
+
`/usr/local/bin/simple-app`)!
|
65
72
|
|
66
73
|
```
|
67
74
|
$ simple-app
|
@@ -79,59 +86,102 @@ Bruce has been smacked.
|
|
79
86
|
Bruce has been smacked.
|
80
87
|
$ simple-app smack Bruce --with fish
|
81
88
|
Bruce has been smacked with a fish.
|
89
|
+
$ simple-app smack with fish Bruce
|
90
|
+
Bruce has been smacked with a fish.
|
82
91
|
```
|
83
92
|
|
84
93
|
## So how does it work?
|
85
94
|
|
86
95
|
* Come up with a name for your app, like `MyApp`
|
87
|
-
|
96
|
+
|
97
|
+
* Create an `Application` class under that namespace which inherits
|
98
|
+
from `Bales::Application`
|
99
|
+
|
88
100
|
* Use the DSL (or define classes manually, if that's your thing)
|
89
101
|
|
90
|
-
Basically, a Bales app is just a bunch of classes with some fairy dust
|
102
|
+
Basically, a Bales app is just a bunch of classes with some fairy dust
|
103
|
+
that turns them into runnable commands. Bales will check the
|
104
|
+
namespace that your subclass of `Bales::Application` lives in for a
|
105
|
+
`Command` namespace, then search there for available commands.
|
91
106
|
|
92
107
|
The application has a few available DSL-ish functions for you to play with.
|
93
108
|
|
94
|
-
* `version`: sets your app's version number. If you use semantic
|
95
|
-
|
109
|
+
* `version`: sets your app's version number. If you use semantic
|
110
|
+
versioning, you can query this with the `major_version`,
|
111
|
+
`minor_version`, and `patch_level` class methods.
|
96
112
|
|
97
|
-
|
113
|
+
* `command "foo" { ... }`: defines a subcommand called "foo", which
|
114
|
+
turns into a class called `MyApp::Command::Foo` (if you picked the
|
115
|
+
name `MyApp` above). If you provide a block, said block will be
|
116
|
+
evaluated in the class' context (see below for things you can do in
|
117
|
+
said context).
|
98
118
|
|
99
|
-
*
|
100
|
-
* `:type`: a valid Ruby class, like `String`. For a boolean, you should provide either `TrueClass` or `FalseClass`, which - when set - will set the option in question to `true` or `false` (respectively).
|
101
|
-
* `:short_form`: a short flag, like `'-v'`. You must specify this if you want a short flag.
|
102
|
-
* `:long_form`: a long flag, like `'--verbose'`. This will be created from the option's name if you don't override it here.
|
103
|
-
* `:description`: a quick description of the option, like `"Whether or not to be verbose"`.
|
104
|
-
* `action`: defines what the command should do when it's called. This is provided in the form of a block. Said block should accept two arguments (an array of arguments and a hash of options), though you don't *have* to name them with pipes and stuff if you know that your command won't take any arguments or options.
|
105
|
-
* `description`: sets a long description of what your command does. Should be a string.
|
106
|
-
* `summary`: sets a short description of what your command does. Should be a string. Should also be shorter than `:description`, though this isn't strictly necessary.
|
119
|
+
Meanwhile, commands *also* have some DSL-ish functions to play around with.
|
107
120
|
|
108
|
-
|
121
|
+
* `option`: defines a command-line option, like `--verbose` or `-f` or
|
122
|
+
something. It takes the name of the option (which becomes a key in
|
123
|
+
your command's options hash) and some named parameters:
|
124
|
+
|
125
|
+
* `:type`: a valid Ruby class, like `String`. For a boolean, you
|
126
|
+
should provide either `TrueClass` or `FalseClass`, which - when
|
127
|
+
set - will set the option in question to `true` or `false`
|
128
|
+
(respectively).
|
129
|
+
|
130
|
+
* `:short_form`: a short flag, like `'-v'`. You must specify this
|
131
|
+
if you want a short flag.
|
132
|
+
|
133
|
+
* `:long_form`: a long flag, like `'--verbose'`. This will be
|
134
|
+
created from the option's name if you don't override it here.
|
135
|
+
|
136
|
+
* `:description`: a quick description of the option, like `"Whether
|
137
|
+
or not to be verbose"`.
|
138
|
+
|
139
|
+
* `action`: defines what the command should do when it's called. This
|
140
|
+
is provided in the form of a block. Said block should accept two
|
141
|
+
arguments (an array of arguments and a hash of options), though you
|
142
|
+
don't *have* to name them with pipes and stuff if you know that your
|
143
|
+
command won't take any arguments or options.
|
144
|
+
|
145
|
+
* `description`: sets a long description of what your command does.
|
146
|
+
Should be a string.
|
147
|
+
|
148
|
+
* `summary`: sets a short description of what your command does.
|
149
|
+
Should be a string. Should also be shorter than `:description`,
|
150
|
+
though this isn't strictly necessary.
|
151
|
+
|
152
|
+
Some of the command functions (`option`, `action`, `description`,
|
153
|
+
`summary`) can also be used from within the application class; doing
|
154
|
+
so will define and configure a "root command", which is what is run if
|
155
|
+
you run your app without any arguments.
|
109
156
|
|
110
157
|
## What can this thing already do?
|
111
158
|
|
112
159
|
* Create a working command-line app
|
113
|
-
|
160
|
+
|
161
|
+
* Automatically produce subcommands (recursively, in fact) based on
|
162
|
+
the namespaces of the corresponding `Bales::Command` subclasses
|
163
|
+
|
114
164
|
* Provide a DSL defining commands and options
|
115
165
|
|
116
166
|
## What might this thing someday do in the future?
|
117
167
|
|
118
168
|
* Provide some helpers to wrap things like HighLine, curses, etc.
|
119
|
-
|
169
|
+
|
170
|
+
* Provide some additional flexibility in how options are specified
|
171
|
+
without requiring users to completely reimplement a command's option
|
172
|
+
parsing functions
|
120
173
|
|
121
174
|
## What kind of a silly name is "Bales", anyway?
|
122
175
|
|
123
|
-
It's shamelessly stolen^H^H^H^H^H^Hborrowed from Jason R. Clark's
|
176
|
+
It's shamelessly stolen^H^H^H^H^H^Hborrowed from Jason R. Clark's
|
177
|
+
"Testing the Multiverse" talk at Ruby on Ales 2015 (which, if you
|
178
|
+
haven't watched, you [totally
|
179
|
+
should](http://confreaks.tv/videos/roa2015-testing-the-multiverse)).
|
180
|
+
Sorry, Jason. Hope you don't mind.
|
124
181
|
|
125
|
-
Ironically enough, despite ripping off the name from a talk about Ruby
|
182
|
+
Ironically enough, despite ripping off the name from a talk about Ruby
|
183
|
+
testing, Bales currently lacks any formal test suite. Hm...
|
126
184
|
|
127
185
|
## What's the license?
|
128
186
|
|
129
|
-
MIT License
|
130
|
-
|
131
|
-
Copyright (c) 2015 Ryan S. Northrup
|
132
|
-
|
133
|
-
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:
|
134
|
-
|
135
|
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
136
|
-
|
137
|
-
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.
|
187
|
+
MIT License; see COPYING for details.
|
data/lib/bales/application.rb
CHANGED
@@ -45,7 +45,7 @@ module Bales
|
|
45
45
|
end
|
46
46
|
|
47
47
|
##
|
48
|
-
# Set or retrieve the application's banner
|
48
|
+
# Set or retrieve the application's banner
|
49
49
|
def self.banner(text=nil)
|
50
50
|
root_command.banner(text) unless text.nil?
|
51
51
|
root_command.banner
|
@@ -58,6 +58,12 @@ module Bales
|
|
58
58
|
root_command.description
|
59
59
|
end
|
60
60
|
|
61
|
+
##
|
62
|
+
# Alias for +description+
|
63
|
+
def self.desc(text=nil)
|
64
|
+
self.description(text)
|
65
|
+
end
|
66
|
+
|
61
67
|
##
|
62
68
|
# Set or retrieve the application's summary
|
63
69
|
def self.summary(text=nil)
|
@@ -124,21 +130,15 @@ module Bales
|
|
124
130
|
command ||= default_command
|
125
131
|
opts, args = command.parse_opts result
|
126
132
|
return command, args, opts
|
127
|
-
end
|
128
|
-
|
129
|
-
##
|
130
|
-
# Parses ARGV (or some other array if you specify one) for a
|
131
|
-
# command to run and its arguments/options, then runs the command.
|
132
|
-
def self.parse_and_run(argv=ARGV)
|
133
|
-
command, args, opts = parse argv
|
134
|
-
run command, *args, **opts
|
135
133
|
rescue OptionParser::MissingArgument
|
136
134
|
flag = $!.message.gsub("missing argument: ", '')
|
137
135
|
puts "#{$0}: error: option needs an argument (#{flag})"
|
136
|
+
puts "Usage: #{command.usage}"
|
138
137
|
exit!
|
139
138
|
rescue OptionParser::InvalidOption
|
140
139
|
flag = $!.message.gsub("invalid option: ", '')
|
141
140
|
puts "#{$0}: error: unknown option (#{flag})"
|
141
|
+
puts "Usage: #{command.usage}"
|
142
142
|
exit!
|
143
143
|
rescue ArgumentError
|
144
144
|
raise unless $!.message.match(/wrong number of arguments/)
|
@@ -148,9 +148,23 @@ module Bales
|
|
148
148
|
.gsub(")", '')
|
149
149
|
.split(" for ")
|
150
150
|
puts "#{$0}: error: expected #{expected} args but got #{received}"
|
151
|
+
puts "Usage: #{command.usage}"
|
151
152
|
exit!
|
152
153
|
end
|
153
154
|
|
155
|
+
##
|
156
|
+
# Parses ARGV (or some other array if you specify one) for a
|
157
|
+
# command to run and its arguments/options, then runs the command.
|
158
|
+
def self.parse_and_run(argv=ARGV)
|
159
|
+
command, args, opts = parse argv
|
160
|
+
# OptionParser includes nil values for missing options, so we
|
161
|
+
# need to make sure those don't clobber the command's defaults.
|
162
|
+
command.method(:run).parameters.select {|p| p[0] == :key}.map do |p|
|
163
|
+
opts.delete p[1] if opts[p[1]].nil?
|
164
|
+
end
|
165
|
+
run command, *args, **opts
|
166
|
+
end
|
167
|
+
|
154
168
|
private
|
155
169
|
|
156
170
|
def self.parse_command_name(argv)
|
@@ -165,10 +179,14 @@ module Bales
|
|
165
179
|
.map { |p| p.capitalize }
|
166
180
|
.join
|
167
181
|
name = "#{const}::#{part}"
|
168
|
-
|
169
|
-
const
|
170
|
-
|
171
|
-
|
182
|
+
begin
|
183
|
+
if const.const_defined? name
|
184
|
+
const = eval(name)
|
185
|
+
depth += 1
|
186
|
+
else
|
187
|
+
break
|
188
|
+
end
|
189
|
+
rescue NameError
|
172
190
|
break
|
173
191
|
end
|
174
192
|
end
|
data/lib/bales/command.rb
CHANGED
@@ -60,6 +60,12 @@ module Bales
|
|
60
60
|
const_set "DESCRIPTION", "(no description)"
|
61
61
|
end
|
62
62
|
|
63
|
+
##
|
64
|
+
# Alias for +description+
|
65
|
+
def self.desc(text=nil)
|
66
|
+
self.description(text)
|
67
|
+
end
|
68
|
+
|
63
69
|
##
|
64
70
|
# Get the command's summary, or set it if a string is passed to
|
65
71
|
# it.
|
@@ -87,6 +93,60 @@ module Bales
|
|
87
93
|
end
|
88
94
|
end
|
89
95
|
|
96
|
+
##
|
97
|
+
# Translates the command's class name to a more complete name
|
98
|
+
# passed on the command line, including the subcommand, all
|
99
|
+
# commands leading to it, and the root command.
|
100
|
+
def self.full_name
|
101
|
+
parts = self
|
102
|
+
.name
|
103
|
+
.split('::')
|
104
|
+
.map { |p| p.gsub(/(.)([A-Z])/, '\1-\2').downcase }
|
105
|
+
parts = parts[2..-1].join(' ')
|
106
|
+
"#{$0} #{parts}"
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Print the command's usage statement.
|
111
|
+
def self.usage
|
112
|
+
usage = []
|
113
|
+
|
114
|
+
# Name
|
115
|
+
usage << full_name
|
116
|
+
|
117
|
+
# Arguments
|
118
|
+
method(:run).parameters.each do |type, name|
|
119
|
+
# We don't handle keys and keyrests, since they're going to be
|
120
|
+
# taken care of once we start dealing with options.
|
121
|
+
case type
|
122
|
+
when :req then usage << "<#{name}>"
|
123
|
+
when :opt then usage << "[<#{name}>]"
|
124
|
+
when :rest then usage << "[<#{name}> ...]"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Options
|
129
|
+
options.each_pair do |opt, args|
|
130
|
+
case
|
131
|
+
when (args[:type] <= TrueClass or args[:type] <= FalseClass)
|
132
|
+
if args.key?(:short_form)
|
133
|
+
usage << "[(#{args[:long_form]}|#{args[:short_form]})]"
|
134
|
+
else
|
135
|
+
usage << "[#{args[:long_form]}]"
|
136
|
+
end
|
137
|
+
else
|
138
|
+
if args.key?(:short_form)
|
139
|
+
usage << "[(#{args[:long_form]}|#{args[:short_form]}) " \
|
140
|
+
"<#{args[:arg]}>]"
|
141
|
+
else
|
142
|
+
usage << "[#{args[:long_form]} <#{args[:arg]}>]"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
usage.join(' ')
|
148
|
+
end
|
149
|
+
|
90
150
|
##
|
91
151
|
# Creates a new subcommand of the current command. Identical in
|
92
152
|
# usage to +Bales::Application.command+, the only significant
|
@@ -203,6 +263,8 @@ module Bales
|
|
203
263
|
opts[:default] = false if opts[:type] <= TrueClass
|
204
264
|
opts[:default] = true if opts[:type] <= FalseClass
|
205
265
|
|
266
|
+
opts[:description] = opts[:desc] if opts.key?(:desc)
|
267
|
+
|
206
268
|
result = options
|
207
269
|
result[name] = opts
|
208
270
|
options = result
|
data/lib/bales/command/help.rb
CHANGED
@@ -14,6 +14,7 @@ class Bales::Command::Help < Bales::Command
|
|
14
14
|
end
|
15
15
|
|
16
16
|
print_summary(target)
|
17
|
+
print_usage(target)
|
17
18
|
print_options(target)
|
18
19
|
print_commands(target)
|
19
20
|
end
|
@@ -40,8 +41,13 @@ class Bales::Command::Help < Bales::Command
|
|
40
41
|
end
|
41
42
|
|
42
43
|
ns.constants
|
43
|
-
.select { |c|
|
44
|
-
|
44
|
+
.select { |c|
|
45
|
+
begin
|
46
|
+
ns.const_defined? "#{ns}::#{c}"
|
47
|
+
rescue NameError
|
48
|
+
false
|
49
|
+
end
|
50
|
+
}.select { |c| eval("#{ns}::#{c}").class == Class }
|
45
51
|
.select { |c| eval("#{ns}::#{c}") <= Bales::Command }
|
46
52
|
.map { |c| eval "#{ns}::#{c}" }
|
47
53
|
end
|
@@ -109,6 +115,10 @@ class Bales::Command::Help < Bales::Command
|
|
109
115
|
print "Description:\n#{command.description}\n\n"
|
110
116
|
end
|
111
117
|
|
118
|
+
def self.print_usage(command)
|
119
|
+
print "Usage: #{command.usage}\n\n"
|
120
|
+
end
|
121
|
+
|
112
122
|
def self.print_commands(namespace)
|
113
123
|
cmds = commands(namespace)
|
114
124
|
|
data/lib/bales/version.rb
CHANGED
metadata
CHANGED
@@ -1,38 +1,33 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bales
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan S. Northrup
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A framework for building command-line applications
|
14
14
|
email:
|
15
|
-
-
|
15
|
+
- northrup@yellowapple.us
|
16
16
|
executables: []
|
17
17
|
extensions: []
|
18
18
|
extra_rdoc_files: []
|
19
19
|
files:
|
20
20
|
- README.md
|
21
21
|
- lib/bales.rb
|
22
|
-
- lib/bales.rb~
|
23
22
|
- lib/bales/application.rb
|
24
|
-
- lib/bales/application.rb~
|
25
23
|
- lib/bales/command.rb
|
26
|
-
- lib/bales/command.rb~
|
27
24
|
- lib/bales/command/help.rb
|
28
|
-
- lib/bales/command/help.rb~
|
29
25
|
- lib/bales/version.rb
|
30
|
-
- lib/bales/version.rb~
|
31
26
|
homepage: http://github.com/YellowApple/bales
|
32
27
|
licenses:
|
33
28
|
- MIT
|
34
29
|
metadata: {}
|
35
|
-
post_install_message:
|
30
|
+
post_install_message:
|
36
31
|
rdoc_options: []
|
37
32
|
require_paths:
|
38
33
|
- lib
|
@@ -47,9 +42,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
42
|
- !ruby/object:Gem::Version
|
48
43
|
version: '0'
|
49
44
|
requirements: []
|
50
|
-
|
51
|
-
|
52
|
-
signing_key:
|
45
|
+
rubygems_version: 3.1.2
|
46
|
+
signing_key:
|
53
47
|
specification_version: 4
|
54
48
|
summary: Ruby on Bales
|
55
49
|
test_files: []
|
data/lib/bales.rb~
DELETED
@@ -1,236 +0,0 @@
|
|
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
|
data/lib/bales/application.rb~
DELETED
@@ -1,71 +0,0 @@
|
|
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
|
data/lib/bales/command.rb~
DELETED
@@ -1,189 +0,0 @@
|
|
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
|
data/lib/bales/command/help.rb~
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
##
|
2
|
-
# Prints help text for a given namespace
|
3
|
-
class Bales::Command::Help < Bales::Command
|
4
|
-
action do |args, opts|
|
5
|
-
puts "This will someday output some help text"
|
6
|
-
end
|
7
|
-
|
8
|
-
private
|
9
|
-
|
10
|
-
def commands(ns)
|
11
|
-
unless eval("defined? #{ns}") == "constant"
|
12
|
-
raise ArgumentError, "expected a constant, but got a #{ns.class}"
|
13
|
-
end
|
14
|
-
|
15
|
-
ns.constants
|
16
|
-
.select { |c| eval("#{ns}::#{c}") <= Bales::Command }
|
17
|
-
.map { |c| eval "#{ns}::#{c}" }
|
18
|
-
end
|
19
|
-
|
20
|
-
def format_option(name, opts, width=72)
|
21
|
-
long = "#{opts[:long_form]}"
|
22
|
-
if opts[:type] <= TrueClass or opts[:type] <= FalseClass
|
23
|
-
if opts[:required]
|
24
|
-
long << " #{opts[:arg]}"
|
25
|
-
else
|
26
|
-
long << " [#{opts[:arg]}]"
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
output = "#{name} (#{opts[:type]}): "
|
31
|
-
output << "#{opts[:short_form]} / " if opts[:short_form]
|
32
|
-
output << long
|
33
|
-
output << "\n"
|
34
|
-
output << opts[:description]
|
35
|
-
output
|
36
|
-
end
|
37
|
-
end
|
data/lib/bales/version.rb~
DELETED