climate 0.1.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 ADDED
File without changes
data/bin/climate ADDED
@@ -0,0 +1,14 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'climate'
4
+ scriptfile = ARGV.shift or begin
5
+ $stderr.puts("Path to ruby script expected")
6
+ $stderr.puts("usage: climate <script> <arguments...>")
7
+ exit 1
8
+ end
9
+
10
+ # if climate is used to invoke a script, we can not use $PROGRAM_NAME to
11
+ # identify the script, so we remember it from here
12
+ $CLIMATE_PROGRAM_NAME = scriptfile
13
+
14
+ load scriptfile
@@ -0,0 +1,30 @@
1
+ module Climate
2
+ class Argument
3
+
4
+ attr_reader :name
5
+ attr_reader :description
6
+
7
+ def initialize(name, description, options={})
8
+ @name = name
9
+ @description = description
10
+ @required = options.fetch(:required, true)
11
+ end
12
+
13
+ def required? ; @required ; end
14
+ def optional? ; ! required? ; end
15
+
16
+ def usage
17
+ string = "<#{name}>"
18
+
19
+ if optional?
20
+ "[#{string}]"
21
+ else
22
+ string
23
+ end
24
+ end
25
+
26
+ def formatted
27
+ required?? name.to_s.upcase : "[#{name.to_s.upcase}]"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,154 @@
1
+ module Climate
2
+
3
+ #
4
+ # A {Command} is a unit of work, intended to be invoked from the command line. It should be
5
+ # extended to either do something itself by implementing run, or just be
6
+ # there as a conduit for subcommands to do their work.
7
+ #
8
+ # See {ParsingMethods} for details on how to specify options and arguments
9
+ #
10
+ class Command
11
+
12
+ class << self
13
+ include ParsingMethods
14
+
15
+ # Create an instance of this command class and run it against the given
16
+ # arguments
17
+ # @param [Array<String>] arguments A list of arguments, ARGV style
18
+ # @param [Hash] options see {#initialize}
19
+ def run(arguments, options={})
20
+ begin
21
+ instance = new(arguments, options)
22
+ rescue Trollop::HelpNeeded
23
+ raise HelpNeeded.new(self)
24
+ end
25
+
26
+ if subcommands.empty?
27
+ instance.run
28
+ else
29
+ find_and_run_subcommand(instance, options)
30
+ end
31
+ end
32
+
33
+ def ancestors(exclude_self=false)
34
+ our_list = exclude_self ? [] : [self]
35
+ parent.nil?? our_list : parent.ancestors + our_list
36
+ end
37
+
38
+ # Supply a name for this command, or return the existing name
39
+ def name(name=nil)
40
+ @name = name if name
41
+ @name
42
+ end
43
+
44
+ # Register this class as being a subcommand of another {Command} class
45
+ # @param [Command] parent_class The parent we hang off of
46
+ def subcommand_of(parent_class)
47
+ raise DefinitionError, 'can not set subcommand before name' unless @name
48
+ parent_class.add_subcommand(self)
49
+ end
50
+
51
+ # Set the description for this command
52
+ # @param [String] string Description/Banner/Help text
53
+ def description(string=nil)
54
+ if string
55
+ @description = string
56
+ else
57
+ @description
58
+ end
59
+ end
60
+
61
+ # Set the parent of this command
62
+ attr_accessor :parent
63
+
64
+ def add_subcommand(subcommand)
65
+ if cli_arguments.empty?
66
+ subcommands << subcommand
67
+ subcommand.parent = self
68
+ stop_on(subcommands.map(&:name))
69
+ else
70
+ raise DefinitionError, 'can not mix subcommands with arguments'
71
+ end
72
+ end
73
+
74
+ def arg(*args)
75
+ if subcommands.empty?
76
+ super(*args)
77
+ else
78
+ raise DefinitionError, 'can not mix subcommands with arguments'
79
+ end
80
+ end
81
+
82
+ def has_subcommands? ; not subcommands.empty? ; end
83
+ def subcommands ; @subcommands ||= [] ; end
84
+
85
+ private
86
+
87
+ def find_and_run_subcommand(parent, options)
88
+ command_name, *arguments = parent.leftovers
89
+
90
+ if command_name.nil?
91
+ raise MissingArgumentError.new(parent, "command #{parent.class.name}" +
92
+ " expects a subcommand as an argument")
93
+ end
94
+
95
+ found = subcommands.find {|c| c.name == command_name }
96
+
97
+ if found
98
+ found.run(arguments, options.merge(:parent => parent))
99
+ else
100
+ raise UnknownCommandError.new(parent, command_name)
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+ # Create an instance of this command to be run. You'll probably want to use
107
+ # {Command.run}
108
+ # @param [Array<String>] arguments ARGV style arguments to be parsed
109
+ # @option options [Command] :parent The parent command, made available as {#parent}
110
+ # @option options [IO] :stdout stream to use as stdout, defaulting to `$stdout`
111
+ # @option options [IO] :stderr stream to use as stderr, defaulting to `$stderr`
112
+ # @option options [IO] :stdin stream to use as stdin, defaulting to `$stdin`
113
+ def initialize(arguments, options={})
114
+ @parent = options[:parent]
115
+
116
+ @stdout = options[:stdout] || $stdout
117
+ @stderr = options[:stderr] || $stderr
118
+ @stdin = options[:stdin] || $stdin
119
+
120
+ @arguments, @options, @leftovers = self.class.parse(arguments)
121
+ end
122
+
123
+ # @return [Hash]
124
+ # Options that were parsed from the command line
125
+ attr_accessor :options
126
+
127
+ # @return [Hash]
128
+ # Arguments that were given on the command line
129
+ attr_accessor :arguments
130
+
131
+ # @return [Array]
132
+ # Unparsed arguments, usually for subcommands
133
+ attr_accessor :leftovers
134
+
135
+ # @return [Command]
136
+ # The parent command, or nil if this is not a subcommand
137
+ attr_accessor :parent
138
+
139
+ # @return [IO]
140
+ # a possibly redirected stream
141
+ attr_accessor :stdout, :stderr, :stdin
142
+
143
+ def ancestor(ancestor_class, include_self=true)
144
+ if include_self && self.class == ancestor_class
145
+ self
146
+ elsif parent.nil?
147
+ raise "no ancestor exists: #{ancestor_class}"
148
+ nil
149
+ else
150
+ parent.ancestor(ancestor_class)
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,45 @@
1
+ module Climate
2
+ class Error < ::StandardError ; end
3
+
4
+ # Raised when {Command} class methods are used incorrectly
5
+ class DefinitionError < Error ; end
6
+
7
+ # Raised by an instance of a {Command} when something went wrong during
8
+ # execution
9
+ class CommandError < Error
10
+
11
+ # The command that raised the error
12
+ attr_reader :command_class
13
+
14
+ def initialize(command_or_class, msg=nil)
15
+ @command_class = command_or_class.is_a?(Command) ? command_or_class.class :
16
+ command_or_class
17
+ super(msg)
18
+ end
19
+ end
20
+
21
+ # Command instances can raise this error to exit
22
+ class ExitException < CommandError
23
+
24
+ attr_reader :exit_code
25
+ # some libraries (popen, process?) refer to this as exitcode without a _
26
+ alias :exitcode :exit_code
27
+
28
+ def initialize(command_class, msg, exit_code=1)
29
+ @exit_code = exit_code
30
+ super(command_class, msg)
31
+ end
32
+ end
33
+
34
+ class HelpNeeded < CommandError ; end
35
+
36
+ # Raised when a {Command} is run with too many arguments
37
+ class UnexpectedArgumentError < CommandError ; end
38
+
39
+ # Raised when a {Command} is run with insufficient arguments
40
+ class MissingArgumentError < CommandError ; end
41
+
42
+ # Raised when a parent {Command} is asked to run a sub command it does not
43
+ # know about
44
+ class UnknownCommandError < CommandError ; end
45
+ end
@@ -0,0 +1,148 @@
1
+ module Climate
2
+ class Help
3
+
4
+ attr_reader :command_class
5
+
6
+ def initialize(command_class, options={})
7
+ @command_class = command_class
8
+ @indent = 0
9
+ @output = options[:output] || $stdout
10
+ end
11
+
12
+ def print
13
+ print_usage
14
+ print_description
15
+ print_options if command_class.has_options? || command_class.has_arguments?
16
+ print_subcommands if command_class.has_subcommands?
17
+ end
18
+
19
+ def print_usage
20
+ ancestor_list = command_class.ancestors.map(&:name).join(' ')
21
+ opts_usage = command_class.cli_options.map {|opt| opt.usage }.join(' ')
22
+ args_usage =
23
+ if command_class.has_subcommands?
24
+ "<subcommand> [<arguments>]"
25
+ else
26
+ command_class.cli_arguments.map {|arg| arg.usage }.join(' ')
27
+ end
28
+ puts("usage: #{ancestor_list} #{opts_usage} #{args_usage}")
29
+ end
30
+
31
+ def print_description
32
+ newline
33
+ puts "Description"
34
+ indent do
35
+ puts(command_class.description || '')
36
+ end
37
+ end
38
+
39
+ def print_subcommands
40
+ newline
41
+ puts "Available subcommands:"
42
+ indent do
43
+ command_class.subcommands.each do |subcommand_class|
44
+ puts "#{subcommand_class.name}"
45
+ end
46
+ end
47
+ end
48
+
49
+ def print_options
50
+ newline
51
+ puts "Options"
52
+ indent do
53
+
54
+ if command_class.has_subcommands?
55
+ puts "<subcommand>"
56
+ indent do
57
+ puts "Name of subcommand to execute"
58
+ end
59
+ newline
60
+ puts "<arguments>"
61
+ indent do
62
+ puts "Arguments for subcommand"
63
+ end
64
+ newline
65
+ end
66
+
67
+ command_class.cli_arguments.each do |argument|
68
+ puts "<#{argument.name}>"
69
+ indent do
70
+ puts argument.description
71
+ end
72
+ newline
73
+ end
74
+
75
+ command_class.cli_options.each do |option|
76
+ puts "#{option.usage(:with_long => true, :hide_optional => true, :separator => ', ')}"
77
+ indent do
78
+ puts option.description
79
+ end
80
+ newline
81
+ end
82
+ end
83
+ end
84
+
85
+ def indent(&block)
86
+ @indent += 1
87
+ yield if block_given?
88
+ unindent if block_given?
89
+ end
90
+
91
+ def unindent
92
+ @indent -= 1
93
+ end
94
+
95
+ def spaces
96
+ @indent * 4
97
+ end
98
+
99
+ def newline
100
+ @output.puts("\n")
101
+ end
102
+
103
+ def puts(string='')
104
+ string.split("\n").each do |line|
105
+ @output.puts((' ' * spaces) + line)
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ # stolen from trollop
112
+ def width #:nodoc:
113
+ @width ||= if $stdout.tty?
114
+ begin
115
+ require 'curses'
116
+ Curses::init_screen
117
+ x = Curses::cols
118
+ Curses::close_screen
119
+ x
120
+ rescue Exception
121
+ 80
122
+ end
123
+ else
124
+ 80
125
+ end
126
+ end
127
+
128
+ def wrap(string)
129
+
130
+ string.split("\n\n").map { |para|
131
+
132
+ words = para.split(/[\n ]/)
133
+ words[1..-1].inject([words.first]) { |m, v|
134
+ new_last_line = m.last + " " + v
135
+
136
+ if new_last_line.length <= (width - spaces)
137
+ m[0...-1] + [new_last_line]
138
+ else
139
+ m + [v]
140
+ end
141
+ }.join("\n")
142
+
143
+ }.join("\n\n")
144
+ end
145
+
146
+
147
+ end
148
+ end
@@ -0,0 +1,61 @@
1
+ module Climate
2
+ class Option
3
+
4
+ attr_reader :name
5
+ attr_reader :description
6
+ attr_reader :options
7
+
8
+ def initialize(name, description, options={})
9
+ @name = name
10
+ @description = description
11
+ @options = options
12
+ end
13
+
14
+ def type ; spec[:type] ; end
15
+ def long ; spec[:long] ; end
16
+ def short ; spec[:short] ; end
17
+ def default ; spec[:default] ; end
18
+
19
+ def optional?
20
+ spec.has_key?(:default)
21
+ end
22
+
23
+ def required? ; ! optional? ; end
24
+
25
+ def parser
26
+ @parser ||= Trollop::Parser.new.tap {|p| add_to(p) }
27
+ end
28
+
29
+ def spec
30
+ @specs ||= parser.specs[@name]
31
+ end
32
+
33
+ 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
48
+ end
49
+
50
+ if optional? && !options.fetch(:hide_optional, false)
51
+ "[#{help}]"
52
+ else
53
+ help
54
+ end
55
+ end
56
+
57
+ def add_to(parser)
58
+ parser.opt(@name, @description, @options)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,95 @@
1
+ module Climate
2
+
3
+ module ParsingMethods
4
+
5
+ def arg(*args)
6
+ raise DefinitionError, "can not define a required argument after an " +
7
+ "optional one" if cli_arguments.any?(&:optional?)
8
+
9
+ cli_arguments << Argument.new(*args)
10
+ end
11
+
12
+ def opt(*args)
13
+ cli_options << Option.new(*args)
14
+ end
15
+
16
+ def stop_on(args)
17
+ @stop_on = args
18
+ end
19
+
20
+ def trollop_parser
21
+ parser = Trollop::Parser.new
22
+
23
+ parser.stop_on @stop_on
24
+
25
+ if cli_arguments.size > 0
26
+ parser.banner ""
27
+ max_length = cli_arguments.map { |h| h.name.to_s.length }.max
28
+ cli_arguments.each do |argument|
29
+ parser.banner(" " + argument.name.to_s.rjust(max_length) + " - #{argument.description}")
30
+ end
31
+ end
32
+
33
+ parser.banner ""
34
+ cli_options.each do |option|
35
+ option.add_to(parser)
36
+ end
37
+ parser
38
+ end
39
+
40
+ def help_banner(out=$stdout)
41
+ trollop_parser.educate(out)
42
+ end
43
+
44
+ def check_arguments(args, command=self)
45
+
46
+ if args.size > cli_arguments.size
47
+ raise UnexpectedArgumentError.new(command, "#{args.size} for #{cli_arguments.size}")
48
+ end
49
+
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)
53
+ end
54
+
55
+ # no arg given is different to an empty arg
56
+ if arg_value.nil?
57
+ {}
58
+ else
59
+ {argument.name => arg_value}
60
+ end
61
+ end.inject {|a,b| a.merge(b) } || {}
62
+ end
63
+
64
+ def parse(arguments)
65
+ parser = self.trollop_parser
66
+ options = parser.parse(arguments)
67
+
68
+ # it would get weird if we allowed arguments alongside options, so
69
+ # lets keep it one or t'other
70
+ arguments, leftovers =
71
+ if @stop_on
72
+ [[], parser.leftovers]
73
+ else
74
+ [self.check_arguments(parser.leftovers), []]
75
+ end
76
+
77
+ [arguments, options, leftovers]
78
+ end
79
+
80
+ def cli_options ; @cli_options ||= [] ; end
81
+ def cli_arguments ; @cli_arguments ||= [] ; end
82
+
83
+ def has_options? ; not cli_options.empty? ; end
84
+ def has_arguments? ; not cli_arguments.empty? ; end
85
+
86
+ end
87
+
88
+ class Parser
89
+ include ParsingMethods
90
+
91
+ def initialize(&block)
92
+ instance_eval(&block) if block_given?
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,43 @@
1
+ module Climate
2
+
3
+ # Module that you can extend any object with to turn it in to a climate
4
+ # script. See the readme for intended usage, but follows the same pattern
5
+ # as the command, with the exception that it does not allow subcommands
6
+ module Script
7
+
8
+ def self.extended(othermodule)
9
+ if @included.nil?
10
+ @included = true
11
+ at_exit do
12
+ Climate.with_standard_exception_handling do
13
+ othermodule.send(:parse_argv)
14
+ othermodule.send(:run)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ include ParsingMethods
21
+ # Set the description for this script
22
+ # @param [String] string Description/Banner/Help text
23
+ def description(string=nil)
24
+ if string.nil?
25
+ @description
26
+ else
27
+ @description = string
28
+ end
29
+ end
30
+
31
+ def parse_argv
32
+ @arguments, @options, @leftovers = self.parse(ARGV)
33
+ end
34
+
35
+ attr_reader :arguments, :options, :leftovers
36
+
37
+ def ancestors ; [self] ; end
38
+ def name ; File.basename($CLIMATE_PROGRAM_NAME || $PROGRAM_NAME) ; end
39
+
40
+ def has_subcommands? ; false ; end
41
+
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ module Climate
2
+ VERSION = '0.1.0'
3
+ end
data/lib/climate.rb ADDED
@@ -0,0 +1,40 @@
1
+ require 'trollop'
2
+
3
+ module Climate
4
+ def self.with_standard_exception_handling(&block)
5
+ begin
6
+ yield
7
+ rescue ExitException => e
8
+ $stderr.puts(e.message)
9
+ exit(e.exitcode)
10
+ rescue HelpNeeded => e
11
+ print_usage(e.command_class)
12
+ rescue UnknownCommandError => e
13
+ $stderr.puts("Unknown command: #{e.message}")
14
+ print_usage(e.command_class)
15
+ rescue MissingArgumentError => e
16
+ $stderr.puts("Missing argument: #{e.message}")
17
+ print_usage(e.command_class)
18
+ rescue => e
19
+ $stderr.puts("Unexpected error: #{e.message}")
20
+ $stderr.puts(e.backtrace)
21
+ end
22
+ end
23
+
24
+ def self.print_usage(command_class, options={})
25
+ help = Help.new(command_class)
26
+
27
+ help.print
28
+ end
29
+
30
+ def run(&block)
31
+ end
32
+ end
33
+
34
+ require 'climate/errors'
35
+ require 'climate/argument'
36
+ require 'climate/option'
37
+ require 'climate/parser'
38
+ require 'climate/command'
39
+ require 'climate/help'
40
+ require 'climate/script'
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: climate
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Nick Griffiths
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-07-15 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: trollop
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: minitest
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: yard
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :development
61
+ version_requirements: *id003
62
+ description: Library, not a framework, for building command line interfaces to your ruby application
63
+ email:
64
+ - nicobrevin@gmail.com
65
+ executables:
66
+ - climate
67
+ extensions: []
68
+
69
+ extra_rdoc_files: []
70
+
71
+ files:
72
+ - lib/climate/version.rb
73
+ - lib/climate/argument.rb
74
+ - lib/climate/command.rb
75
+ - lib/climate/option.rb
76
+ - lib/climate/parser.rb
77
+ - lib/climate/errors.rb
78
+ - lib/climate/script.rb
79
+ - lib/climate/help.rb
80
+ - lib/climate.rb
81
+ - README
82
+ - bin/climate
83
+ homepage:
84
+ licenses: []
85
+
86
+ post_install_message:
87
+ rdoc_options: []
88
+
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ hash: 3
97
+ segments:
98
+ - 0
99
+ version: "0"
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ hash: 3
106
+ segments:
107
+ - 0
108
+ version: "0"
109
+ requirements: []
110
+
111
+ rubyforge_project:
112
+ rubygems_version: 1.8.10
113
+ signing_key:
114
+ specification_version: 3
115
+ summary: Library for building command line interfaces
116
+ test_files: []
117
+