climate 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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: