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 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
@@ -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
- if optional?
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
@@ -24,7 +24,13 @@ module Climate
24
24
  end
25
25
 
26
26
  if subcommands.empty?
27
- instance.run
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(parent, "command #{parent.class.name}" +
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(parent, command_name)
106
+ raise UnknownCommandError.new(command_name, parent)
101
107
  end
102
108
  end
103
109
 
@@ -9,9 +9,9 @@ module Climate
9
9
  class CommandError < Error
10
10
 
11
11
  # The command that raised the error
12
- attr_reader :command_class
12
+ attr_accessor :command_class
13
13
 
14
- def initialize(command_or_class, msg=nil)
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 :exitcode :exit_code
26
+ alias :exitstatus :exit_code
27
27
 
28
- def initialize(command_class, msg, exit_code=1)
28
+ def initialize(msg, exit_code=1)
29
29
  @exit_code = exit_code
30
- super(command_class, msg)
30
+ super(msg)
31
31
  end
32
32
  end
33
33
 
34
- class HelpNeeded < CommandError ; end
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 }.join(' ')
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 }.join(' ')
26
+ command_class.cli_arguments.map {|arg| arg.usage }
27
27
  end
28
- puts("usage: #{ancestor_list} #{opts_usage} #{args_usage}")
28
+ puts("usage: #{ancestor_list} #{(opts_usage + args_usage).join(' ')}")
29
29
  end
30
30
 
31
31
  def print_description
@@ -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
- spec.has_key?(:default)
21
- end
19
+ def optional? ; spec.has_key?(:default) ; end
20
+ def required? ; ! optional? ; end
22
21
 
23
- def required? ; ! optional? ; end
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 spec
30
- @specs ||= parser.specs[@name]
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
- if type == :flag
36
- "-#{short}"
37
- else
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)
@@ -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 << Argument.new(*args)
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 check_arguments(args, command=self)
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
- if args.size > cli_arguments.size
47
- raise UnexpectedArgumentError.new(command, "#{args.size} for #{cli_arguments.size}")
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
- cli_arguments.zip(args).map do |argument, arg_value|
51
- if argument.required? && (arg_value.nil? || arg_value.empty?)
52
- raise MissingArgumentError.new(command, argument.name)
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
- options = parser.parse(arguments)
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
- [[], parser.leftovers]
107
+ [{}, parser.leftovers]
73
108
  else
74
- [self.check_arguments(parser.leftovers), []]
109
+ [self.parse_arguments(parser.leftovers), []]
75
110
  end
76
111
 
77
112
  [arguments, options, leftovers]
@@ -1,3 +1,3 @@
1
1
  module Climate
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
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.exitcode)
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: 27
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
8
+ - 2
9
9
  - 0
10
- version: 0.1.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-15 00:00:00 Z
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