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