climate 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/bin/climate CHANGED
File without changes
data/lib/climate.rb CHANGED
@@ -1,30 +1,27 @@
1
1
  require 'trollop'
2
2
 
3
3
  module Climate
4
- def self.with_standard_exception_handling(&block)
4
+
5
+ def self.with_standard_exception_handling(options={}, &block)
6
+ error_messages = {
7
+ UnexpectedArgumentError => 'Unknown argument',
8
+ UnknownCommandError => 'Unknown command',
9
+ MissingArgumentError => 'Missing argument',
10
+ ConflictingOptionError => 'Conflicting options given'
11
+ }
12
+
5
13
  begin
6
14
  yield
7
15
  rescue ExitException => e
8
- $stderr.puts(e.message)
16
+ # exit silently if there is no error message to print out
17
+ $stderr.puts(e.message) if e.has_message?
9
18
  exit(e.exit_code)
10
19
  rescue HelpNeeded => e
11
- print_usage(e.command_class)
20
+ print_usage(e.command_class, options)
12
21
  exit(0)
13
- rescue UnexpectedArgumentError => e
14
- $stderr.puts("Unknown argument: #{e.message}")
15
- print_usage(e.command_class)
16
- exit(1)
17
- rescue UnknownCommandError => e
18
- $stderr.puts("Unknown command: #{e.message}")
19
- print_usage(e.command_class)
20
- exit(1)
21
- rescue MissingArgumentError => e
22
- $stderr.puts("Missing argument: #{e.message}")
23
- print_usage(e.command_class)
24
- exit(1)
25
- rescue CommandError => e
26
- $stderr.puts(e.message)
27
- print_usage(e.command_class)
22
+ rescue ParsingError => e
23
+ $stderr.puts(error_messages[e.class] + ": #{e.message}")
24
+ print_usage(e.command_class, options)
28
25
  exit(1)
29
26
  rescue => e
30
27
  $stderr.puts("Unexpected error: #{e.class.name} - #{e.message}")
@@ -36,7 +33,7 @@ module Climate
36
33
  def self.print_usage(command_class, options={})
37
34
  help = Help.new(command_class)
38
35
 
39
- help.print
36
+ help.print(options)
40
37
  end
41
38
 
42
39
  def run(&block)
@@ -1,5 +1,20 @@
1
1
  module Climate
2
2
 
3
+ # Create a new sub-class of Command with the given name. You can either
4
+ # extend this class in the traditional `class MyCommand < Command('bob') way`
5
+ # or you can define the class using class_eval by passing a block.
6
+ def self.Command(name, &block)
7
+ Class.new(Command).tap do |clazz|
8
+ clazz.instance_eval """
9
+ def command_name
10
+ '#{name}'
11
+ end
12
+ """
13
+
14
+ clazz.class_eval(&block) if block_given?
15
+ end
16
+ end
17
+
3
18
  #
4
19
  # A {Command} is a unit of work, intended to be invoked from the command line. It should be
5
20
  # extended to either do something itself by implementing run, or just be
@@ -41,26 +56,28 @@ module Climate
41
56
  parent.nil?? our_list : parent.ancestors + our_list
42
57
  end
43
58
 
44
- # Supply a name for this command, or return the existing name
45
- def name(name=nil)
46
- @name = name if name
47
- @name
59
+ # Set the name of this command, use if you don't want to use the class
60
+ # function to define your command
61
+ def set_name(command_name)
62
+ @name = command_name
48
63
  end
49
64
 
50
- # because we've extended Class.name, we expose the original method
51
- # under another name
52
- # FIXME: surely there is a saner way of doing this?
53
- def class_name
54
- Class.method(:name).unbind.bind(self).call
65
+ # Return the name of the command
66
+ def command_name
67
+ @name
55
68
  end
56
69
 
57
70
  # Register this class as being a subcommand of another {Command} class
58
71
  # @param [Command] parent_class The parent we hang off of
59
72
  def subcommand_of(parent_class)
60
- raise DefinitionError, 'can not set subcommand before name' unless @name
73
+ raise DefinitionError, 'can not set subcommand before name' unless command_name
61
74
  parent_class.add_subcommand(self)
62
75
  end
63
76
 
77
+ def subcommand_of?(parent_class)
78
+ parent_class.has_subcommand?(self)
79
+ end
80
+
64
81
  # Set the description for this command
65
82
  # @param [String] string Description/Banner/Help text
66
83
  def description(string=nil)
@@ -87,12 +104,16 @@ module Climate
87
104
  if cli_arguments.empty?
88
105
  subcommands << subcommand
89
106
  subcommand.parent = self
90
- stop_on(subcommands.map(&:name))
107
+ stop_on(subcommands.map(&:command_name))
91
108
  else
92
109
  raise DefinitionError, 'can not mix subcommands with arguments'
93
110
  end
94
111
  end
95
112
 
113
+ def has_subcommand?(subcommand)
114
+ subcommands.include?(subcommand)
115
+ end
116
+
96
117
  def arg(*args)
97
118
  if subcommands.empty?
98
119
  super(*args)
@@ -110,11 +131,11 @@ module Climate
110
131
  command_name, *arguments = parent.leftovers
111
132
 
112
133
  if command_name.nil?
113
- raise MissingArgumentError.new("command #{parent.class.name}" +
134
+ raise MissingArgumentError.new("command #{parent.class.command_name}" +
114
135
  " expects a subcommand as an argument", parent)
115
136
  end
116
137
 
117
- found = subcommands.find {|c| c.name == command_name }
138
+ found = subcommands.find {|c| c.command_name == command_name }
118
139
 
119
140
  if found
120
141
  found.run(arguments, options.merge(:parent => parent))
@@ -186,5 +207,9 @@ module Climate
186
207
  def run
187
208
  raise NotImplementedError, "Leaf commands must implement a run method"
188
209
  end
210
+
211
+ def exit(status)
212
+ raise Climate::ExitException.new(nil, status)
213
+ end
189
214
  end
190
215
  end
@@ -0,0 +1,18 @@
1
+ module Climate
2
+ # require this file to monkey patch Command to have old <= 0.4 Command.name
3
+ # method that was removed to fix https://github.com/playlouder/climate/issues/6
4
+ class Command
5
+ def self.name(name=nil)
6
+ set_name(name) if name
7
+ command_name
8
+ end
9
+
10
+ # because we've extended Class.name, we expose the original method
11
+ # under another name. Can be removed once we move away from Command.name
12
+ # method
13
+ # FIXME: surely there is a saner way of doing this?
14
+ def self.class_name
15
+ Class.method(:name).unbind.bind(self).call
16
+ end
17
+ end
18
+ end
@@ -18,6 +18,10 @@ module Climate
18
18
  end
19
19
  end
20
20
 
21
+ # Raised when there is some problem with parsing the command line, where the
22
+ # user is at fault
23
+ class ParsingError < CommandError ; end
24
+
21
25
  # Command instances can raise this error to exit
22
26
  class ExitException < CommandError
23
27
 
@@ -27,8 +31,14 @@ module Climate
27
31
 
28
32
  def initialize(msg, exit_code=1)
29
33
  @exit_code = exit_code
34
+ @message = msg
30
35
  super(msg)
31
36
  end
37
+
38
+ # Ruby will helpfully change a nil message to be the name of the exception
39
+ # class, so we have to have a special predicate method to tell us whether
40
+ # message was nil or not
41
+ def has_message? ; !!@message ; end
32
42
  end
33
43
 
34
44
  class HelpNeeded < CommandError
@@ -36,12 +46,15 @@ module Climate
36
46
  end
37
47
 
38
48
  # Raised when a {Command} is run with too many arguments
39
- class UnexpectedArgumentError < CommandError ; end
49
+ class UnexpectedArgumentError < ParsingError ; end
40
50
 
41
51
  # Raised when a {Command} is run with insufficient arguments
42
- class MissingArgumentError < CommandError ; end
52
+ class MissingArgumentError < ParsingError ; end
53
+
54
+ # Raised when two or more options conflict
55
+ class ConflictingOptionError < ParsingError ; end
43
56
 
44
57
  # Raised when a parent {Command} is asked to run a sub command it does not
45
58
  # know about
46
- class UnknownCommandError < CommandError ; end
59
+ class UnknownCommandError < ParsingError ; end
47
60
  end
data/lib/climate/help.rb CHANGED
@@ -9,7 +9,8 @@ module Climate
9
9
  @output = options[:output] || $stdout
10
10
  end
11
11
 
12
- def print
12
+ def print(options={})
13
+ run_pager if options[:pager]
13
14
  print_usage
14
15
  print_description
15
16
  print_options if command_class.has_options? || command_class.has_arguments?
@@ -17,7 +18,7 @@ module Climate
17
18
  end
18
19
 
19
20
  def print_usage
20
- ancestor_list = command_class.ancestors.map(&:name).join(' ')
21
+ ancestor_list = command_class.ancestors.map(&:command_name).join(' ')
21
22
  opts_usage = command_class.cli_options.map {|opt| opt.usage }
22
23
  args_usage =
23
24
  if command_class.has_subcommands?
@@ -41,7 +42,7 @@ module Climate
41
42
  puts "Available subcommands:"
42
43
  indent do
43
44
  command_class.subcommands.each do |subcommand_class|
44
- puts "#{subcommand_class.name}"
45
+ puts "#{subcommand_class.command_name}"
45
46
  end
46
47
  end
47
48
  end
@@ -143,6 +144,32 @@ module Climate
143
144
  }.join("\n\n")
144
145
  end
145
146
 
147
+ # taken from http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby
148
+ def run_pager
149
+ return if PLATFORM =~ /win32/
150
+ return if PLATFORM == 'java'
151
+ return unless STDOUT.tty?
146
152
 
153
+ read, write = IO.pipe
154
+
155
+ unless Kernel.fork # Child process
156
+ STDOUT.reopen(write)
157
+ STDERR.reopen(write) if STDERR.tty?
158
+ read.close
159
+ write.close
160
+ return
161
+ end
162
+
163
+ # Parent process, become pager
164
+ STDIN.reopen(read)
165
+ read.close
166
+ write.close
167
+
168
+ ENV['LESS'] = 'FSRX' # Don't page if the input is short enough
169
+
170
+ Kernel.select [STDIN] # Wait until we have input before we start the pager
171
+ pager = ENV['PAGER'] || 'less'
172
+ exec pager rescue exec "/bin/sh", "-c", pager
173
+ end
147
174
  end
148
175
  end
@@ -10,8 +10,7 @@ class Climate::Help
10
10
  class Man
11
11
 
12
12
  # Eat my own dog food
13
- class Script < Climate::Command
14
- name 'man'
13
+ class Script < Climate::Command('man')
15
14
  description 'Creates man/nroff output for a command'
16
15
 
17
16
  arg :command_class, "name of class that defines the command, i.e. Foo::Bar::Command",
@@ -58,7 +57,7 @@ class Climate::Help
58
57
  stack = []
59
58
  command_ptr = command_class
60
59
  while command_ptr
61
- stack.unshift(command_ptr.name)
60
+ stack.unshift(command_ptr.command_name)
62
61
  command_ptr = command_ptr.parent
63
62
  end
64
63
 
@@ -66,7 +65,7 @@ class Climate::Help
66
65
  end
67
66
 
68
67
  def short_name
69
- command_class.name
68
+ command_class.command_name
70
69
  end
71
70
 
72
71
  def date ; Date.today.strftime('%b, %Y') ; end
@@ -41,7 +41,7 @@ module Climate
41
41
  end
42
42
 
43
43
  def long_usage
44
- type == :flag ? "--#{long}" : "--#{long}=<#{type}>"
44
+ type == :flag ? "--[no-]#{long}" : "--#{long}=<#{type}>"
45
45
  end
46
46
 
47
47
  def short_usage
@@ -114,6 +114,14 @@ module Climate
114
114
  @stop_on = args
115
115
  end
116
116
 
117
+ def conflicts(*args)
118
+ conflicting_options << args
119
+ end
120
+
121
+ def depends(*args)
122
+ dependent_options << args
123
+ end
124
+
117
125
  def trollop_parser
118
126
  Trollop::Parser.new.tap do |parser|
119
127
  parser.stop_on @stop_on
@@ -121,6 +129,14 @@ module Climate
121
129
  cli_options.each do |option|
122
130
  option.add_to(parser)
123
131
  end
132
+
133
+ conflicting_options.each do |conflicting|
134
+ parser.conflicts(*conflicting)
135
+ end
136
+
137
+ dependent_options.each do |dependent|
138
+ parser.depends(*dependent)
139
+ end
124
140
  end
125
141
  end
126
142
 
@@ -165,6 +181,7 @@ module Climate
165
181
 
166
182
  def parse(arguments, command=self)
167
183
  parser = self.trollop_parser
184
+
168
185
  begin
169
186
  options = parser.parse(arguments)
170
187
  rescue Trollop::CommandlineError => e
@@ -172,6 +189,10 @@ module Climate
172
189
  raise UnexpectedArgumentError.new(m[1], command)
173
190
  elsif (m = /option (.+) must be specified/.match(e.message))
174
191
  raise MissingArgumentError.new(m[1], command)
192
+ elsif /.+ conflicts with .+/.match(e.message)
193
+ raise ConflictingOptionError.new(e.message, command)
194
+ elsif /.+ requires .+/.match(e.message)
195
+ raise MissingArgumentError.new(e.message, command)
175
196
  else
176
197
  raise CommandError.new(e.message, command)
177
198
  end
@@ -189,12 +210,26 @@ module Climate
189
210
  [arguments, options, leftovers]
190
211
  end
191
212
 
192
- def cli_options ; @cli_options ||= [] ; end
193
- def cli_arguments ; @cli_arguments ||= [] ; end
213
+ def cli_options ; @cli_options ||= [] ; end
214
+ def cli_arguments ; @cli_arguments ||= [] ; end
215
+ def conflicting_options ; @conflicting_options ||= [] ; end
216
+ def dependent_options ; @dependent_options ||= [] ; end
194
217
 
195
218
  def has_options? ; not cli_options.empty? ; end
196
219
  def has_arguments? ; not cli_arguments.empty? ; end
197
220
 
221
+ def has_argument?(name)
222
+ cli_arguments.map(&:name).include?(name)
223
+ end
224
+
225
+ def has_required_argument?(name)
226
+ cli_arguments.select(&:required?).map(&:name).include?(name)
227
+ end
228
+
229
+ def has_multi_argument?(name)
230
+ cli_arguments.select(&:multi?).map(&:name).include?(name)
231
+ end
232
+
198
233
  end
199
234
 
200
235
  class Parser
@@ -1,3 +1,3 @@
1
1
  module Climate
2
- VERSION = '0.4.0'
2
+ VERSION = '0.5.0'
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: climate
3
3
  version: !ruby/object:Gem::Version
4
- hash: 15
4
+ hash: 11
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 4
8
+ - 5
9
9
  - 0
10
- version: 0.4.0
10
+ version: 0.5.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Nick Griffiths
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-08-29 00:00:00 Z
18
+ date: 2012-10-05 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: trollop
@@ -23,12 +23,13 @@ dependencies:
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
24
  none: false
25
25
  requirements:
26
- - - ">="
26
+ - - ~>
27
27
  - !ruby/object:Gem::Version
28
28
  hash: 3
29
29
  segments:
30
+ - 2
30
31
  - 0
31
- version: "0"
32
+ version: "2.0"
32
33
  type: :runtime
33
34
  version_requirements: *id001
34
35
  - !ruby/object:Gem::Dependency
@@ -83,14 +84,15 @@ extensions: []
83
84
  extra_rdoc_files: []
84
85
 
85
86
  files:
87
+ - lib/climate.rb
88
+ - lib/climate/help.rb
89
+ - lib/climate/command_compat.rb
86
90
  - lib/climate/version.rb
87
- - lib/climate/help/man.rb
88
- - lib/climate/command.rb
89
- - lib/climate/parser.rb
90
- - lib/climate/errors.rb
91
91
  - lib/climate/script.rb
92
- - lib/climate/help.rb
93
- - lib/climate.rb
92
+ - lib/climate/errors.rb
93
+ - lib/climate/parser.rb
94
+ - lib/climate/command.rb
95
+ - lib/climate/help/man.rb
94
96
  - README.md
95
97
  - bin/climate
96
98
  homepage:
@@ -122,10 +124,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
124
  requirements: []
123
125
 
124
126
  rubyforge_project:
125
- rubygems_version: 1.8.10
127
+ rubygems_version: 1.8.24
126
128
  signing_key:
127
129
  specification_version: 3
128
130
  summary: Library for building command line interfaces
129
131
  test_files: []
130
132
 
131
- has_rdoc: