thunder-1.8.7 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
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,38 @@
1
+ [Thunder](http://stevenkaras.github.com/thunder)
2
+ =======
3
+ Ruby gem for quick and easy command line interfaces
4
+
5
+ Usage
6
+ -----
7
+
8
+ gem install thunder
9
+
10
+ see '[sample](http://github.com/stevenkaras/thunder/blob/master/sample)' for a quick overview of all features
11
+
12
+ Philosophy
13
+ ----------
14
+ 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.
15
+
16
+ While this is possible, and even easy using existing libraries, they either violate the principle of locality, or provide too many services.
17
+
18
+ The overarching philosophy here is that a library should do one or two things, and do them extremely well.
19
+
20
+ Goals
21
+ -----
22
+ 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.
23
+
24
+ Notable features as of the current release:
25
+
26
+ * options parsing for commands
27
+ * default commands
28
+ * subcommands (ala git)
29
+ * integrated help system
30
+ * bash completion script generator
31
+
32
+ Development
33
+ -----------
34
+ If you'd like to contribute, fork, commit, and request a pull. I'll get around to it. No special dependencies, or anything fancy
35
+
36
+ License
37
+ -------
38
+ 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
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'thunder'
4
+ require 'erb'
5
+
6
+ def thunder_commands(bolt)
7
+ bolt[:commands].map(&:first).map(&:to_s)
8
+ end
9
+
10
+ def thunder_options(command)
11
+ return nil unless command[:options]
12
+
13
+ result = []
14
+ command[:options].each do |opt, option|
15
+ result << "--#{opt}"
16
+ next unless option[:short]
17
+ result << "-#{option[:short]}"
18
+ end
19
+ return result
20
+ end
21
+
22
+ module Thunder
23
+ def start(args=ARGV.dup, options={})
24
+ template = ERB.new <<-TEMPLATE, nil, "%"
25
+ #!/bin/bash
26
+
27
+ % progname = File.basename(ARGV.first)
28
+ __<%= progname %>_commands() {
29
+ echo "<%= thunder_commands(thunder).join(" ") %>"
30
+ }
31
+ % thunder[:commands].each do |name, command|
32
+ % name = name.to_s
33
+ % if command[:options]
34
+
35
+ __<%= progname %>_<%= name %>_options() {
36
+ echo "<%= thunder_options(command).join(" ") %>"
37
+ }
38
+ % end
39
+ % end
40
+
41
+ __<%= progname %>_complete() {
42
+ local words=""
43
+ if [[ ${COMP_WORDS[COMP_CWORD]:0:1} == "-" ]]; then
44
+ if ((COMP_CWORD == 1)); then
45
+ words=
46
+ else
47
+ local command="__<%= progname %>_${COMP_WORDS[1]}_options"
48
+ words=$($command)
49
+ fi
50
+ elif ((COMP_CWORD == 1)); then
51
+ # display only commands
52
+ words=$(__<%= progname %>_commands)
53
+ fi
54
+ COMPREPLY=($(compgen -W "$words" -- ${COMP_WORDS[COMP_CWORD]}))
55
+ }
56
+
57
+ complete -o default -o nospace -F __<%= progname %>_complete <%= progname %>
58
+
59
+ TEMPLATE
60
+ context = (proc { |thiz, thunder|
61
+ binding
62
+ }).call(self, self.class.thunder)
63
+ puts template.result(context)
64
+ end
65
+ end
66
+
67
+ if ARGV.size != 1
68
+ puts "Usage: thunder-completion THUNDER_SCRIPT"
69
+ puts
70
+ puts "Prints out the suggested template for a bash completion script for the given thunder script"
71
+ exit 1
72
+ end
73
+
74
+ load File.expand_path(ARGV.first)
data/bin/thunder-spec ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'thunder'
4
+ require 'pp'
5
+
6
+ module Thunder
7
+ def start(args=ARGV.dup, options={})
8
+ spec = self.class.thunder
9
+ pp renderThunderSpec(spec)
10
+ end
11
+ end
12
+
13
+ def renderThunderSpec(spec)
14
+ spec[:commands].each do |name, command|
15
+ command[:subcommand] = renderThunderSpec(command[:subcommand].class.thunder) if command[:subcommand]
16
+ end
17
+ spec
18
+ end
19
+
20
+ if ARGV.size != 1
21
+ puts "Usage: thunder-spec THUNDER_SCRIPT"
22
+ puts
23
+ puts "Dumps the thunder spec to the command line for debugging and analysis"
24
+ exit 1
25
+ end
26
+
27
+ load File.expand_path(ARGV.first)
@@ -0,0 +1,94 @@
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
+ footer = ""
10
+ footer << command_spec[:description] + "\n" if command_spec[:description]
11
+ footer << command_spec[:long_description] + "\n" if command_spec[:long_description]
12
+ footer << "\n" + format_options(command_spec[:options]) if command_spec[:options]
13
+ output = <<-EOS
14
+ Usage:
15
+ #{preamble} #{command_spec[:usage]}
16
+
17
+ #{footer.strip}
18
+ EOS
19
+ output.rstrip
20
+ end
21
+
22
+ # @see Thunder#help_list(commands)
23
+ def help_list(commands)
24
+ preamble = determine_preamble
25
+ help = []
26
+ commands.each do |name, command_spec|
27
+ help << short_help(preamble, command_spec)
28
+ end
29
+ render_table(help)
30
+ end
31
+
32
+ private
33
+
34
+ # format a set of option specs
35
+ #
36
+ # @param options [<Hash>] the option specs to format
37
+ # @return [String]
38
+ def format_options(options)
39
+ data = []
40
+ options.each do |name, option_spec|
41
+ data << format_option(option_spec)
42
+ end
43
+ "Options:\n" + render_table(data, ": ")
44
+ end
45
+
46
+ # format an option
47
+ #
48
+ # @param option_spec [Hash] the option spec to format
49
+ # @return [(String, String)] the formatted option and its description
50
+ def format_option(option_spec)
51
+ usage = " -#{option_spec[:short]}, --#{option_spec[:name]}"
52
+ usage << " [#{option_spec[:name].to_s.upcase}]" unless option_spec[:type] == Boolean
53
+ return usage, option_spec[:desc]
54
+ end
55
+
56
+ # determine the preamble
57
+ #
58
+ # @return [String] the preamble
59
+ def determine_preamble
60
+ preamble = "#{File.basename($0)}"
61
+ ARGV.each do |arg|
62
+ break if arg == "help"
63
+ preamble << " #{arg}"
64
+ end
65
+ preamble
66
+ end
67
+
68
+ # render the short help string for a command
69
+ #
70
+ # @param preamble [String] the preamble
71
+ # @param command_spec [Hash]
72
+ # @return [String] the short help string for the given command
73
+ def short_help(preamble, command_spec)
74
+ return " #{preamble} #{command_spec[:usage]}", command_spec[:description]
75
+ end
76
+
77
+ # render a two-column table
78
+ #
79
+ # @param data [(String,String)]
80
+ # @param separator [String]
81
+ # @return [String] a two-column table
82
+ def render_table(data, separator = " # ")
83
+ column_width = data.group_by do |row|
84
+ row.first.size
85
+ end.max.first
86
+ "".tap do |output|
87
+ data.each do |row|
88
+ output << "%-#{column_width}s#{separator}%s\n" % row
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,42 @@
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 {} unless command_spec[:options]
9
+ options = {}
10
+ command_spec[:options_processor] ||= OptionParser.new do |parser|
11
+ command_spec[:options].each do |name, option_spec|
12
+ opt = []
13
+ opt << "-#{option_spec[:short]}"
14
+ opt << if option_spec[:type] == Boolean
15
+ "--[no-]#{name}"
16
+ else
17
+ "--#{name} [#{name.to_s.upcase}]"
18
+ end
19
+ opt << option_spec[:type] unless option_spec[:type] == Boolean
20
+ opt << option_spec[:desc]
21
+ parser.on(*opt) do |value|
22
+ options[name] = value
23
+ end
24
+ end
25
+ end
26
+ command_spec[:options_processor].parse!(args)
27
+
28
+ # set default values
29
+ command_spec[:options].each do |name, option_spec|
30
+ next if options.has_key? name
31
+ next unless option_spec[:default]
32
+ options[name] = option_spec[:default]
33
+ end
34
+
35
+ return options
36
+ rescue OptionParser::InvalidOption => e
37
+ puts e
38
+ puts "Try --help for help."
39
+ exit 1
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,27 @@
1
+ require 'trollop'
2
+
3
+ module Thunder
4
+ # provides an adapter to the popular trollop option parsing library (requires trollop.rb be on the load path)
5
+ class TrollopAdapter
6
+ # @see Thunder#process_options
7
+ def self.process_options(args, command_spec)
8
+ return nil unless command_spec[:options]
9
+ #TODO: fix the unspecified option bug
10
+ command_spec[:option_processor] ||= Trollop::Parser.new do
11
+ command_spec[:options].each do |name, option_spec|
12
+ opt_options = {}
13
+ description = option_spec[:desc] || ""
14
+ type = option_spec[:type]
15
+ type = :flag if type == Thunder::Boolean
16
+ opt_options[:type] = type
17
+ default_value = option_spec[:default]
18
+ opt_options[:default] = default_value if default_value
19
+ opt_options[:short] = "-" + option_spec[:short]
20
+
21
+ opt name, description, opt_options
22
+ end
23
+ end
24
+ command_spec[:option_processor].parse(args)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ module Thunder
2
+ # Version string for gemspec
3
+ VERSION = "0.5.2"
4
+ end
data/lib/thunder.rb ADDED
@@ -0,0 +1,223 @@
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.dup, 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
+ parsed_options = process_options(args, command_spec)
27
+ options.merge!(parsed_options) if parsed_options
28
+ if command_spec[:subcommand]
29
+ return command_spec[:subcommand].start(args, options)
30
+ elsif parsed_options
31
+ #TODO: do arity check
32
+ args << options
33
+ return send command_spec[:name], *args
34
+ else
35
+ #TODO: do arity check
36
+ return send command_spec[:name], *args
37
+ end
38
+ end
39
+
40
+ protected
41
+ # Determine the command to use from the given arguments
42
+ #
43
+ # @param args [<String>] the arguments to process
44
+ # @return [Hash,nil] the command specification for the given arguments,
45
+ # or nil if there is no appropriate command
46
+ def determine_command(args)
47
+ if args.empty?
48
+ return self.class.thunder[:commands][self.class.thunder[:default_command]]
49
+ end
50
+ command_name = args.first.to_sym
51
+ command_spec = self.class.thunder[:commands][command_name]
52
+ if command_spec
53
+ args.shift
54
+ else
55
+ command_spec = self.class.thunder[:commands][self.class.thunder[:default_command]]
56
+ end
57
+ return command_spec
58
+ end
59
+
60
+ # Process command line options from the given argument list
61
+ #
62
+ # @param args [<String>] the argument list to process
63
+ # @param command_spec [Hash] the command specification to use
64
+ # @return [{Symbol => *},nil] the options
65
+ def process_options(args, command_spec)
66
+ return nil unless command_spec[:options]
67
+
68
+ unless self.class.thunder[:options_processor]
69
+ require 'thunder/options/optparse'
70
+ self.class.thunder[:options_processor] = Thunder::OptParseAdapter
71
+ end
72
+ self.class.thunder[:options_processor].process_options(args, command_spec)
73
+ end
74
+
75
+ # get help on the provided subjects
76
+ #
77
+ # @param args [<String>] the arguments list
78
+ # @param options [Hash] any included options
79
+ def get_help(args, options)
80
+ unless self.class.thunder[:help_formatter]
81
+ require 'thunder/help/default'
82
+ self.class.thunder[:help_formatter] = Thunder::DefaultHelp
83
+ end
84
+ if args.size == 0
85
+ puts help_list(self.class.thunder[:commands])
86
+ else
87
+ puts help_command(determine_command(args))
88
+ end
89
+ end
90
+
91
+ # Render a usage list of the given commands
92
+ #
93
+ # @param commands [<Hash>] the commands to list
94
+ # @return [String] the rendered help
95
+ def help_list(commands)
96
+ self.class.thunder[:help_formatter].help_list(commands)
97
+ end
98
+
99
+ # Render detailed help on a specific command
100
+ #
101
+ # @param command_spec [Hash] the command to render detailed help for
102
+ # @return [String] the rendered help
103
+ def help_command(command_spec)
104
+ self.class.thunder[:help_formatter].help_command(command_spec)
105
+ end
106
+
107
+ public
108
+ # @api private
109
+ # Automatically extends the singleton with {ClassMethods}
110
+ def self.included(base)
111
+ base.send :extend, ClassMethods
112
+ end
113
+
114
+ # This module provides methods for any class that includes Thunder
115
+ module ClassMethods
116
+
117
+ # @api private
118
+ # Get the thunder configuration
119
+ def thunder
120
+ @thunder ||= {
121
+ :default_command => :help,
122
+ :commands => {
123
+ :help => {
124
+ :name => :help,
125
+ :usage => "help [COMMAND]",
126
+ :description => "list available commands or describe a specific command",
127
+ :long_description => nil,
128
+ :options => nil,
129
+ :default_help => true
130
+ },
131
+ }
132
+ }
133
+ end
134
+
135
+ # @api private
136
+ # Registers a method as a thunder task
137
+ def method_added(method)
138
+ add_command(method.to_sym)
139
+ # thunder[:commands][method][:params] = instance_method(method).parameters
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
+ # @option options :default [*] the default value
186
+ #
187
+ # @example
188
+ # option :output_file, type: String
189
+ #
190
+ # @example
191
+ # option "verbose", desc: "print extra information"
192
+ def option(name, options={})
193
+ name = name.to_sym
194
+ options[:name] = name
195
+ options[:short] ||= name.to_s[0,1]
196
+ options[:type] ||= Boolean
197
+ options[:desc] ||= ""
198
+ thunder[:options] ||= {}
199
+ thunder[:options][name] = options
200
+ end
201
+
202
+ # Define a subcommand
203
+ #
204
+ # @param command [Symbol,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
+ add_command(command.to_sym)
208
+ thunder[:commands][command.to_sym][:subcommand] = handler
209
+ end
210
+
211
+ private
212
+ def add_command(command)
213
+ attributes = [:usage, :description, :options, :long_description]
214
+ return unless attributes.reduce(nil) { |a, key| a || thunder[key] }
215
+ thunder[:commands][command] = {
216
+ :name => command,
217
+ }
218
+ attributes.each do |key|
219
+ thunder[:commands][command][key] = thunder.delete(key)
220
+ end
221
+ end
222
+ end
223
+ 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,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thunder-1.8.7
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Steven Karas
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-01 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Thunder does command line interfaces. Nothing more, nothing less.
15
+ email: steven.karas@gmail.com
16
+ executables:
17
+ - thunder-spec
18
+ - thunder-completion
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - lib/thunder/help/default.rb
23
+ - lib/thunder/version.rb
24
+ - lib/thunder/options/optparse.rb
25
+ - lib/thunder/options/trollop.rb
26
+ - lib/thunder.rb
27
+ - spec/spec_thunder.rb
28
+ - Rakefile
29
+ - DESIGN.md
30
+ - README.md
31
+ - bin/thunder-spec
32
+ - bin/thunder-completion
33
+ homepage: http://stevenkaras.github.com/thunder
34
+ licenses: []
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 1.8.24
54
+ signing_key:
55
+ specification_version: 3
56
+ summary: Thunder makes command lines apps easy!
57
+ test_files: []
58
+ has_rdoc: