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