bales 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +57 -28
- data/lib/bales/application.rb +147 -35
- data/lib/bales/command/help.rb +159 -0
- data/lib/bales/command/help.rb~ +37 -0
- data/lib/bales/command.rb +112 -71
- data/lib/bales/version.rb +1 -1
- data/lib/bales.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1fb40f18edf38481a779d05e661167b9a504577f
|
4
|
+
data.tar.gz: fdfb53af1b8ead967df753d0e03c332c10ffec3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 08f36ccf3b1ca492be4d60d7881add98f74e9f3732491e0041fe8280cb2e4d61b2ca9817a1086c3e6f5338bf2c535b5b6a04f49346552c3edda00fe0de8f41f7
|
7
|
+
data.tar.gz: 4f86ed0b34ad824ea2bcab12100c1bfaa2fa921d8a7a3213ba9fb805df1375221cf7954c4bef6182bb546c2eac5b5be08950d13f6a51ce901cb0fcd686726fd1
|
data/README.md
CHANGED
@@ -18,32 +18,43 @@ require 'bales'
|
|
18
18
|
module SimpleApp
|
19
19
|
class Application < Bales::Application
|
20
20
|
version "0.0.1"
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
description "Sample app"
|
22
|
+
|
23
|
+
# Default action
|
24
|
+
option :recipient,
|
25
|
+
long_form: '--to',
|
26
|
+
short_form: '-2',
|
27
|
+
type: String
|
28
|
+
|
29
|
+
action do |args, opts|
|
30
|
+
opts[:recipient] ||= "world"
|
31
|
+
puts "Hello, #{opts[:recipient]}!"
|
26
32
|
end
|
27
|
-
|
28
|
-
|
33
|
+
|
34
|
+
# Subcommand
|
35
|
+
command "smack" do
|
29
36
|
option :weapon,
|
30
37
|
type: String,
|
31
38
|
description: "Thing to smack with",
|
32
39
|
short_form: '-w',
|
33
40
|
long_form: '--with'
|
34
|
-
|
41
|
+
|
35
42
|
action do |victims, opts|
|
36
43
|
suffix = opts[:weapon] ? " with a #{opts[:weapon]}" : ""
|
37
|
-
|
44
|
+
|
38
45
|
if victims.none?
|
39
46
|
puts "You have been smacked#{suffix}."
|
40
47
|
else
|
41
|
-
|
42
|
-
puts "#{victim} has been smacked#{suffix}."
|
43
|
-
end
|
48
|
+
puts "#{victim} has been smacked#{suffix}."
|
44
49
|
end
|
45
50
|
end
|
46
51
|
end
|
52
|
+
|
53
|
+
# Specify subcommand's parent class
|
54
|
+
command "help", parent: Bales::Command::Help
|
55
|
+
|
56
|
+
# This is what makes the app actually run!
|
57
|
+
parse_and_run
|
47
58
|
end
|
48
59
|
end
|
49
60
|
|
@@ -53,34 +64,35 @@ SimpleApp::Application.parse_and_run
|
|
53
64
|
And like this (assuming the above script lives in `/usr/local/bin/simple-app`)!
|
54
65
|
|
55
66
|
```
|
67
|
+
$ simple-app
|
68
|
+
Hello, world!
|
69
|
+
$ simple-app -2 Bruce
|
70
|
+
Hello, Bruce!
|
71
|
+
$ simple-app --to Bruce
|
72
|
+
Hello, Bruce!
|
56
73
|
$ simple-app smack
|
57
74
|
You have been smacked.
|
58
|
-
$ simple-app smack
|
59
|
-
|
60
|
-
$ simple-app
|
61
|
-
|
62
|
-
|
63
|
-
$ simple-app smack
|
64
|
-
|
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.
|
75
|
+
$ simple-app smack Bruce
|
76
|
+
Bruce has been smacked.
|
77
|
+
$ simple-app smack Bruce Bruce
|
78
|
+
Bruce has been smacked.
|
79
|
+
Bruce has been smacked.
|
80
|
+
$ simple-app smack Bruce --with fish
|
81
|
+
Bruce has been smacked with a fish.
|
69
82
|
```
|
70
83
|
|
71
84
|
## So how does it work?
|
72
85
|
|
73
86
|
* Come up with a name for your app, like `MyApp`
|
74
87
|
* Create an `Application` class under that namespace which inherits from `Bales::Application`
|
75
|
-
*
|
76
|
-
* Give that `Command` an `action`, which will be what your application does by default if no valid subcommands are passed to it
|
77
|
-
* (Optional) Create one or more classes under the `MyApp::Command` namespace, inheriting from some subclass of `Bales::Command` (including the base command you defined previously), if you want some git-style or rails-style subcommands.
|
88
|
+
* Use the DSL (or define classes manually, if that's your thing)
|
78
89
|
|
79
90
|
Basically, a Bales app is just a bunch of classes with some fairy dust that turns them into runnable commands. Bales will check the namespace that your subclass of `Bales::Application` lives in for a `Command` namespace, then search there for available commands.
|
80
91
|
|
81
|
-
The application has
|
92
|
+
The application has a few available DSL-ish functions for you to play with.
|
82
93
|
|
83
94
|
* `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.
|
95
|
+
* `command "foo" { ... }`: defines a subcommand called "foo", which turns into a class called `MyApp::Command::Foo` (if you picked the name `MyApp` above). If you provide a block, said block will be evaluated in the class' context (see below for things you can do in said context).
|
84
96
|
|
85
97
|
Meanwhile, commands *also* have some DSL-ish functions to play around with.
|
86
98
|
|
@@ -90,11 +102,28 @@ Meanwhile, commands *also* have some DSL-ish functions to play around with.
|
|
90
102
|
* `:long_form`: a long flag, like `'--verbose'`. This will be created from the option's name if you don't override it here.
|
91
103
|
* `:description`: a quick description of the option, like `"Whether or not to be verbose"`.
|
92
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.
|
107
|
+
|
108
|
+
Some of the command functions (`option`, `action`, `description`, `summary`) can also be used from within the application class; doing so will define and configure a "root command", which is what is run if you run your app without any arguments.
|
93
109
|
|
94
|
-
## What
|
110
|
+
## What can this thing already do?
|
111
|
+
|
112
|
+
* Create a working command-line app
|
113
|
+
* Automatically produce subcommands (recursively, in fact) based on the namespaces of the corresponding `Bales::Command` subclasses
|
114
|
+
* Provide a DSL defining commands and options
|
115
|
+
|
116
|
+
## What might this thing someday do in the future?
|
117
|
+
|
118
|
+
* Provide some helpers to wrap things like HighLine, curses, etc.
|
119
|
+
* Provide some additional flexibility in how options are specified without requiring users to completely reimplement a command's option parsing functions
|
120
|
+
|
121
|
+
## What kind of a silly name is "Bales", anyway?
|
95
122
|
|
96
123
|
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.
|
97
124
|
|
125
|
+
Ironically enough, despite ripping off the name from a talk about Ruby testing, Bales currently lacks any formal test suite. Hm...
|
126
|
+
|
98
127
|
## What's the license?
|
99
128
|
|
100
129
|
MIT License
|
data/lib/bales/application.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'bales/command'
|
2
2
|
|
3
3
|
##
|
4
|
-
# Base class for Bales apps. Your command-line program should create
|
5
|
-
# subclass of this, then call said subclass' +#parse_and_run+
|
6
|
-
# method, like so:
|
4
|
+
# Base class for Bales apps. Your command-line program should create
|
5
|
+
# a subclass of this, then call said subclass' +#parse_and_run+
|
6
|
+
# instance method, like so:
|
7
7
|
#
|
8
8
|
# class MyApp::Application < Bales::Application
|
9
9
|
# # insert customizations here
|
@@ -12,23 +12,92 @@ require 'bales/command'
|
|
12
12
|
# MyApp::Application.parse_and_run
|
13
13
|
module Bales
|
14
14
|
class Application
|
15
|
+
def self.inherited(child) # :nodoc:
|
16
|
+
child
|
17
|
+
.base_name
|
18
|
+
.const_set("Command", Class.new(Bales::Command))
|
19
|
+
.const_set("Help", Class.new(Bales::Command::Help))
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Set or retrieve the application's version number.
|
24
|
+
def self.version(v=nil)
|
25
|
+
const_set("VERSION", v) unless v.nil?
|
26
|
+
const_set("VERSION", "0.0.0") if const_get("VERSION").nil?
|
27
|
+
const_get("VERSION")
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Define a command (specifically, a subclass of +Bales::Command+).
|
32
|
+
# Command should be a string corresponding to how the command will
|
33
|
+
# be invoked on the command-line; thus, a command with the class
|
34
|
+
# name +FooBar::Baz+ should be passed as "foo-bar baz".
|
35
|
+
def self.command(name=nil, **opts, &code)
|
36
|
+
const_name = "#{base_name.name}::Command"
|
37
|
+
opts[:parent] ||= Bales::Command
|
38
|
+
|
39
|
+
if eval("defined? #{const_name}") == "constant"
|
40
|
+
const = eval(const_name)
|
41
|
+
else
|
42
|
+
const = base_name.const_set('Command', Class.new(opts[:parent]))
|
43
|
+
end
|
44
|
+
|
45
|
+
unless name.nil?
|
46
|
+
name
|
47
|
+
.to_s
|
48
|
+
.split(' ')
|
49
|
+
.map { |p| p
|
50
|
+
.downcase
|
51
|
+
.gsub('_','-')
|
52
|
+
.split('-')
|
53
|
+
.map { |pp| pp.capitalize }
|
54
|
+
.join }
|
55
|
+
.each do |part|
|
56
|
+
name = "#{const.name}::#{part}"
|
57
|
+
if const.const_defined? name
|
58
|
+
const = eval(name)
|
59
|
+
else
|
60
|
+
const = const.const_set(part, Class.new(opts[:parent]))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
const.instance_eval(&code) if block_given?
|
66
|
+
end
|
67
|
+
|
15
68
|
##
|
16
|
-
# Set or retrieve the application's
|
17
|
-
def self.
|
18
|
-
|
19
|
-
|
69
|
+
# Set or retrieve the application's banner.
|
70
|
+
def self.banner(text=nil)
|
71
|
+
root_command.banner(text) unless text.nil?
|
72
|
+
root_command.banner
|
20
73
|
end
|
21
74
|
|
22
75
|
##
|
23
|
-
#
|
24
|
-
|
76
|
+
# Set or retrieve the application's description
|
77
|
+
def self.description(text=nil)
|
78
|
+
root_command.description(text) unless text.nil?
|
79
|
+
root_command.description
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Set or retrieve the application's summary
|
84
|
+
def self.summary(text=nil)
|
85
|
+
root_command.summary(text) unless text.nil?
|
86
|
+
root_command.summary
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Major version number. Assumes semantic versioning, but will
|
91
|
+
# work with any versioning scheme with at least major and minor
|
92
|
+
# version numbers.
|
25
93
|
def self.major_version
|
26
94
|
version.split('.')[0]
|
27
95
|
end
|
28
96
|
|
29
97
|
##
|
30
|
-
# Minor version number. Assumes semantic versioning, but will
|
31
|
-
# any versioning scheme with at least major and minor
|
98
|
+
# Minor version number. Assumes semantic versioning, but will
|
99
|
+
# work with any versioning scheme with at least major and minor
|
100
|
+
# version numbers.
|
32
101
|
def self.minor_version
|
33
102
|
version.split('.')[1]
|
34
103
|
end
|
@@ -40,17 +109,37 @@ module Bales
|
|
40
109
|
end
|
41
110
|
|
42
111
|
##
|
43
|
-
#
|
44
|
-
#
|
45
|
-
|
112
|
+
# Set an application-level option. See +Bales::Command+'s
|
113
|
+
# +option+ method for more details.
|
114
|
+
def self.option(name, **opts)
|
115
|
+
root_command.option(name, **opts)
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Set an application-level action. See +Bales::Command+'s
|
120
|
+
# +action+ method for more details.
|
121
|
+
def self.action(&code)
|
122
|
+
root_command.action(&code)
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
# Runs the specified command (should be a valid class; preferably,
|
127
|
+
# should be a subclass of +Bales::Command+, or should otherwise
|
128
|
+
# have a +.run+ class method that accepts a list of args and a
|
129
|
+
# hash of opts). Takes a list of positional args followed by
|
130
|
+
# named options.
|
46
131
|
def self.run(command, *args, **opts)
|
47
|
-
|
132
|
+
if opts.none?
|
133
|
+
command.run *args
|
134
|
+
else
|
135
|
+
command.run *args, **opts
|
136
|
+
end
|
48
137
|
end
|
49
138
|
|
50
139
|
##
|
51
|
-
# Parses ARGV (or some other array if you specify one), returning
|
52
|
-
# class of the identified command, a hash containing the
|
53
|
-
# options, and a list of any remaining arguments
|
140
|
+
# Parses ARGV (or some other array if you specify one), returning
|
141
|
+
# the class of the identified command, a hash containing the
|
142
|
+
# passed-in options, and a list of any remaining arguments
|
54
143
|
def self.parse(argv=ARGV)
|
55
144
|
command, result = parse_command_name argv.dup
|
56
145
|
command ||= default_command
|
@@ -59,38 +148,54 @@ module Bales
|
|
59
148
|
end
|
60
149
|
|
61
150
|
##
|
62
|
-
# Parses ARGV (or some other array if you specify one) for a
|
63
|
-
# run and its arguments/options, then runs the command.
|
151
|
+
# Parses ARGV (or some other array if you specify one) for a
|
152
|
+
# command to run and its arguments/options, then runs the command.
|
64
153
|
def self.parse_and_run(argv=ARGV)
|
65
154
|
command, args, opts = parse argv
|
66
155
|
run command, *args, **opts
|
156
|
+
rescue OptionParser::MissingArgument
|
157
|
+
flag = $!.message.gsub("missing argument: ", '')
|
158
|
+
puts "#{$0}: error: option needs an argument (#{flag})"
|
159
|
+
exit!
|
160
|
+
rescue OptionParser::InvalidOption
|
161
|
+
flag = $!.message.gsub("invalid option: ", '')
|
162
|
+
puts "#{$0}: error: unknown option (#{flag})"
|
163
|
+
exit!
|
164
|
+
rescue ArgumentError
|
165
|
+
raise unless $!.message.match(/wrong number of arguments/)
|
166
|
+
received, expected = $!
|
167
|
+
.message
|
168
|
+
.gsub("wrong number of arguments (", '')
|
169
|
+
.gsub(")", '')
|
170
|
+
.split(" for ")
|
171
|
+
puts "#{$0}: error: expected #{expected} args but got #{received}"
|
172
|
+
exit!
|
67
173
|
end
|
68
174
|
|
69
175
|
private
|
70
176
|
|
71
177
|
def self.parse_command_name(argv)
|
72
|
-
|
178
|
+
const = base_name::Command
|
73
179
|
depth = 0
|
74
|
-
catch(:end) do
|
75
|
-
argv.each_with_index do |arg, i|
|
76
|
-
throw(:end) if arg.match(/^-/)
|
77
|
-
begin
|
78
|
-
test = args_to_constant [*command_name_parts, arg]
|
79
|
-
rescue NameError
|
80
|
-
throw(:end)
|
81
|
-
end
|
82
180
|
|
83
|
-
|
84
|
-
|
181
|
+
argv.each do |arg|
|
182
|
+
part = arg
|
183
|
+
.downcase
|
184
|
+
.gsub('_','-')
|
185
|
+
.split('-')
|
186
|
+
.map { |p| p.capitalize }
|
187
|
+
.join
|
188
|
+
name = "#{const}::#{part}"
|
189
|
+
if const.const_defined? name
|
190
|
+
const = eval(name)
|
85
191
|
depth += 1
|
86
192
|
else
|
87
|
-
|
193
|
+
break
|
88
194
|
end
|
89
195
|
end
|
90
|
-
|
91
|
-
command = args_to_constant [*command_name_parts]
|
196
|
+
|
92
197
|
argv.shift depth
|
93
|
-
return
|
198
|
+
return const, argv
|
94
199
|
end
|
95
200
|
|
96
201
|
def self.base_name
|
@@ -98,6 +203,13 @@ module Bales
|
|
98
203
|
eval result.join('::')
|
99
204
|
end
|
100
205
|
|
206
|
+
def self.root_command
|
207
|
+
unless eval("defined? #{base_name}::Command") == "constant"
|
208
|
+
base_name.const_set "Command", Class.new(Bales::Command)
|
209
|
+
end
|
210
|
+
eval "#{base_name}::Command"
|
211
|
+
end
|
212
|
+
|
101
213
|
def self.constant_to_args(constant)
|
102
214
|
constant.name.split('::').map { |e| e.gsub!(/(.)([A-Z])/,'\1_\2') }
|
103
215
|
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
##
|
2
|
+
# Prints help text for a given namespace
|
3
|
+
class Bales::Command::Help < Bales::Command
|
4
|
+
summary "Print this help text"
|
5
|
+
action do |*args, **opts|
|
6
|
+
target = ''
|
7
|
+
if args.empty?
|
8
|
+
target = basename
|
9
|
+
elsif command?(args[0])
|
10
|
+
target = args[0].gsub('_','-').split('-').map { |p| p.capitalize }.join
|
11
|
+
target = eval "#{rootname}::Command::#{target}"
|
12
|
+
else
|
13
|
+
target = basename
|
14
|
+
end
|
15
|
+
|
16
|
+
print_summary(target)
|
17
|
+
print_options(target)
|
18
|
+
print_commands(target)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def self.basename(constant=self)
|
24
|
+
eval constant.name.split('::')[0..-2].join('::')
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.rootname(constant=self)
|
28
|
+
eval constant.name.split('::').first
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.command?(name)
|
32
|
+
test = name.gsub('_','-').split('-').map { |p| p.capitalize }.join
|
33
|
+
test = "#{rootname}::Command::#{test}"
|
34
|
+
eval("defined? #{test}") == "constant"
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.commands(ns)
|
38
|
+
unless eval("defined? #{ns}") == "constant"
|
39
|
+
raise ArgumentError, "expected a constant, but got a #{ns.class}"
|
40
|
+
end
|
41
|
+
|
42
|
+
ns.constants
|
43
|
+
.select { |c| ns.const_defined? "#{ns}::#{c}" }
|
44
|
+
.select { |c| eval("#{ns}::#{c}").class == Class }
|
45
|
+
.select { |c| eval("#{ns}::#{c}") <= Bales::Command }
|
46
|
+
.map { |c| eval "#{ns}::#{c}" }
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.format_option(name, opts, width=72)
|
50
|
+
long = "#{opts[:long_form]}"
|
51
|
+
if opts[:type] <= TrueClass or opts[:type] <= FalseClass
|
52
|
+
if opts[:required]
|
53
|
+
long << " #{opts[:arg]}"
|
54
|
+
else
|
55
|
+
long << " [#{opts[:arg]}]"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
output = "#{name} (#{opts[:type]}): "
|
60
|
+
output << "#{opts[:short_form]} / " if opts[:short_form]
|
61
|
+
output << long
|
62
|
+
output << "\n"
|
63
|
+
output << opts[:description]
|
64
|
+
output
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.print_options(command)
|
68
|
+
optstrings = command.options.map do |opt|
|
69
|
+
opt = opt[1]
|
70
|
+
result = ""
|
71
|
+
result += "#{opt[:short_form]}, " unless opt[:short_form].nil?
|
72
|
+
result += "#{opt[:long_form]}"
|
73
|
+
result += "=[#{opt[:arg]}]" unless opt[:arg].nil?
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
unless optstrings.none?
|
78
|
+
max_length = optstrings.max_by(&:length).length
|
79
|
+
|
80
|
+
puts "Options:"
|
81
|
+
|
82
|
+
pairs = command.options.zip optstrings
|
83
|
+
|
84
|
+
pairs.each do |pair|
|
85
|
+
opt, string = *pair
|
86
|
+
opt = opt[1]
|
87
|
+
printf "%-#{max_length}s : %s\n", string, squeeze_text(
|
88
|
+
opt[:description],
|
89
|
+
width: ENV['COLUMNS'].to_i - max_length - 3,
|
90
|
+
offset: max_length,
|
91
|
+
indent_first_line: false
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
print "\n"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.print_summary(command)
|
100
|
+
if command.name == "#{rootname}::Command"
|
101
|
+
# TODO: allow this to be overridden somewhere (or otherwise make
|
102
|
+
# #command_name smarter)
|
103
|
+
name = rootname
|
104
|
+
else
|
105
|
+
name = command.command_name
|
106
|
+
end
|
107
|
+
|
108
|
+
print "#{name}: #{command.summary}\n\n"
|
109
|
+
print "Description:\n#{command.description}\n\n"
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.print_commands(namespace)
|
113
|
+
cmds = commands(namespace)
|
114
|
+
|
115
|
+
unless cmds.none?
|
116
|
+
max_length = cmds.map { |c| c.command_name }.max_by(&:length).length
|
117
|
+
|
118
|
+
puts "Subcommands:"
|
119
|
+
|
120
|
+
cmds.each do |command|
|
121
|
+
printf "%-#{max_length}s : %s\n", command.command_name, squeeze_text(
|
122
|
+
command.summary,
|
123
|
+
width: ENV['COLUMNS'].to_i - max_length - 3,
|
124
|
+
offset: max_length,
|
125
|
+
indent_first_line: false
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
print "\n"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.squeeze_text(*strings, **opts)
|
134
|
+
text = strings.join('..')
|
135
|
+
result = ""
|
136
|
+
opts[:indent_with] ||= ' '
|
137
|
+
|
138
|
+
# wrap
|
139
|
+
text.split("\n").map! do |line|
|
140
|
+
if line.length > opts[:width]
|
141
|
+
line.gsub(/(.{1,#{opts[:width]}})(\s+|$)/, "\\1\n").strip
|
142
|
+
else
|
143
|
+
line
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# indent
|
148
|
+
text.split("\n").map! do |line|
|
149
|
+
indent = opts[:indent_with] * opts[:offset]
|
150
|
+
(indent + line).sub(/[\s]+$/,'')
|
151
|
+
end
|
152
|
+
|
153
|
+
if opts[:indent_first_line]
|
154
|
+
return text
|
155
|
+
else
|
156
|
+
return text.strip
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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/command.rb
CHANGED
@@ -42,20 +42,55 @@ module Bales
|
|
42
42
|
##
|
43
43
|
# Accessor for the options hash generated by +#option+.
|
44
44
|
def self.options
|
45
|
-
|
46
|
-
|
45
|
+
const_get "OPTIONS"
|
46
|
+
rescue NameError
|
47
|
+
const_set("OPTIONS", {})
|
47
48
|
end
|
49
|
+
def self.options=(new) # :nodoc:
|
50
|
+
const_set("OPTIONS", new)
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Get the command's description, or set it if a string is passed
|
55
|
+
# to it.
|
56
|
+
def self.description(text=nil)
|
57
|
+
const_set "DESCRIPTION", text unless text.nil?
|
58
|
+
const_get "DESCRIPTION"
|
59
|
+
rescue NameError
|
60
|
+
const_set "DESCRIPTION", "(no description)"
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Get the command's summary, or set it if a string is passed to
|
65
|
+
# it.
|
66
|
+
def self.summary(text=nil)
|
67
|
+
const_set "SUMMARY", text unless text.nil?
|
68
|
+
const_get "SUMMARY"
|
69
|
+
rescue NameError
|
70
|
+
const_set "SUMMARY", "(no summary)"
|
71
|
+
end
|
72
|
+
|
48
73
|
##
|
49
|
-
#
|
50
|
-
#
|
51
|
-
def self.
|
52
|
-
|
74
|
+
# Translates the command's class name to the corresponding name
|
75
|
+
# passed on the command line.
|
76
|
+
def self.command_name
|
77
|
+
name = self
|
78
|
+
.name
|
79
|
+
.split('::')
|
80
|
+
.last
|
81
|
+
.gsub(/(.)([A-Z])/, '\1-\2')
|
82
|
+
.downcase
|
83
|
+
if name == "command"
|
84
|
+
$0
|
85
|
+
else
|
86
|
+
name
|
87
|
+
end
|
53
88
|
end
|
54
89
|
|
55
90
|
##
|
56
|
-
# Assigns an action to this command. Said action is represented
|
57
|
-
# block, which should accept an array of arguments and a hash
|
58
|
-
# For example:
|
91
|
+
# Assigns an action to this command. Said action is represented
|
92
|
+
# as a block, which should accept an array of arguments and a hash
|
93
|
+
# of options. For example:
|
59
94
|
#
|
60
95
|
# class MyApp::Hello < Bales::Command
|
61
96
|
# action do |args, opts|
|
@@ -63,50 +98,64 @@ module Bales
|
|
63
98
|
# end
|
64
99
|
# end
|
65
100
|
def self.action(&code)
|
66
|
-
|
101
|
+
singleton_class.instance_eval do
|
102
|
+
define_method :run, &code
|
103
|
+
end
|
67
104
|
end
|
68
105
|
|
106
|
+
##
|
107
|
+
# Primary entry point for a +Bales::Command+. Generally a good
|
108
|
+
# idea to set this with +.action+, but it's possible to override
|
109
|
+
# this manually should you choose to do so.
|
69
110
|
def self.run(*args, **opts)
|
70
|
-
|
111
|
+
my_help_class_name = "#{self.name}::Help"
|
112
|
+
root_help_class_name = "#{self.to_s.split('::').first}::Command::Help"
|
113
|
+
if eval("defined? #{my_help_class_name}")
|
114
|
+
eval(my_help_class_name).run *args, **opts
|
115
|
+
elsif eval("defined? #{root_help_class_name}")
|
116
|
+
eval(root_help_class_name).run *args, **opts
|
117
|
+
else
|
118
|
+
Bales::Command::Help.run *args, **opts
|
119
|
+
end
|
71
120
|
end
|
72
121
|
|
73
122
|
##
|
74
|
-
# Defines a named option that the command will accept, along with
|
75
|
-
# named arguments:
|
123
|
+
# Defines a named option that the command will accept, along with
|
124
|
+
# some named arguments:
|
76
125
|
#
|
77
|
-
# [+:short_form+ (optional)]
|
78
|
-
# (like +-v+). This should be a string, like
|
79
|
-
# +"-v"+.
|
126
|
+
# [+:short_form+ (optional)]
|
80
127
|
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
# of the option if not specified. This should
|
84
|
-
# be a string, like +"--verbose"+
|
128
|
+
# A shorthand flag to use for the option (like +-v+). This
|
129
|
+
# should be a string, like +"-v"+.
|
85
130
|
#
|
86
|
-
# [+:
|
87
|
-
# Defaults to +TrueClass+. Should be a valid
|
88
|
-
# class name, like +String+ or +Integer+
|
131
|
+
# [+:long_form+ (optional)]
|
89
132
|
#
|
90
|
-
#
|
91
|
-
#
|
92
|
-
#
|
93
|
-
# want it to default to +false+, set +:type+
|
94
|
-
# to +FalseClass+.
|
133
|
+
# A longhand flag to use for the option (like +--verbose+).
|
134
|
+
# This is derived from the name of the option if not
|
135
|
+
# specified. This should be a string, like +"--verbose"+
|
95
136
|
#
|
96
|
-
# [+:
|
97
|
-
# accepts. This should be a symbol (like
|
98
|
-
# :level) or +false+ (if the option is a
|
99
|
-
# boolean flag). Defaults to the name of the
|
100
|
-
# option or (if the option's +:type+ is
|
101
|
-
# +TrueClass+ or +FalseClass+) +false+.
|
137
|
+
# [+:type+ (optional)]
|
102
138
|
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
139
|
+
# The type that this option represents. Defaults to
|
140
|
+
# +TrueClass+. Should be a valid class name, like +String+ or
|
141
|
+
# +Integer+
|
106
142
|
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
# to
|
143
|
+
# A special note on boolean options: if you want your boolean
|
144
|
+
# to default to `true`, set +:type+ to +TrueClass+. Likewise,
|
145
|
+
# if you want it to default to +false+, set +:type+ to
|
146
|
+
# +FalseClass+.
|
147
|
+
#
|
148
|
+
# [+:arg+ (optional)]
|
149
|
+
#
|
150
|
+
# The name of the argument this option accepts. This should
|
151
|
+
# be a symbol (like :level) or +false+ (if the option is a
|
152
|
+
# boolean flag). Defaults to the name of the option or (if
|
153
|
+
# the option's +:type+ is +TrueClass+ or +FalseClass+)
|
154
|
+
# +false+.
|
155
|
+
#
|
156
|
+
# Aside from the hash of option-options, +option+ takes a single
|
157
|
+
# +name+ argument, which should be a symbol representing the name
|
158
|
+
# of the option to be set, like +:verbose+.
|
110
159
|
def self.option(name, **opts)
|
111
160
|
name = name.to_sym
|
112
161
|
opts[:long_form] ||= "--#{name.to_s}".gsub("_","-")
|
@@ -117,12 +166,12 @@ module Bales
|
|
117
166
|
raise ArgumentError, ":type option should be a valid class"
|
118
167
|
end
|
119
168
|
|
120
|
-
|
169
|
+
unless opts[:type] <= TrueClass or opts[:type] <= FalseClass
|
121
170
|
opts[:arg] ||= name
|
122
171
|
end
|
123
172
|
|
124
|
-
opts[:default] = false if opts[:type]
|
125
|
-
opts[:default] = true if opts[:type]
|
173
|
+
opts[:default] = false if opts[:type] <= TrueClass
|
174
|
+
opts[:default] = true if opts[:type] <= FalseClass
|
126
175
|
|
127
176
|
result = options
|
128
177
|
result[name] = opts
|
@@ -130,41 +179,41 @@ module Bales
|
|
130
179
|
end
|
131
180
|
|
132
181
|
##
|
133
|
-
# Takes an ARGV-like array and returns a hash of options and
|
134
|
-
# of the original array. This is rarely needed for
|
135
|
-
# an integral part of how a
|
136
|
-
# receives.
|
182
|
+
# Takes an ARGV-like array and returns a hash of options and
|
183
|
+
# what's left of the original array. This is rarely needed for
|
184
|
+
# normal use, but is an integral part of how a
|
185
|
+
# +Bales::Application+ parses the ARGV it receives.
|
137
186
|
#
|
138
|
-
# Normally, this should be perfectly fine to leave alone, but if
|
139
|
-
# prefer to define your own parsing method (e.g. if you want
|
140
|
-
# an alternative format for command-line options, or
|
141
|
-
# dissatisfied with the default approach of
|
142
|
-
# is the method you'd want to
|
187
|
+
# Normally, this should be perfectly fine to leave alone, but if
|
188
|
+
# you prefer to define your own parsing method (e.g. if you want
|
189
|
+
# to specify an alternative format for command-line options, or
|
190
|
+
# you are otherwise dissatisfied with the default approach of
|
191
|
+
# wrapping OptionParser), this is the method you'd want to
|
192
|
+
# override.
|
143
193
|
def self.parse_opts(argv)
|
144
194
|
optparser = OptionParser.new
|
145
195
|
result = {}
|
146
196
|
options.each do |name, opts|
|
147
197
|
result[name] = opts[:default]
|
148
198
|
parser_args = []
|
149
|
-
|
150
|
-
|
199
|
+
if opts[:type] <= TrueClass or opts[:type] <= FalseClass
|
200
|
+
parser_args.push opts[:short_form] if opts[:short_form]
|
201
|
+
parser_args.push opts[:long_form]
|
202
|
+
else
|
151
203
|
argstring = opts[:arg].to_s.upcase
|
152
|
-
if opts[:
|
153
|
-
parser_args.push "#{opts[:
|
154
|
-
else
|
155
|
-
parser_args.push "#{opts[:long_form]} [#{argstring}]"
|
204
|
+
if opts[:short_form]
|
205
|
+
parser_args.push "#{opts[:short_form]} #{argstring}"
|
156
206
|
end
|
207
|
+
parser_args.push "#{opts[:long_form]} #{argstring}"
|
157
208
|
parser_args.push opts[:type]
|
158
|
-
else
|
159
|
-
parser_args.push opts[:long_form]
|
160
209
|
end
|
161
|
-
parser_args.push opts[:description]
|
210
|
+
parser_args.push opts[:description] if opts[:description]
|
162
211
|
|
163
|
-
if opts[:type]
|
212
|
+
if opts[:type] <= FalseClass
|
164
213
|
optparser.on(*parser_args) do
|
165
214
|
result[name] = false
|
166
215
|
end
|
167
|
-
elsif opts[:type]
|
216
|
+
elsif opts[:type] <= TrueClass
|
168
217
|
optparser.on(*parser_args) do
|
169
218
|
result[name] = true
|
170
219
|
end
|
@@ -180,11 +229,3 @@ module Bales
|
|
180
229
|
end
|
181
230
|
end
|
182
231
|
end
|
183
|
-
|
184
|
-
##
|
185
|
-
# Default help command. You'll probably use your own...
|
186
|
-
class Bales::Command::Help < Bales::Command
|
187
|
-
action do |args, opts|
|
188
|
-
puts "This will someday output some help text"
|
189
|
-
end
|
190
|
-
end
|
data/lib/bales/version.rb
CHANGED
data/lib/bales.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bales
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan S. Northrup
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-07-
|
11
|
+
date: 2015-07-27 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A framework for building command-line applications
|
14
14
|
email:
|
@@ -24,6 +24,8 @@ files:
|
|
24
24
|
- lib/bales/application.rb~
|
25
25
|
- lib/bales/command.rb
|
26
26
|
- lib/bales/command.rb~
|
27
|
+
- lib/bales/command/help.rb
|
28
|
+
- lib/bales/command/help.rb~
|
27
29
|
- lib/bales/version.rb
|
28
30
|
- lib/bales/version.rb~
|
29
31
|
homepage: http://github.com/YellowApple/bales
|