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 +0 -0
- data/lib/climate.rb +16 -19
- data/lib/climate/command.rb +38 -13
- data/lib/climate/command_compat.rb +18 -0
- data/lib/climate/errors.rb +16 -3
- data/lib/climate/help.rb +30 -3
- data/lib/climate/help/man.rb +3 -4
- data/lib/climate/parser.rb +38 -3
- data/lib/climate/version.rb +1 -1
- metadata +15 -14
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
|
-
|
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
|
-
|
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
|
14
|
-
$stderr.puts("
|
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)
|
data/lib/climate/command.rb
CHANGED
@@ -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
|
-
#
|
45
|
-
|
46
|
-
|
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
|
-
#
|
51
|
-
|
52
|
-
|
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
|
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(&:
|
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.
|
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.
|
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
|
data/lib/climate/errors.rb
CHANGED
@@ -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 <
|
49
|
+
class UnexpectedArgumentError < ParsingError ; end
|
40
50
|
|
41
51
|
# Raised when a {Command} is run with insufficient arguments
|
42
|
-
class MissingArgumentError <
|
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 <
|
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(&:
|
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.
|
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
|
data/lib/climate/help/man.rb
CHANGED
@@ -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.
|
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.
|
68
|
+
command_class.command_name
|
70
69
|
end
|
71
70
|
|
72
71
|
def date ; Date.today.strftime('%b, %Y') ; end
|
data/lib/climate/parser.rb
CHANGED
@@ -41,7 +41,7 @@ module Climate
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def long_usage
|
44
|
-
type == :flag ? "
|
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
|
193
|
-
def cli_arguments
|
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
|
data/lib/climate/version.rb
CHANGED
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:
|
4
|
+
hash: 11
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 5
|
9
9
|
- 0
|
10
|
-
version: 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-
|
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/
|
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.
|
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:
|