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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 58dd5ac6bd3070bc03dc5c84db2f92c394a43fd2
4
- data.tar.gz: dac3a1f876ae148806de122ceca026ac39db55ed
3
+ metadata.gz: 1fb40f18edf38481a779d05e661167b9a504577f
4
+ data.tar.gz: fdfb53af1b8ead967df753d0e03c332c10ffec3f
5
5
  SHA512:
6
- metadata.gz: 650893c15923467acaa65cb97aad581b66b8cc1dcb037f74cb9013c54887cefa2d41f39897ca06a9823a6517dc24e0caffef08862d600daf8d751be0dff97484
7
- data.tar.gz: 4b1f685e20a94a1012ce13ea5ec1152449accd0515c452cf02c4fbdb69a83b407df67b1ad672ba540f6dd801501754a675070b4868400a46eeadb057203aa8be
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
- end
22
-
23
- class Command < Bales::Command
24
- action do
25
- Bales::Command::Help.run
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
- class Smack < Command
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
- victims.each do |victim|
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 foo
59
- foo has been smacked.
60
- $ simple-app Fred Wilma
61
- Fred has been smacked.
62
- Wilma has been smacked.
63
- $ simple-app smack John -w fish
64
- John has been smacked with a fish.
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
- * Create a `Command` class under that namespace which inherits from `Bales::Command`
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 (or *will* have, more precisely; I don't have a whole lot for you on this front just yet) a few available DSL-ish functions for you to play with.
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 kind of a silly names is "Bales", anyway?
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
@@ -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 a
5
- # subclass of this, then call said subclass' +#parse_and_run+ instance
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 version number. Defaults to "0.0.0".
17
- def self.version(v="0.0.0")
18
- @version ||= v
19
- @version
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
- # Major version number. Assumes semantic versioning, but will work with
24
- # any versioning scheme with at least major and minor version numbers.
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 work with
31
- # any versioning scheme with at least major and minor version numbers.
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
- # Runs the specified command (should be a valid class; preferably, should
44
- # be a subclass of +Bales::Command+). Takes a list of positional args
45
- # followed by named options.
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
- command.run *args, **opts
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 the
52
- # class of the identified command, a hash containing the passed-in
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 command to
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
- command_name_parts = [*constant_to_args(base_name), "command"]
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
- if eval("defined? #{test}") == "constant"
84
- command_name_parts.push arg
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
- throw(:end)
193
+ break
88
194
  end
89
195
  end
90
- end
91
- command = args_to_constant [*command_name_parts]
196
+
92
197
  argv.shift depth
93
- return command, argv
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
- @options ||= {}
46
- @options
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
- # Setter for the options hash generated by +#option+. Usually not
50
- # needed, since that's what +#option+ is for.
51
- def self.options=(new)
52
- @options = new
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 as a
57
- # block, which should accept an array of arguments and a hash of options.
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
- @action = code
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
- @action.call(args, opts) unless @action.nil?
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 some
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)] A shorthand flag to use for the option
78
- # (like +-v+). This should be a string, like
79
- # +"-v"+.
126
+ # [+:short_form+ (optional)]
80
127
  #
81
- # [+:long_form+ (optional)] A longhand flag to use for the option (like
82
- # +--verbose+). This is derived from the name
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
- # [+:type+ (optional)] The type that this option represents.
87
- # Defaults to +TrueClass+. Should be a valid
88
- # class name, like +String+ or +Integer+
131
+ # [+:long_form+ (optional)]
89
132
  #
90
- # A special note on boolean options: if you
91
- # want your boolean to default to `true`, set
92
- # +:type+ to +TrueClass+. Likewise, if you
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
- # [+:arg+ (optional)] The name of the argument this option
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
- # [+:required+ (optional)] Whether or not the option is required. This
104
- # should be a boolean (+true+ or +false+).
105
- # Default is `false`.
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
- # Aside from the hash of option-options, +option+ takes a single +name+
108
- # argument, which should be a symbol representing the name of the option
109
- # to be set, like +:verbose+.
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
- if (opts[:type].ancestors & [TrueClass, FalseClass]).empty?
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].ancestors.include? TrueClass
125
- opts[:default] = true if opts[:type].ancestors.include? FalseClass
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 what's left
134
- # of the original array. This is rarely needed for normal use, but is
135
- # an integral part of how a +Bales::Application+ parses the ARGV it
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 you
139
- # prefer to define your own parsing method (e.g. if you want to specify
140
- # an alternative format for command-line options, or you are otherwise
141
- # dissatisfied with the default approach of wrapping OptionParser), this
142
- # is the method you'd want to override.
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
- parser_args.push opts[:short_form] if opts[:short_form]
150
- if (opts[:type].ancestors & [TrueClass,FalseClass]).empty?
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[:required]
153
- parser_args.push "#{opts[:long_form]} #{argstring}"
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].ancestors.include? FalseClass
212
+ if opts[:type] <= FalseClass
164
213
  optparser.on(*parser_args) do
165
214
  result[name] = false
166
215
  end
167
- elsif opts[:type].ancestors.include? TrueClass
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
@@ -1,3 +1,3 @@
1
1
  module Bales
2
- VERSION="0.0.2"
2
+ VERSION="0.0.3"
3
3
  end
data/lib/bales.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'bales/application'
4
4
  require 'bales/command'
5
+ require 'bales/command/help'
5
6
 
6
7
  ##
7
8
  # Ruby on Bales (or just "Bales" for short) is to command-line apps what
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.2
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-08 00:00:00.000000000 Z
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