climate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+