climate 0.1.0 → 0.2.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/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
|