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 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