caty 0.0.1
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/HISTORY.markdown +7 -0
- data/LICENSE.txt +22 -0
- data/README.markdown +92 -0
- data/Rakefile +33 -0
- data/VERSION +1 -0
- data/caty.gemspec +73 -0
- data/lib/caty.rb +341 -0
- data/lib/caty/converters.rb +101 -0
- data/lib/caty/errors.rb +17 -0
- data/lib/caty/global_option.rb +40 -0
- data/lib/caty/has_description.rb +26 -0
- data/lib/caty/help_system.rb +107 -0
- data/lib/caty/helpers.rb +19 -0
- data/lib/caty/indirection.rb +26 -0
- data/lib/caty/option.rb +100 -0
- data/lib/caty/option_array.rb +30 -0
- data/lib/caty/option_constructor.rb +89 -0
- data/lib/caty/task.rb +74 -0
- data/lib/caty/task_hash.rb +75 -0
- data/test/kitty.rb +87 -0
- data/test/test_caty.rb +100 -0
- data/test/test_option.rb +81 -0
- data/test/test_option_array.rb +25 -0
- data/test/test_structure.rb +72 -0
- data/test/test_task.rb +28 -0
- metadata +94 -0
@@ -0,0 +1,101 @@
|
|
1
|
+
#
|
2
|
+
# Contains all the option converters.
|
3
|
+
#
|
4
|
+
|
5
|
+
#
|
6
|
+
# Base class for all option converters.
|
7
|
+
# Offers some metaprogramming.
|
8
|
+
#
|
9
|
+
class Caty::Converter
|
10
|
+
|
11
|
+
include Caty::Helpers
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
attr_accessor :allowed_defaults
|
16
|
+
|
17
|
+
#
|
18
|
+
# Registers a converter subclass for a type (symbol)
|
19
|
+
# and some default value classes
|
20
|
+
#
|
21
|
+
def type( type, *allowed_defaults )
|
22
|
+
@@types ||= Hash.new
|
23
|
+
@@types[type] = self
|
24
|
+
Caty::OptionConstructor.register(type)
|
25
|
+
@allowed_defaults = allowed_defaults
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Returns a hash containing
|
30
|
+
# :type => Class
|
31
|
+
# for all converter subclasses
|
32
|
+
#
|
33
|
+
def types
|
34
|
+
@@types ||= Hash.new
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Converter for boolean values:
|
43
|
+
# %w(true false 1 0) << nil
|
44
|
+
#
|
45
|
+
class Caty::BooleanConverter < Caty::Converter
|
46
|
+
|
47
|
+
type(:boolean, TrueClass, FalseClass)
|
48
|
+
|
49
|
+
def convert( value )
|
50
|
+
case value
|
51
|
+
when 'true', '1', nil then true
|
52
|
+
when 'false', '0' then false
|
53
|
+
else
|
54
|
+
e = Caty::OptionArgumentError.new
|
55
|
+
e.expected = '0, 1, true, false or no argument'
|
56
|
+
raise e
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Converter for string values:
|
64
|
+
# /.*/
|
65
|
+
#
|
66
|
+
class Caty::StringConverter < Caty::Converter
|
67
|
+
|
68
|
+
type(:string, String)
|
69
|
+
|
70
|
+
def convert( value )
|
71
|
+
case value
|
72
|
+
when nil
|
73
|
+
e = Caty::OptionArgumentError.new
|
74
|
+
e.expected = 'a string'
|
75
|
+
raise e
|
76
|
+
else value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Converter for integer values:
|
84
|
+
# /^[+-]?[0-9]+$/
|
85
|
+
#
|
86
|
+
class Caty::IntegerConverter < Caty::Converter
|
87
|
+
|
88
|
+
type(:integer, Fixnum)
|
89
|
+
|
90
|
+
def convert( value )
|
91
|
+
case value
|
92
|
+
when %r{^[+-]?[0-9]+$} then value.to_i
|
93
|
+
else
|
94
|
+
e = Caty::OptionArgumentError.new
|
95
|
+
e.expected = 'an integer'
|
96
|
+
raise e
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
data/lib/caty/errors.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#
|
2
|
+
# Contains error classes used by Caty.
|
3
|
+
#
|
4
|
+
|
5
|
+
class Caty::NoSuchTaskError < ArgumentError
|
6
|
+
end
|
7
|
+
|
8
|
+
class Caty::OptionArgumentError < ArgumentError
|
9
|
+
|
10
|
+
attr_accessor :expected, :option
|
11
|
+
|
12
|
+
def message
|
13
|
+
"Bad argument to option `#{self.option}'. Expected #{self.expected}."
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
#
|
2
|
+
# Contains the GlobalOption class.
|
3
|
+
#
|
4
|
+
|
5
|
+
#
|
6
|
+
# Represents a single global option.
|
7
|
+
#
|
8
|
+
# A GlobalOption object is created for every option
|
9
|
+
# specified via Caty::global_options().
|
10
|
+
# The sum of all global options parsed is accessible
|
11
|
+
# via the Caty#global_options() method.
|
12
|
+
#
|
13
|
+
class Caty::GlobalOption < Caty::Option
|
14
|
+
|
15
|
+
include Caty::HasDescription
|
16
|
+
|
17
|
+
#
|
18
|
+
# Returns a string representation to be used by
|
19
|
+
# the help system.
|
20
|
+
#
|
21
|
+
def to_help
|
22
|
+
[ self.to_s, self.short_description, self.description ]
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s( ljust = 0 )
|
26
|
+
returning('') do |str|
|
27
|
+
str << "#{self.prefix}#{@name}=#{
|
28
|
+
@default.nil? ? '' : @default.inspect
|
29
|
+
}".ljust(ljust)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def prefix
|
36
|
+
'--'
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
#
|
2
|
+
# Contains the HasDescription module.
|
3
|
+
#
|
4
|
+
|
5
|
+
#
|
6
|
+
# Mixin for everything that can has description ;-)
|
7
|
+
#
|
8
|
+
module Caty::HasDescription
|
9
|
+
|
10
|
+
attr_accessor :description
|
11
|
+
|
12
|
+
#
|
13
|
+
# If a description was set for this object,
|
14
|
+
# this will return the first line of that desciption.
|
15
|
+
# Else nil will be returned.
|
16
|
+
#
|
17
|
+
def short_description
|
18
|
+
if self.description.nil?
|
19
|
+
nil
|
20
|
+
else
|
21
|
+
self.description[%r{^[^\n\r]*}]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,107 @@
|
|
1
|
+
#
|
2
|
+
# Contains the objects related to the help feature.
|
3
|
+
#
|
4
|
+
|
5
|
+
#
|
6
|
+
# Will be mixed into the Caty class.
|
7
|
+
#
|
8
|
+
module Caty::HelpSystem
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
#
|
13
|
+
# Interface for Caty's help system.
|
14
|
+
# Displays either a general help page for all tasks
|
15
|
+
# and options known to Caty in case no argument was given;
|
16
|
+
# or a page descriping a certain task/option.
|
17
|
+
#
|
18
|
+
def help( task_or_option = nil )
|
19
|
+
if task_or_option.nil?
|
20
|
+
help_overview
|
21
|
+
else
|
22
|
+
token_help(task_or_option.to_sym)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Applies the standard help task
|
28
|
+
#
|
29
|
+
def help_task
|
30
|
+
self.class_eval do
|
31
|
+
desc('[TOKEN]', cut("
|
32
|
+
Provides help about the program.
|
33
|
+
A TOKEN is the name of a task or an option, e.g. 'help' for
|
34
|
+
the help task or 'version' for the --version option.
|
35
|
+
If a TOKEN is given, help for that specific TOKEN is displayed.
|
36
|
+
Otherwise, an overview of all the tasks and options is shown.
|
37
|
+
"))
|
38
|
+
def help( task_or_option = nil )
|
39
|
+
self.class.help(task_or_option)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
#
|
47
|
+
# Displays the auto-generated help for all tasks,
|
48
|
+
# options and global options known to Caty.
|
49
|
+
#
|
50
|
+
def help_overview
|
51
|
+
task_descs = @tasks.to_a.reject { |task| task.is_a?(Caty::Indirection) }.map(&:to_help)
|
52
|
+
goption_descs = @global_options.map(&:to_help)
|
53
|
+
column_width = (task_descs + goption_descs).map(&:first).map(&:length).max
|
54
|
+
|
55
|
+
$stdout.puts 'Commands'
|
56
|
+
$stdout.puts '========'
|
57
|
+
$stdout.puts
|
58
|
+
|
59
|
+
task_descs.each do |desc|
|
60
|
+
$stdout.puts "#{desc[0].ljust(column_width)} # #{desc[1]}"
|
61
|
+
end
|
62
|
+
|
63
|
+
$stdout.puts
|
64
|
+
$stdout.puts 'Global Options'
|
65
|
+
$stdout.puts '=============='
|
66
|
+
$stdout.puts
|
67
|
+
|
68
|
+
goption_descs.each do |desc|
|
69
|
+
$stdout.puts "#{desc[0].ljust(column_width)} # #{desc[1]}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Displays the auto-generated help for a certain task/option.
|
75
|
+
#
|
76
|
+
def token_help( token )
|
77
|
+
goptions = @global_options
|
78
|
+
tasks = @tasks.to_a
|
79
|
+
item = @tasks.resolve(token) || goptions.find { |item| item.name == token }
|
80
|
+
|
81
|
+
if item.nil?
|
82
|
+
$stdout.puts "Sorry, but I don't know `#{token}'"
|
83
|
+
else
|
84
|
+
aliases = @tasks.find_all do |_,task|
|
85
|
+
task.is_a?(Caty::Indirection) and task.target == item.name
|
86
|
+
end.map(&:first).join(', ')
|
87
|
+
|
88
|
+
help_text = item.to_help
|
89
|
+
$stdout.puts help_text[0]
|
90
|
+
$stdout.puts
|
91
|
+
$stdout.puts help_text[2]
|
92
|
+
|
93
|
+
unless aliases.empty?
|
94
|
+
$stdout.puts
|
95
|
+
$stdout.puts "Aliases: #{aliases}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Returns the tasks as an array, with all indirections and removed.
|
102
|
+
#
|
103
|
+
def taskarray
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
data/lib/caty/helpers.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#
|
2
|
+
# Contains the Indirection class.
|
3
|
+
#
|
4
|
+
|
5
|
+
#
|
6
|
+
# Used to map things to other tasks and resolve
|
7
|
+
# those mappings at runtime.
|
8
|
+
#
|
9
|
+
Caty::Indirection = Struct.new(:name, :target) do
|
10
|
+
|
11
|
+
#
|
12
|
+
# Follows the indirection until it reaches
|
13
|
+
# an end point and returns that.
|
14
|
+
#
|
15
|
+
def resolve( task_hash )
|
16
|
+
next_item = task_hash[self.target]
|
17
|
+
|
18
|
+
if next_item.is_a?(Caty::Indirection)
|
19
|
+
next_item.resolve(task_hash)
|
20
|
+
else
|
21
|
+
next_item
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
data/lib/caty/option.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
#
|
2
|
+
# Contains the Option class.
|
3
|
+
#
|
4
|
+
|
5
|
+
#
|
6
|
+
# Represents a single option.
|
7
|
+
#
|
8
|
+
# An Option object is created for every option
|
9
|
+
# specified via Caty#task_options().
|
10
|
+
# The sum of all options parsed is accessible
|
11
|
+
# via the Caty#task_options() method.
|
12
|
+
#
|
13
|
+
class Caty::Option
|
14
|
+
|
15
|
+
include Caty::Helpers
|
16
|
+
|
17
|
+
attr_reader :name
|
18
|
+
|
19
|
+
#
|
20
|
+
# Creates a new option with the given name and default value.
|
21
|
+
#
|
22
|
+
# default may be one of the following:
|
23
|
+
# - :boolean
|
24
|
+
# nil is the default, any given argument will be coerced
|
25
|
+
# into a boolean value.
|
26
|
+
# - :string
|
27
|
+
# nil is the default, any given argument will be coerced
|
28
|
+
# into a string value.
|
29
|
+
# - :integer
|
30
|
+
# nil is the default, any given argument will be coerced
|
31
|
+
# into a integer value.
|
32
|
+
# - an Integer, String or Boolean value
|
33
|
+
# the passed value is the default value, any given argument
|
34
|
+
# will be coerced into the given type.
|
35
|
+
# - any other value will be treated as if a String had been
|
36
|
+
# given.
|
37
|
+
#
|
38
|
+
# If the deduced argument type is boolean, not giving an argument
|
39
|
+
# on the command line is interpreted as giving 'true' as the argument.
|
40
|
+
# For all the other types, a MissingOptionArgumentError is thrown.
|
41
|
+
#
|
42
|
+
def initialize( name, default )
|
43
|
+
@name = name
|
44
|
+
@converter = nil
|
45
|
+
|
46
|
+
Caty::Converter.types.each do |type,converter|
|
47
|
+
if default == type
|
48
|
+
@converter = converter.new
|
49
|
+
@default = nil
|
50
|
+
break
|
51
|
+
elsif converter.allowed_defaults.any? { |klass| default.is_a?(klass) }
|
52
|
+
@converter = converter.new
|
53
|
+
@default = default
|
54
|
+
break
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
raise(
|
59
|
+
ArgumentError,
|
60
|
+
'Only boolean, string and integer values or :boolean, :integer, :string allowed.'
|
61
|
+
) if @converter.nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# Tries to remove the option from the args.
|
66
|
+
# Returns the value grepped for this option.
|
67
|
+
#
|
68
|
+
def grep!( args )
|
69
|
+
rex = %r{^#{self.prefix}#{@name}(?:=(.+))?$}
|
70
|
+
index = args.index { |arg| rex =~ arg }
|
71
|
+
|
72
|
+
if index.nil?
|
73
|
+
@default
|
74
|
+
else
|
75
|
+
match = rex.match(args.delete_at(index))
|
76
|
+
@converter.convert(match[1])
|
77
|
+
end
|
78
|
+
rescue Caty::OptionArgumentError => e
|
79
|
+
e.option = @name
|
80
|
+
raise e
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
"[#{self.prefix}#{@name}=#{
|
85
|
+
@default.nil? ? '' : @default.inspect
|
86
|
+
}]"
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
#
|
92
|
+
# Defines the prefix of this option.
|
93
|
+
# May be overwritten by subclasses.
|
94
|
+
#
|
95
|
+
def prefix
|
96
|
+
'-'
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|