climate 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +60 -0
- data/lib/climate/argument.rb +4 -6
- data/lib/climate/command.rb +10 -4
- data/lib/climate/errors.rb +8 -6
- data/lib/climate/help/man.rb +108 -0
- data/lib/climate/help.rb +4 -4
- data/lib/climate/option.rb +13 -20
- data/lib/climate/parser.rb +47 -12
- data/lib/climate/version.rb +1 -1
- data/lib/climate.rb +10 -2
- metadata +21 -5
- data/README +0 -0
data/README.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# Climate
|
2
|
+
|
3
|
+
Yet another bloody CLI library for ruby, based on, and inspired by, the
|
4
|
+
magnificence that is trollop, with more of a mind for building up a git-like
|
5
|
+
CLI to access your application without enforcing a particular style of project
|
6
|
+
structure.
|
7
|
+
|
8
|
+
Designed for both simple and more complex cases.
|
9
|
+
|
10
|
+
# Easy
|
11
|
+
|
12
|
+
Useful for one-shot scripts:
|
13
|
+
|
14
|
+
#! /usr/bin/env climate
|
15
|
+
# the shebang is optional, you can just load the script with
|
16
|
+
# `ruby -r rubygems -r climate script.rb`
|
17
|
+
extend Climate::Script
|
18
|
+
description "Do something arbitrary to a file"
|
19
|
+
|
20
|
+
opt :log, "Whether to log to stdout" :default => false
|
21
|
+
arg :path "Path to input file"
|
22
|
+
|
23
|
+
def run
|
24
|
+
file = File.open(arguments[:path], 'r')
|
25
|
+
puts("loaded #{file}") if options[:log]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Medium
|
29
|
+
|
30
|
+
This style is intended for embedding a CLI in to your existing application.
|
31
|
+
|
32
|
+
class Parent < Climate::Command
|
33
|
+
name 'thing'
|
34
|
+
description "App that does it all, yet without fuss"
|
35
|
+
opt :log, "Whether to log to stdout" :default => false
|
36
|
+
end
|
37
|
+
|
38
|
+
class Arbitrary < Climate::Command
|
39
|
+
name 'arbitrary'
|
40
|
+
subcommand_of, Parent
|
41
|
+
description "Do something arbitrary to a file"
|
42
|
+
arg :path "Path to input file"
|
43
|
+
|
44
|
+
def run
|
45
|
+
file = File.open(arguments[:path], 'r')
|
46
|
+
puts("loaded #{file}") if parent.options[:log]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Climate.with_standard_exception_handling do
|
51
|
+
Parent.run(ARGV)
|
52
|
+
end
|
53
|
+
|
54
|
+
There is a working example, `example.rb` that you can test out with
|
55
|
+
|
56
|
+
ruby -rrubygems -rclimate example.rb --log /tmp/file
|
57
|
+
|
58
|
+
or
|
59
|
+
|
60
|
+
ruby -rrubygems -rclimate example.rb --help
|
data/lib/climate/argument.rb
CHANGED
@@ -8,19 +8,17 @@ module Climate
|
|
8
8
|
@name = name
|
9
9
|
@description = description
|
10
10
|
@required = options.fetch(:required, true)
|
11
|
+
@multi = options.fetch(:multi, false)
|
11
12
|
end
|
12
13
|
|
13
14
|
def required? ; @required ; end
|
14
15
|
def optional? ; ! required? ; end
|
16
|
+
def multi? ; @multi ; end
|
15
17
|
|
16
18
|
def usage
|
17
19
|
string = "<#{name}>"
|
18
|
-
|
19
|
-
|
20
|
-
"[#{string}]"
|
21
|
-
else
|
22
|
-
string
|
23
|
-
end
|
20
|
+
string += '...' if multi?
|
21
|
+
optional?? "[#{string}]" : string
|
24
22
|
end
|
25
23
|
|
26
24
|
def formatted
|
data/lib/climate/command.rb
CHANGED
@@ -24,7 +24,13 @@ module Climate
|
|
24
24
|
end
|
25
25
|
|
26
26
|
if subcommands.empty?
|
27
|
-
|
27
|
+
begin
|
28
|
+
instance.run
|
29
|
+
rescue Climate::CommandError => e
|
30
|
+
# make it easier on users
|
31
|
+
e.command_class = self if e.command_class.nil?
|
32
|
+
raise
|
33
|
+
end
|
28
34
|
else
|
29
35
|
find_and_run_subcommand(instance, options)
|
30
36
|
end
|
@@ -88,8 +94,8 @@ module Climate
|
|
88
94
|
command_name, *arguments = parent.leftovers
|
89
95
|
|
90
96
|
if command_name.nil?
|
91
|
-
raise MissingArgumentError.new(
|
92
|
-
" expects a subcommand as an argument")
|
97
|
+
raise MissingArgumentError.new("command #{parent.class.name}" +
|
98
|
+
" expects a subcommand as an argument", parent)
|
93
99
|
end
|
94
100
|
|
95
101
|
found = subcommands.find {|c| c.name == command_name }
|
@@ -97,7 +103,7 @@ module Climate
|
|
97
103
|
if found
|
98
104
|
found.run(arguments, options.merge(:parent => parent))
|
99
105
|
else
|
100
|
-
raise UnknownCommandError.new(
|
106
|
+
raise UnknownCommandError.new(command_name, parent)
|
101
107
|
end
|
102
108
|
end
|
103
109
|
|
data/lib/climate/errors.rb
CHANGED
@@ -9,9 +9,9 @@ module Climate
|
|
9
9
|
class CommandError < Error
|
10
10
|
|
11
11
|
# The command that raised the error
|
12
|
-
|
12
|
+
attr_accessor :command_class
|
13
13
|
|
14
|
-
def initialize(
|
14
|
+
def initialize(msg=nil, command_or_class=nil)
|
15
15
|
@command_class = command_or_class.is_a?(Command) ? command_or_class.class :
|
16
16
|
command_or_class
|
17
17
|
super(msg)
|
@@ -23,15 +23,17 @@ module Climate
|
|
23
23
|
|
24
24
|
attr_reader :exit_code
|
25
25
|
# some libraries (popen, process?) refer to this as exitcode without a _
|
26
|
-
alias :
|
26
|
+
alias :exitstatus :exit_code
|
27
27
|
|
28
|
-
def initialize(
|
28
|
+
def initialize(msg, exit_code=1)
|
29
29
|
@exit_code = exit_code
|
30
|
-
super(
|
30
|
+
super(msg)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
class HelpNeeded < CommandError
|
34
|
+
class HelpNeeded < CommandError
|
35
|
+
def initialize(command_class) ; super(nil, command_class) ; end
|
36
|
+
end
|
35
37
|
|
36
38
|
# Raised when a {Command} is run with too many arguments
|
37
39
|
class UnexpectedArgumentError < CommandError ; end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
begin
|
2
|
+
require 'erubis'
|
3
|
+
rescue LoadError => e
|
4
|
+
$stderr.puts("erubis gem is required for man output")
|
5
|
+
exit 1
|
6
|
+
end
|
7
|
+
|
8
|
+
class Climate::Help
|
9
|
+
# can produce a troff file suitable for man
|
10
|
+
class Man
|
11
|
+
|
12
|
+
# Eat my own dog food
|
13
|
+
class Script < Climate::Command
|
14
|
+
name 'man'
|
15
|
+
description 'Creates man/nroff output for a command'
|
16
|
+
|
17
|
+
arg :command_class, "name of class that defines the command, i.e. Foo::Bar::Command",
|
18
|
+
:type => :string, :required => true
|
19
|
+
|
20
|
+
opt :out_file, "Name of a file to write nroff output to. Defaults to stdout",
|
21
|
+
:type => :string, :required => false
|
22
|
+
|
23
|
+
opt :template, "Path to an alternative template to use. The default " +
|
24
|
+
"produces output for man/nroff, but you can change it to whatever " +
|
25
|
+
"you like", :required => false, :type => :string
|
26
|
+
|
27
|
+
def run
|
28
|
+
out_file = (of = options[:out_file]) && File.open(of, 'w') || $stdout
|
29
|
+
|
30
|
+
command_class = arguments[:command_class].split('::').
|
31
|
+
inject(Object) {|m,v| m.const_get(v) }
|
32
|
+
|
33
|
+
template_file = options[:template] || File.join(
|
34
|
+
File.dirname(__FILE__), 'man.erb')
|
35
|
+
|
36
|
+
Man.new(command_class, :output => out_file,
|
37
|
+
:template_file => template_file).print
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Presenter
|
42
|
+
|
43
|
+
def self.proxy(method_name)
|
44
|
+
define_method(method_name) do
|
45
|
+
@command_class.send(method_name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize(command_class)
|
50
|
+
@command_class = command_class
|
51
|
+
end
|
52
|
+
|
53
|
+
attr_reader :command_class
|
54
|
+
|
55
|
+
public :binding
|
56
|
+
|
57
|
+
def full_name
|
58
|
+
stack = []
|
59
|
+
command_ptr = command_class
|
60
|
+
while command_ptr
|
61
|
+
stack.unshift(command_ptr.name)
|
62
|
+
command_ptr = command_ptr.parent
|
63
|
+
end
|
64
|
+
|
65
|
+
stack.join(' ')
|
66
|
+
end
|
67
|
+
|
68
|
+
def short_name
|
69
|
+
command_class.name
|
70
|
+
end
|
71
|
+
|
72
|
+
def date ; Date.today.strftime('%b, %Y') ; end
|
73
|
+
|
74
|
+
proxy :has_subcommands?
|
75
|
+
proxy :cli_options
|
76
|
+
proxy :cli_arguments
|
77
|
+
proxy :has_options?
|
78
|
+
proxy :has_arguments?
|
79
|
+
|
80
|
+
def paragraphs
|
81
|
+
command_class.description.split("\n\n")
|
82
|
+
end
|
83
|
+
|
84
|
+
def short_description
|
85
|
+
command_class.description.split(".").first
|
86
|
+
end
|
87
|
+
|
88
|
+
def subcommands
|
89
|
+
command_class.subcommands.map {|c| self.class.new(c) }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
attr_reader :command_class
|
94
|
+
|
95
|
+
def initialize(command_class, options={})
|
96
|
+
@command_class = command_class
|
97
|
+
@output = options[:output] || $stdout
|
98
|
+
@template_file = options[:template_file]
|
99
|
+
end
|
100
|
+
|
101
|
+
def print
|
102
|
+
template = Erubis::Eruby.new(File.read(@template_file))
|
103
|
+
presenter = Presenter.new(command_class)
|
104
|
+
@output.puts(template.result(presenter.binding))
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
data/lib/climate/help.rb
CHANGED
@@ -18,14 +18,14 @@ module Climate
|
|
18
18
|
|
19
19
|
def print_usage
|
20
20
|
ancestor_list = command_class.ancestors.map(&:name).join(' ')
|
21
|
-
opts_usage = command_class.cli_options.map {|opt| opt.usage }
|
21
|
+
opts_usage = command_class.cli_options.map {|opt| opt.usage }
|
22
22
|
args_usage =
|
23
23
|
if command_class.has_subcommands?
|
24
|
-
"<subcommand> [<arguments
|
24
|
+
["<subcommand> [<arguments>...]"]
|
25
25
|
else
|
26
|
-
command_class.cli_arguments.map {|arg| arg.usage }
|
26
|
+
command_class.cli_arguments.map {|arg| arg.usage }
|
27
27
|
end
|
28
|
-
puts("usage: #{ancestor_list} #{opts_usage
|
28
|
+
puts("usage: #{ancestor_list} #{(opts_usage + args_usage).join(' ')}")
|
29
29
|
end
|
30
30
|
|
31
31
|
def print_description
|
data/lib/climate/option.rb
CHANGED
@@ -16,35 +16,28 @@ module Climate
|
|
16
16
|
def short ; spec[:short] ; end
|
17
17
|
def default ; spec[:default] ; end
|
18
18
|
|
19
|
-
def optional?
|
20
|
-
|
21
|
-
end
|
19
|
+
def optional? ; spec.has_key?(:default) ; end
|
20
|
+
def required? ; ! optional? ; end
|
22
21
|
|
23
|
-
def
|
22
|
+
def spec ; @specs ||= parser.specs[@name] ; end
|
24
23
|
|
25
24
|
def parser
|
26
25
|
@parser ||= Trollop::Parser.new.tap {|p| add_to(p) }
|
27
26
|
end
|
28
27
|
|
29
|
-
def
|
30
|
-
|
28
|
+
def long_usage
|
29
|
+
type == :flag ? "--#{long}" : "--#{long}=<#{type}>"
|
30
|
+
end
|
31
|
+
|
32
|
+
def short_usage
|
33
|
+
short && (type == :flag ? "-#{short}" : "-#{short}<#{type}>")
|
31
34
|
end
|
32
35
|
|
33
36
|
def usage(options={})
|
34
|
-
help =
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
"-#{short}<#{type}>"
|
39
|
-
end
|
40
|
-
|
41
|
-
if options[:with_long]
|
42
|
-
help = help + options.fetch(:separator, '|') +
|
43
|
-
if type == :flag
|
44
|
-
"--#{long}"
|
45
|
-
else
|
46
|
-
"--#{long}=<#{type}>"
|
47
|
-
end
|
37
|
+
help = short_usage || long_usage
|
38
|
+
|
39
|
+
if options[:with_long] && (long_usage != help)
|
40
|
+
help = [help, long_usage].compact.join(options.fetch(:separator, '|'))
|
48
41
|
end
|
49
42
|
|
50
43
|
if optional? && !options.fetch(:hide_optional, false)
|
data/lib/climate/parser.rb
CHANGED
@@ -3,10 +3,16 @@ module Climate
|
|
3
3
|
module ParsingMethods
|
4
4
|
|
5
5
|
def arg(*args)
|
6
|
+
|
7
|
+
arg = Argument.new(*args)
|
8
|
+
|
9
|
+
raise DefinitionError, "can not define more arguments after a multi " +
|
10
|
+
" argument" if cli_arguments.any?(&:multi?)
|
11
|
+
|
6
12
|
raise DefinitionError, "can not define a required argument after an " +
|
7
|
-
"optional one" if cli_arguments.any?(&:optional?)
|
13
|
+
"optional one" if cli_arguments.any?(&:optional?) && arg.required?
|
8
14
|
|
9
|
-
cli_arguments <<
|
15
|
+
cli_arguments << arg
|
10
16
|
end
|
11
17
|
|
12
18
|
def opt(*args)
|
@@ -41,17 +47,36 @@ module Climate
|
|
41
47
|
trollop_parser.educate(out)
|
42
48
|
end
|
43
49
|
|
44
|
-
def
|
50
|
+
def parse_arguments(args, command=self)
|
51
|
+
|
52
|
+
arg_list = cli_arguments
|
53
|
+
|
54
|
+
if arg_list.none?(&:multi?) && args.size > arg_list.size
|
55
|
+
raise UnexpectedArgumentError.new("#{args.size} for #{arg_list.size}", command)
|
56
|
+
end
|
45
57
|
|
46
|
-
|
47
|
-
|
58
|
+
# mung the last arguments to appear as one for multi args, this is fairly
|
59
|
+
# ugly - thank heavens for unit tests
|
60
|
+
if arg_list.last && arg_list.last.multi?
|
61
|
+
multi_arg = arg_list.last
|
62
|
+
last_args = args[(arg_list.size - 1)..-1] || []
|
63
|
+
|
64
|
+
# depending on the number of args that were supplied, you may get nil
|
65
|
+
# or an empty array because of how slicing works, either way we want nil
|
66
|
+
# if no args were supplied so `required?` detection works below
|
67
|
+
args = args[0...(arg_list.size - 1)] +
|
68
|
+
[last_args.empty?? nil : last_args].compact
|
48
69
|
end
|
49
70
|
|
50
|
-
|
51
|
-
|
52
|
-
|
71
|
+
arg_list.zip(args).map do |argument, arg_value|
|
72
|
+
|
73
|
+
if argument.required? && arg_value.nil?
|
74
|
+
raise MissingArgumentError.new(argument.name, command)
|
53
75
|
end
|
54
76
|
|
77
|
+
# empty list is nil for multi arg
|
78
|
+
arg_value = [] if argument.multi? && arg_value.nil?
|
79
|
+
|
55
80
|
# no arg given is different to an empty arg
|
56
81
|
if arg_value.nil?
|
57
82
|
{}
|
@@ -61,17 +86,27 @@ module Climate
|
|
61
86
|
end.inject {|a,b| a.merge(b) } || {}
|
62
87
|
end
|
63
88
|
|
64
|
-
def parse(arguments)
|
89
|
+
def parse(arguments, command=self)
|
65
90
|
parser = self.trollop_parser
|
66
|
-
|
91
|
+
begin
|
92
|
+
options = parser.parse(arguments)
|
93
|
+
rescue Trollop::CommandlineError => e
|
94
|
+
if (m = /unknown argument '(.+)'/.match(e.message))
|
95
|
+
raise UnexpectedArgumentError.new(m[1], command)
|
96
|
+
elsif (m = /option (.+) must be specified/.match(e.message))
|
97
|
+
raise MissingArgumentError.new(m[1], command)
|
98
|
+
else
|
99
|
+
raise
|
100
|
+
end
|
101
|
+
end
|
67
102
|
|
68
103
|
# it would get weird if we allowed arguments alongside options, so
|
69
104
|
# lets keep it one or t'other
|
70
105
|
arguments, leftovers =
|
71
106
|
if @stop_on
|
72
|
-
[
|
107
|
+
[{}, parser.leftovers]
|
73
108
|
else
|
74
|
-
[self.
|
109
|
+
[self.parse_arguments(parser.leftovers), []]
|
75
110
|
end
|
76
111
|
|
77
112
|
[arguments, options, leftovers]
|
data/lib/climate/version.rb
CHANGED
data/lib/climate.rb
CHANGED
@@ -6,18 +6,26 @@ module Climate
|
|
6
6
|
yield
|
7
7
|
rescue ExitException => e
|
8
8
|
$stderr.puts(e.message)
|
9
|
-
exit(e.
|
9
|
+
exit(e.exit_code)
|
10
10
|
rescue HelpNeeded => e
|
11
11
|
print_usage(e.command_class)
|
12
|
+
exit(0)
|
13
|
+
rescue UnexpectedArgumentError => e
|
14
|
+
$stderr.puts("Unknown argument: #{e.message}")
|
15
|
+
print_usage(e.command_class)
|
16
|
+
exit(1)
|
12
17
|
rescue UnknownCommandError => e
|
13
18
|
$stderr.puts("Unknown command: #{e.message}")
|
14
19
|
print_usage(e.command_class)
|
20
|
+
exit(1)
|
15
21
|
rescue MissingArgumentError => e
|
16
22
|
$stderr.puts("Missing argument: #{e.message}")
|
17
23
|
print_usage(e.command_class)
|
24
|
+
exit(1)
|
18
25
|
rescue => e
|
19
|
-
$stderr.puts("Unexpected error: #{e.message}")
|
26
|
+
$stderr.puts("Unexpected error: #{e.class.name} - #{e.message}")
|
20
27
|
$stderr.puts(e.backtrace)
|
28
|
+
exit(2)
|
21
29
|
end
|
22
30
|
end
|
23
31
|
|
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: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 2
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.2.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-07-
|
18
|
+
date: 2012-07-23 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: trollop
|
@@ -59,6 +59,20 @@ dependencies:
|
|
59
59
|
version: "0"
|
60
60
|
type: :development
|
61
61
|
version_requirements: *id003
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: popen4
|
64
|
+
prerelease: false
|
65
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
hash: 3
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
version: "0"
|
74
|
+
type: :development
|
75
|
+
version_requirements: *id004
|
62
76
|
description: Library, not a framework, for building command line interfaces to your ruby application
|
63
77
|
email:
|
64
78
|
- nicobrevin@gmail.com
|
@@ -70,6 +84,7 @@ extra_rdoc_files: []
|
|
70
84
|
|
71
85
|
files:
|
72
86
|
- lib/climate/version.rb
|
87
|
+
- lib/climate/help/man.rb
|
73
88
|
- lib/climate/argument.rb
|
74
89
|
- lib/climate/command.rb
|
75
90
|
- lib/climate/option.rb
|
@@ -78,7 +93,7 @@ files:
|
|
78
93
|
- lib/climate/script.rb
|
79
94
|
- lib/climate/help.rb
|
80
95
|
- lib/climate.rb
|
81
|
-
- README
|
96
|
+
- README.md
|
82
97
|
- bin/climate
|
83
98
|
homepage:
|
84
99
|
licenses: []
|
@@ -115,3 +130,4 @@ specification_version: 3
|
|
115
130
|
summary: Library for building command line interfaces
|
116
131
|
test_files: []
|
117
132
|
|
133
|
+
has_rdoc:
|
data/README
DELETED
File without changes
|