thunder 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ v0.4.0
2
+ + add help formatter
3
+
4
+ v0.3.0
5
+ + provide facility for defining options
6
+
7
+ v0.2.0
8
+ + pass leftover arguments to the command
9
+
10
+ v0.1.0
11
+ + call specified command
@@ -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.
@@ -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
@@ -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
@@ -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,4 @@
1
+ module Thunder
2
+ # Version string for gemspec
3
+ VERSION = "0.4.0"
4
+ 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: