thunder 0.4.0
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.
- data/CHANGELOG +11 -0
- data/DESIGN.md +20 -0
- data/README.md +29 -0
- data/Rakefile +25 -0
- data/lib/thunder.rb +212 -0
- data/lib/thunder/help/default.rb +69 -0
- data/lib/thunder/options/optparse.rb +32 -0
- data/lib/thunder/version.rb +4 -0
- data/spec/spec_thunder.rb +19 -0
- metadata +55 -0
data/CHANGELOG
ADDED
data/DESIGN.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
This document will document my various design decisions
|
2
|
+
|
3
|
+
Option Parsing
|
4
|
+
--------------
|
5
|
+
Rather than include option parsing as part of the Thunder Core, I farm out the work through an adapter.
|
6
|
+
The default adapter is only loaded if no other adapter is specified, and uses the ruby std-lib builtin OptParse library.
|
7
|
+
|
8
|
+
Help Formatting
|
9
|
+
---------------
|
10
|
+
Similar to option parsing in that the functionality is provided via an adapter.
|
11
|
+
The default implementation uses a style similar to rake and thor. This was chosen since it is the easiest to parse using regular expressions, barring developer error.
|
12
|
+
|
13
|
+
Subcommands
|
14
|
+
-----------
|
15
|
+
One of the stated design goals was to provide subcommand support, ala svn, git, gem and rails.
|
16
|
+
It has occured to me that as a result of the way I farm out option parsing and help formatting, this information is not passed on to the subcommand. As a result, each subcommand would need to declare which formatter to use, if the default one is not desired. This is possible to work around, but it would add an extra parameter or two to the start method, which I'd like to avoid. As there is no other way to solve this without resorting to smelly code, I'm going to put off this decision until it actually becomes relevant (it may in the future, who knows?)
|
17
|
+
|
18
|
+
Singleton vs. Instance #start()
|
19
|
+
-------------------------------
|
20
|
+
Thor runs the start through the class singleton. But it also requires you to inherit from the Thor class. This means that more complex classes cannot be turned into adhoc command line utilities, which I view as a significant limitation against the integration of command line and code. Bridging that gap is exactly what this library is supposed to do.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
Thunder
|
2
|
+
=======
|
3
|
+
Ruby gem for quick and easy command line interfaces
|
4
|
+
|
5
|
+
Philosophy
|
6
|
+
----------
|
7
|
+
The command line is the most basic interface we have with a computer. It makes sense that we should invest as much time and effort as possible to build tools to work with the command line, and tie into code we write as quickly as possible. Sadly, this has not taken place as much as one would expect. This library steps up to bridge the admittedly small gap between your standard shell and ruby code.
|
8
|
+
|
9
|
+
The overarching philosophy here is that a library should do one or two things, and do them extremely well.
|
10
|
+
|
11
|
+
Goals
|
12
|
+
-----
|
13
|
+
Provide a simple, DRY syntatic sugar library that provides the necessary services to create command line mappings to ruby methods. It will not provide any other services.
|
14
|
+
|
15
|
+
Phase 1: call a method from the command line
|
16
|
+
Phase 2: provide options and arguments
|
17
|
+
Phase 3: provide help/banner formatter
|
18
|
+
Phase 4: provide yardoc integration
|
19
|
+
Phase 5: provide a bash-completion script.
|
20
|
+
Phase 6: ???
|
21
|
+
Phase 7: Profit!
|
22
|
+
|
23
|
+
Development
|
24
|
+
-----------
|
25
|
+
If you'd like to contribute, fork, commit, and request a pull. I'll get around to it. No special dependencies, or anything fancy
|
26
|
+
|
27
|
+
License
|
28
|
+
-------
|
29
|
+
MIT License
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
require File.expand_path("../lib/thunder/version", __FILE__)
|
4
|
+
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.test_files = FileList['test/*_test.rb']
|
7
|
+
t.test_files = FileList['test/test_*.rb']
|
8
|
+
t.test_files = FileList['spec/*_spec.rb']
|
9
|
+
t.test_files = FileList['spec/spec_*.rb']
|
10
|
+
t.libs << 'spec'
|
11
|
+
t.libs << 'test'
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Run tests"
|
15
|
+
task :default => :test
|
16
|
+
|
17
|
+
desc "Build the gem"
|
18
|
+
task :build do
|
19
|
+
system "gem build thunder.gemspec"
|
20
|
+
end
|
21
|
+
|
22
|
+
task :gem => :build do
|
23
|
+
system "gem uninstall -a thunder"
|
24
|
+
system "gem install thunder-#{Thunder::VERSION}.gem"
|
25
|
+
end
|
data/lib/thunder.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
# Provides a simple, yet powerful ability to quickly and easily tie Ruby methods
|
2
|
+
# with command line actions.
|
3
|
+
#
|
4
|
+
# The syntax is very similar to Thor, so switching over should be extremely easy
|
5
|
+
module Thunder
|
6
|
+
|
7
|
+
# Used to indicate a boolean true or false value for options processing
|
8
|
+
class Boolean; end
|
9
|
+
|
10
|
+
# Start the object as a command line program,
|
11
|
+
# processing the given arguments and using the provided options.
|
12
|
+
#
|
13
|
+
# @param args [<String>] the command line arguments [ARGV]
|
14
|
+
# @param options [{Symbol => *}] the default options to use [{}]
|
15
|
+
def start(args=ARGV, options={})
|
16
|
+
command_spec = determine_command(args)
|
17
|
+
|
18
|
+
unless command_spec
|
19
|
+
return
|
20
|
+
end
|
21
|
+
|
22
|
+
if command_spec[:name] == :help && command_spec[:default_help]
|
23
|
+
return get_help(args, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
options.merge!(process_options(args, command_spec))
|
27
|
+
if command_spec[:subcommand]
|
28
|
+
return command_spec[:subcommand].start(args, options)
|
29
|
+
elsif options
|
30
|
+
#TODO: do arity check
|
31
|
+
return send command_spec, *args, options
|
32
|
+
else
|
33
|
+
#TODO: do arity check
|
34
|
+
return send command_spec, *args
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
# Determine the command to use from the given arguments
|
40
|
+
#
|
41
|
+
# @param args [<String>] the arguments to process
|
42
|
+
# @return [Hash,nil] the command specification for the given arguments,
|
43
|
+
# or nil if there is no appropriate command
|
44
|
+
def determine_command(args)
|
45
|
+
if args.empty?
|
46
|
+
return self.class.thunder[:commands][self.class.thunder[:default_command]]
|
47
|
+
end
|
48
|
+
command_name = args.first.to_sym
|
49
|
+
command_spec = self.class.thunder[:commands][command_name]
|
50
|
+
args.shift if command_spec
|
51
|
+
return command_spec
|
52
|
+
end
|
53
|
+
|
54
|
+
# Process command line options from the given argument list
|
55
|
+
#
|
56
|
+
# @param args [<String>] the argument list to process
|
57
|
+
# @param command_spec [Hash] the command specification to use
|
58
|
+
# @return [{Symbol => *}] the options
|
59
|
+
def process_options(args, command_spec)
|
60
|
+
return nil unless command_spec[:options]
|
61
|
+
|
62
|
+
unless self.class.thunder[:options_processor]
|
63
|
+
require 'thunder/options/optparse'
|
64
|
+
self.class.thunder[:options_processor] = Thunder::OptParseAdapter
|
65
|
+
end
|
66
|
+
self.class.thunder[:options_processor].process_options(args, command_spec)
|
67
|
+
end
|
68
|
+
|
69
|
+
# get help on the provided subjects
|
70
|
+
#
|
71
|
+
# @param args [<String>] the arguments list
|
72
|
+
# @param options [Hash] any included options
|
73
|
+
def get_help(args, options)
|
74
|
+
unless self.class.thunder[:help_formatter]
|
75
|
+
require 'thunder/help/default'
|
76
|
+
self.class.thunder[:help_formatter] = Thunder::DefaultHelp
|
77
|
+
end
|
78
|
+
if args.size == 0
|
79
|
+
puts help_list(self.class.thunder[:commands])
|
80
|
+
else
|
81
|
+
puts help_command(determine_command(args))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Render a usage list of the given commands
|
86
|
+
#
|
87
|
+
# @param commands [<Hash>] the commands to list
|
88
|
+
# @return [String] the rendered help
|
89
|
+
def help_list(commands)
|
90
|
+
self.class.thunder[:help_formatter].help_list(commands)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Render detailed help on a specific command
|
94
|
+
#
|
95
|
+
# @param command_spec [Hash] the command to render detailed help for
|
96
|
+
# @return [String] the rendered help
|
97
|
+
def help_command(command_spec)
|
98
|
+
self.class.thunder[:help_formatter].help_command(command_spec)
|
99
|
+
end
|
100
|
+
|
101
|
+
public
|
102
|
+
# @api private
|
103
|
+
# Automatically extends the singleton with {ClassMethods}
|
104
|
+
def self.included(base)
|
105
|
+
base.send :extend, ClassMethods
|
106
|
+
end
|
107
|
+
|
108
|
+
# This module provides methods for any class that includes Thunder
|
109
|
+
module ClassMethods
|
110
|
+
|
111
|
+
# @api private
|
112
|
+
# Get the thunder configuration
|
113
|
+
def thunder
|
114
|
+
@thunder ||= {
|
115
|
+
default_command: :help,
|
116
|
+
commands: {
|
117
|
+
help: {
|
118
|
+
name: :help,
|
119
|
+
usage: "help [COMMAND]",
|
120
|
+
description: "list available commands or describe a specific command",
|
121
|
+
options: nil,
|
122
|
+
default_help: true
|
123
|
+
},
|
124
|
+
}
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
# @api private
|
129
|
+
# Registers a method as a thunder task
|
130
|
+
def method_added(method)
|
131
|
+
attributes = [:usage, :description, :options, :long_description]
|
132
|
+
return unless attributes.reduce { |a, key| a || thunder[key] }
|
133
|
+
thunder[:commands][method] = {
|
134
|
+
name: method,
|
135
|
+
}
|
136
|
+
attributes.each do |key|
|
137
|
+
thunder[:commands][method][key] = thunder[key]
|
138
|
+
thunder[key] = nil
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Set the options processor.
|
143
|
+
#
|
144
|
+
# @param processor [#process_options]
|
145
|
+
def options_processor(processor)
|
146
|
+
thunder[:options_processor] = processor
|
147
|
+
end
|
148
|
+
|
149
|
+
# Set the help formatter.
|
150
|
+
#
|
151
|
+
# @param formatter [#help_list,#help_command]
|
152
|
+
def help_formatter(formatter)
|
153
|
+
thunder[:help_formatter] = formatter
|
154
|
+
end
|
155
|
+
|
156
|
+
# Set the default command to be executed when no suitable command is found.
|
157
|
+
#
|
158
|
+
# @param command [Symbol] the default command
|
159
|
+
def default_command(command)
|
160
|
+
thunder[:default_command] = command
|
161
|
+
end
|
162
|
+
|
163
|
+
# Describe the next method (or subcommand). A longer description can be given
|
164
|
+
# using the {#longdesc} command
|
165
|
+
#
|
166
|
+
# @param usage [String] the perscribed usage of the command
|
167
|
+
# @param description [String] a short description of what the command does
|
168
|
+
def desc(usage, description="")
|
169
|
+
thunder[:usage], thunder[:description] = usage, description
|
170
|
+
end
|
171
|
+
|
172
|
+
# Provide a long description for the next method (or subcommand).
|
173
|
+
#
|
174
|
+
# @param description [String] a long description of what the command does
|
175
|
+
def longdesc(description)
|
176
|
+
thunder[:long_description] = description
|
177
|
+
end
|
178
|
+
|
179
|
+
# Define an option for the next method (or subcommand)
|
180
|
+
#
|
181
|
+
# @param name [Symbol,String] the long name of this option
|
182
|
+
# @option options :short [String] the short version of the option [the first letter of the option name]
|
183
|
+
# @option options :type [Class] the datatype of this option [Boolean]
|
184
|
+
# @option options :desc [String] the long description of this option [""]
|
185
|
+
#
|
186
|
+
# @example
|
187
|
+
# option :output_file, type: String
|
188
|
+
#
|
189
|
+
# @example
|
190
|
+
# option "verbose", desc: "print extra information"
|
191
|
+
def option(name, options={})
|
192
|
+
#TODO: have this generate YARDoc for the option (as it should match a method option)
|
193
|
+
name = name.to_sym
|
194
|
+
options[:name] = name
|
195
|
+
options[:short] ||= name[0]
|
196
|
+
options[:type] ||= Boolean
|
197
|
+
options[:description] ||= ""
|
198
|
+
thunder[:options] ||= {}
|
199
|
+
thunder[:options][name] = options
|
200
|
+
end
|
201
|
+
|
202
|
+
# Define a subcommand
|
203
|
+
#
|
204
|
+
# @param command [String] the command that transfers processing to the provided handler
|
205
|
+
# @param handler [Thunder] the handler that processes the request
|
206
|
+
def subcommand(command, handler)
|
207
|
+
method_added(command)
|
208
|
+
thunder[:commands][command][:subcommand] = handler
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Thunder
|
2
|
+
# Provides an easy to parse help formatter
|
3
|
+
class DefaultHelp
|
4
|
+
class << self
|
5
|
+
|
6
|
+
# @see Thunder#help_command(command_spec)
|
7
|
+
def help_command(command_spec)
|
8
|
+
preamble = determine_preamble
|
9
|
+
#TODO: add options to output
|
10
|
+
output = <<-EOS
|
11
|
+
Usage:
|
12
|
+
#{preamble} #{command_spec[:usage]}
|
13
|
+
|
14
|
+
#{command_spec[:description]}
|
15
|
+
#{command_spec[:long_description]}
|
16
|
+
EOS
|
17
|
+
output.chomp
|
18
|
+
end
|
19
|
+
|
20
|
+
# @see Thunder#help_list(commands)
|
21
|
+
def help_list(commands)
|
22
|
+
preamble = determine_preamble
|
23
|
+
help = []
|
24
|
+
commands.each do |name, command_spec|
|
25
|
+
help << short_help(preamble, command_spec)
|
26
|
+
end
|
27
|
+
render_table(help)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
# determine the preamble
|
32
|
+
#
|
33
|
+
# @return [String] the preamble
|
34
|
+
def determine_preamble
|
35
|
+
preamble = "#{File.basename($0)}"
|
36
|
+
ARGV.each do |arg|
|
37
|
+
break if arg == "help"
|
38
|
+
preamble << " #{arg}"
|
39
|
+
end
|
40
|
+
preamble
|
41
|
+
end
|
42
|
+
|
43
|
+
# render the short help string for a command
|
44
|
+
#
|
45
|
+
# @param preamble [String] the preamble
|
46
|
+
# @param command_spec [Hash]
|
47
|
+
# @return [String] the short help string for the given command
|
48
|
+
def short_help(preamble, command_spec)
|
49
|
+
return " #{preamble} #{command_spec[:usage]}", command_spec[:description]
|
50
|
+
end
|
51
|
+
|
52
|
+
# render a two-column table
|
53
|
+
#
|
54
|
+
# @param data [(String,String)]
|
55
|
+
# @param separator [String]
|
56
|
+
# @return [String] a two-column table
|
57
|
+
def render_table(data, separator = "#")
|
58
|
+
column_width = data.group_by do |data|
|
59
|
+
data.first.size
|
60
|
+
end.max.first
|
61
|
+
"".tap do |output|
|
62
|
+
data.each do |line|
|
63
|
+
output << "%-#{column_width}s #{separator} %s\n" % line
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
module Thunder
|
4
|
+
# Provides an adapter to the optparse library included in the Ruby std-lib
|
5
|
+
class OptParseAdapter
|
6
|
+
# @see Thunder#process_options
|
7
|
+
def self.process_options(args, command_spec)
|
8
|
+
return nil unless command_spec[:options]
|
9
|
+
|
10
|
+
options = {}
|
11
|
+
command_spec[:options_processor] ||= OptionParser.new do |parser|
|
12
|
+
command_spec[:options].each do |name, option_spec|
|
13
|
+
opt = []
|
14
|
+
opt << "-#{option_spec[:short]}"
|
15
|
+
opt << if option_spec[:type] == Boolean
|
16
|
+
"--[no]#{name}"
|
17
|
+
else
|
18
|
+
"--#{name} OPT"
|
19
|
+
end
|
20
|
+
opt << option_spec[:type] unless option_spec[:type] == Boolean
|
21
|
+
opt << option_spec[:description]
|
22
|
+
parser.on(*opt) do |value|
|
23
|
+
options[name] = value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
command_spec[:options_processor].parse!(args)
|
28
|
+
|
29
|
+
return options
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
|
3
|
+
class ThunderSpec < MiniTest::Spec
|
4
|
+
|
5
|
+
describe "a simple example" do
|
6
|
+
it "should be easy to use" do
|
7
|
+
# test is subjective. Awaiting singularity for objective analysis
|
8
|
+
end
|
9
|
+
it "should pass on arguments" do
|
10
|
+
# TODO: write this test(s)
|
11
|
+
end
|
12
|
+
it "should pass options" do
|
13
|
+
# TODO: write these tests
|
14
|
+
end
|
15
|
+
it "should allow use of subcommands" do
|
16
|
+
# TODO: write some more tests
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: thunder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Steven Karas
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-04 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Thor does everything and the kitchen sink. Thunder only does command
|
15
|
+
line interfaces.
|
16
|
+
email: steven.karas@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/thunder/help/default.rb
|
22
|
+
- lib/thunder/options/optparse.rb
|
23
|
+
- lib/thunder/version.rb
|
24
|
+
- lib/thunder.rb
|
25
|
+
- spec/spec_thunder.rb
|
26
|
+
- CHANGELOG
|
27
|
+
- DESIGN.md
|
28
|
+
- Rakefile
|
29
|
+
- README.md
|
30
|
+
homepage: http://stevenkaras.github.com
|
31
|
+
licenses: []
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
require_paths:
|
35
|
+
- lib
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubyforge_project:
|
50
|
+
rubygems_version: 1.8.24
|
51
|
+
signing_key:
|
52
|
+
specification_version: 3
|
53
|
+
summary: Thunder makes command lines apps easy!
|
54
|
+
test_files: []
|
55
|
+
has_rdoc:
|