bales 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|