clive 0.8.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/README.md +328 -227
- data/lib/clive.rb +130 -50
- data/lib/clive/argument.rb +170 -0
- data/lib/clive/arguments.rb +139 -0
- data/lib/clive/arguments/parser.rb +210 -0
- data/lib/clive/base.rb +189 -0
- data/lib/clive/command.rb +342 -444
- data/lib/clive/error.rb +66 -0
- data/lib/clive/formatter.rb +57 -141
- data/lib/clive/formatter/colour.rb +37 -0
- data/lib/clive/formatter/plain.rb +172 -0
- data/lib/clive/option.rb +185 -75
- data/lib/clive/option/runner.rb +163 -0
- data/lib/clive/output.rb +141 -16
- data/lib/clive/parser.rb +180 -87
- data/lib/clive/struct_hash.rb +109 -0
- data/lib/clive/type.rb +117 -0
- data/lib/clive/type/definitions.rb +170 -0
- data/lib/clive/type/lookup.rb +23 -0
- data/lib/clive/version.rb +3 -3
- data/spec/clive/a_cli_spec.rb +245 -0
- data/spec/clive/argument_spec.rb +148 -0
- data/spec/clive/arguments/parser_spec.rb +35 -0
- data/spec/clive/arguments_spec.rb +191 -0
- data/spec/clive/command_spec.rb +276 -209
- data/spec/clive/formatter/colour_spec.rb +129 -0
- data/spec/clive/formatter/plain_spec.rb +129 -0
- data/spec/clive/option/runner_spec.rb +92 -0
- data/spec/clive/option_spec.rb +149 -23
- data/spec/clive/output_spec.rb +86 -2
- data/spec/clive/parser_spec.rb +201 -81
- data/spec/clive/struct_hash_spec.rb +82 -0
- data/spec/clive/type/definitions_spec.rb +312 -0
- data/spec/clive/type_spec.rb +107 -0
- data/spec/clive_spec.rb +60 -0
- data/spec/extras/expectations.rb +86 -0
- data/spec/extras/focus.rb +22 -0
- data/spec/helper.rb +35 -0
- metadata +56 -36
- data/lib/clive/bool.rb +0 -67
- data/lib/clive/exceptions.rb +0 -54
- data/lib/clive/flag.rb +0 -199
- data/lib/clive/switch.rb +0 -31
- data/lib/clive/tokens.rb +0 -141
- data/spec/clive/bool_spec.rb +0 -54
- data/spec/clive/flag_spec.rb +0 -117
- data/spec/clive/formatter_spec.rb +0 -108
- data/spec/clive/switch_spec.rb +0 -14
- data/spec/clive/tokens_spec.rb +0 -38
- data/spec/shared_specs.rb +0 -16
- data/spec/spec_helper.rb +0 -12
data/lib/clive.rb
CHANGED
@@ -1,64 +1,144 @@
|
|
1
|
-
|
2
|
-
require 'attr_plus'
|
1
|
+
$: << File.dirname(__FILE__)
|
3
2
|
|
4
|
-
require 'clive/
|
5
|
-
require 'clive/
|
6
|
-
require 'clive/
|
3
|
+
require 'clive/error'
|
4
|
+
require 'clive/output'
|
5
|
+
require 'clive/version'
|
6
|
+
require 'clive/struct_hash'
|
7
|
+
require 'clive/formatter'
|
8
|
+
require 'clive/formatter/plain'
|
9
|
+
require 'clive/formatter/colour'
|
7
10
|
|
11
|
+
require 'clive/type'
|
12
|
+
require 'clive/argument'
|
13
|
+
require 'clive/arguments'
|
14
|
+
require 'clive/arguments/parser'
|
15
|
+
require 'clive/option/runner'
|
8
16
|
require 'clive/option'
|
9
17
|
require 'clive/command'
|
10
|
-
require 'clive/
|
11
|
-
require 'clive/
|
12
|
-
require 'clive/bool'
|
18
|
+
require 'clive/parser'
|
19
|
+
require 'clive/base'
|
13
20
|
|
14
|
-
require 'clive/output'
|
15
|
-
require 'clive/formatter'
|
16
21
|
|
17
|
-
# Clive is a
|
22
|
+
# Clive is a DSL for creating command line interfaces. Generally to use it you
|
23
|
+
# will inherit from it with your own class.
|
24
|
+
#
|
25
|
+
# class CLI < Clive
|
26
|
+
# opt :working, 'Test if it is working' do
|
27
|
+
# puts "YEP!".green
|
28
|
+
# end
|
29
|
+
# end
|
18
30
|
#
|
19
|
-
#
|
31
|
+
# CLI.run ARGV
|
20
32
|
#
|
21
|
-
#
|
33
|
+
# # app.rb --working
|
34
|
+
# #=> "YEP!"
|
22
35
|
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# puts "
|
29
|
-
# end
|
30
|
-
#
|
31
|
-
# desc 'A flag'
|
32
|
-
# flag :hello, :args => "NAME" do |name|
|
33
|
-
# puts "Hello, #{name}"
|
34
|
-
# end
|
35
|
-
#
|
36
|
-
# desc 'True or false'
|
37
|
-
# bool :which do |which|
|
38
|
-
# case which
|
39
|
-
# when true
|
40
|
-
# puts "true, yay"
|
41
|
-
# when false
|
42
|
-
# puts "false, not yay"
|
43
|
-
# end
|
44
|
-
# end
|
45
|
-
#
|
46
|
-
# option_list :purchases
|
47
|
-
#
|
48
|
-
# command :new, :buy do
|
49
|
-
# switch :toaster do
|
50
|
-
# purchases << :toaster
|
51
|
-
# end
|
52
|
-
#
|
53
|
-
# switch :tv do
|
54
|
-
# purchases << :tv
|
55
|
-
# end
|
36
|
+
# But it is possible to create a new instance of Clive instead in almost the
|
37
|
+
# same way.
|
38
|
+
#
|
39
|
+
# cli = Clive.new do
|
40
|
+
# opt :working, 'Test if it is working' do
|
41
|
+
# puts "YEP!".green
|
56
42
|
# end
|
57
|
-
#
|
58
43
|
# end
|
59
44
|
#
|
60
|
-
#
|
45
|
+
# cli.run ARGV
|
46
|
+
#
|
47
|
+
# For very small tasks where you _just_ need to collect options passed and query
|
48
|
+
# about them you can use the {Kernel#Clive} method.
|
49
|
+
#
|
50
|
+
# r = Clive(:quiet, :verbose).run(ARGV)
|
51
|
+
#
|
52
|
+
# $log = Logger.new(STDOUT)
|
53
|
+
# $log.level = Logger::FATAL if r.quiet
|
54
|
+
# $log.level = Logger::DEBUG if r.verbose
|
61
55
|
#
|
62
|
-
|
63
|
-
|
56
|
+
# # do some stuff
|
57
|
+
#
|
58
|
+
class Clive
|
59
|
+
|
60
|
+
extend Type::Lookup
|
61
|
+
|
62
|
+
class << self
|
63
|
+
attr_accessor :instance
|
64
|
+
|
65
|
+
# Sets up proxy methods for each relevent method in {Base} to an instance of {Base}.
|
66
|
+
def inherited(klass)
|
67
|
+
klass.instance = Base.new
|
68
|
+
|
69
|
+
str = (Base.instance_methods(false) | Command.instance_methods(false)).map do |sym|
|
70
|
+
<<-EOS
|
71
|
+
def self.#{sym}(*args, &block)
|
72
|
+
instance.send(:#{sym}, *args, &block)
|
73
|
+
end
|
74
|
+
EOS
|
75
|
+
end.join("\n")
|
76
|
+
klass.instance_eval str
|
77
|
+
end
|
78
|
+
|
79
|
+
def method_missing(sym, *args, &block)
|
80
|
+
instance.send(sym, *args, &block)
|
81
|
+
end
|
82
|
+
|
83
|
+
def respond_to_missing?(sym, include_private=false)
|
84
|
+
instance.respond_to?(sym, include_private)
|
85
|
+
end
|
86
|
+
|
87
|
+
unless Kernel.respond_to?(:respond_to_missing?)
|
88
|
+
def respond_to?(sym, include_private=false)
|
89
|
+
respond_to_missing?(sym, include_private)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
# This allows you to use Clive without defining a class, but while keeping all
|
96
|
+
# of the control.
|
97
|
+
#
|
98
|
+
# There is one caveat though when using this style: types can not be
|
99
|
+
# referenced with just the type name. Instead the full class path/name must be
|
100
|
+
# given. So instead of using +opt :num, as: Integer+ you need to use +opt
|
101
|
+
# :num, as: Clive::Type::Integer+, and similarly for all types.
|
102
|
+
#
|
103
|
+
# @param opts [Hash] Options to create with, see {Base#initialize}
|
104
|
+
# @example
|
105
|
+
#
|
106
|
+
# c = Clive.new { opt :v, :verbose }
|
107
|
+
# r = c.run ARGV
|
108
|
+
#
|
109
|
+
def self.new(opts={}, &block)
|
110
|
+
Base.new(opts, &block)
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
module Kernel
|
116
|
+
|
117
|
+
# The quickest way to grab a few options. This form does not allow arguments or
|
118
|
+
# commands! It is meant to be quick and simple.
|
119
|
+
#
|
120
|
+
# @example
|
121
|
+
#
|
122
|
+
# r = Clive(:verbose, [:b, :bare]).run(%w(--verbose))
|
123
|
+
# r.bare #=> false
|
124
|
+
# r.verbose #=> true
|
125
|
+
#
|
126
|
+
#
|
127
|
+
# # The above example is equivalent to
|
128
|
+
# r = Clive.new {
|
129
|
+
# opt :verbose
|
130
|
+
# opt :b, :bare
|
131
|
+
# }.run(%w(--verbose))
|
132
|
+
#
|
133
|
+
# @param names [#to_sym, Array<#to_sym>] List of names to create options for
|
134
|
+
# @return [Clive] A clive instance setup with the correct options
|
135
|
+
#
|
136
|
+
def Clive(*names)
|
137
|
+
c = Clive::Base.new
|
138
|
+
names.each do |o|
|
139
|
+
c.option *Array(o).map(&:to_sym)
|
140
|
+
end
|
141
|
+
c
|
142
|
+
end
|
143
|
+
|
64
144
|
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
class Clive
|
2
|
+
|
3
|
+
# An Argument represents an argument for an Option or Command, it can be optional
|
4
|
+
# and can also be constricted by various other values, see {#initialize}.
|
5
|
+
class Argument
|
6
|
+
|
7
|
+
# Creates an object which will respond with true to all call to the method(s)
|
8
|
+
# given.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# eg = AlwaysTrue.for(:a, :b, :c)
|
12
|
+
# eg.a #=> true
|
13
|
+
# eg.b(1,2,3) #=> true
|
14
|
+
# eg.c { 1 } #=> true
|
15
|
+
# eg.d #=> NoMethodError
|
16
|
+
#
|
17
|
+
class AlwaysTrue
|
18
|
+
|
19
|
+
# @param syms [Symbol] Methods which should return true
|
20
|
+
def self.for(*syms)
|
21
|
+
c = Class.new
|
22
|
+
syms.each do |sym|
|
23
|
+
c.send(:define_method, sym) {|*a| true }
|
24
|
+
end
|
25
|
+
c.send(:define_method, :inspect) { "#<AlwaysTrue #{syms.map {|i| ":#{i}" }.join(', ') }>" }
|
26
|
+
c.new
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# An Argument will have these traits by default.
|
31
|
+
DEFAULTS = {
|
32
|
+
:optional => false,
|
33
|
+
:type => Type::Object,
|
34
|
+
:match => AlwaysTrue.for(:match),
|
35
|
+
:within => AlwaysTrue.for(:include?),
|
36
|
+
:default => nil,
|
37
|
+
:constraint => AlwaysTrue.for(:call)
|
38
|
+
}
|
39
|
+
|
40
|
+
attr_reader :name, :default, :type
|
41
|
+
|
42
|
+
# A new instance of Argument.
|
43
|
+
#
|
44
|
+
# @param opts [Hash]
|
45
|
+
#
|
46
|
+
# @option opts [#to_sym] :name
|
47
|
+
# Name of the argument.
|
48
|
+
#
|
49
|
+
# @option opts [Boolean] :optional
|
50
|
+
# Whether this argument is optional. An optional argument does not have
|
51
|
+
# to be given and will pass +:default+ to the block instead.
|
52
|
+
#
|
53
|
+
# @option opts [Type] :type
|
54
|
+
# Type that the matching argument should be cast to. See {Type} and the
|
55
|
+
# various subclasses for details. Each {Type} defines something that the
|
56
|
+
# argument must match in addition to the +:match+ argument given.
|
57
|
+
#
|
58
|
+
# @option opts [#match] :match
|
59
|
+
# Regular expression the argument must match.
|
60
|
+
#
|
61
|
+
# @option opts [#include?] :within
|
62
|
+
# Collection that the argument should be in. This will be checked
|
63
|
+
# against the string argument and the cast object (see +:type+). So for
|
64
|
+
# instance if +:type+ is set to +Integer+ you can set +:within+ to be an array
|
65
|
+
# of integers, [1,2,3], or an array of strings, %w(1 2 3), and get the
|
66
|
+
# same result.
|
67
|
+
#
|
68
|
+
# @option opts :default
|
69
|
+
# Default value the argument takes. This is only set or used if the Option or
|
70
|
+
# Command is actually called.
|
71
|
+
#
|
72
|
+
# @option opts [#call, #to_proc] :constraint
|
73
|
+
# Proc which is passed the found argument and should return +true+ if the
|
74
|
+
# value is ok and false if not.
|
75
|
+
# If the object responds to #to_proc this will be called and the resulting
|
76
|
+
# Proc object saved for later use. This allows you to pass method symbols.
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
#
|
80
|
+
# Argument.new(:arg, :optional => true, :type => Integer, :constraint => :odd?)
|
81
|
+
#
|
82
|
+
def initialize(name, opts={})
|
83
|
+
@name = name.to_sym
|
84
|
+
|
85
|
+
opts[:constraint] = opts[:constraint].to_proc if opts[:constraint].respond_to?(:to_proc)
|
86
|
+
opts = DEFAULTS.merge(opts)
|
87
|
+
|
88
|
+
@optional = opts[:optional]
|
89
|
+
@type = Type.find_class(opts[:type].to_s) rescue opts[:type]
|
90
|
+
@match = opts[:match]
|
91
|
+
@within = opts[:within]
|
92
|
+
@default = opts[:default]
|
93
|
+
@constraint = opts[:constraint]
|
94
|
+
end
|
95
|
+
|
96
|
+
# @return Whether the argument is optional.
|
97
|
+
def optional?
|
98
|
+
@optional
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return [String] String representation for the argument.
|
102
|
+
def to_s
|
103
|
+
optional? ? "[<#@name>]" : "<#@name>"
|
104
|
+
end
|
105
|
+
|
106
|
+
# @return [String]
|
107
|
+
# Choices or range of choices that can be made, for the help string.
|
108
|
+
def choice_str
|
109
|
+
if @within
|
110
|
+
case @within
|
111
|
+
when Array
|
112
|
+
'(' + @within.join(', ') + ')'
|
113
|
+
when Range
|
114
|
+
'(' + @within.to_s + ')'
|
115
|
+
else
|
116
|
+
''
|
117
|
+
end
|
118
|
+
else
|
119
|
+
''
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def inspect
|
124
|
+
"#<#{self.class} #{to_s}>"
|
125
|
+
end
|
126
|
+
|
127
|
+
# Determines whether the object given can be this argument. Checks whether
|
128
|
+
# it is valid based on the options passed to {#initialize}.
|
129
|
+
#
|
130
|
+
# @param obj [String, Object]
|
131
|
+
# This method will be called at least twice for each argument, the first
|
132
|
+
# time when testing for {Arguments#possible?} and then for {Arguments#valid?}.
|
133
|
+
# When called in {Arguments#possible?} +obj+ will be passed as a string,
|
134
|
+
# for {Arguments#valid?} though +obj+ will have been cast using {#coerce}
|
135
|
+
# to the correct type meaning this method must deal with both cases.
|
136
|
+
#
|
137
|
+
# @return Whether +obj+ could be this argument.
|
138
|
+
#
|
139
|
+
def possible?(obj)
|
140
|
+
return false if obj.is_a?(String) && !@type.valid?(obj)
|
141
|
+
return false unless @match.match(obj.to_s)
|
142
|
+
|
143
|
+
coerced = coerce(obj)
|
144
|
+
|
145
|
+
unless @within.include?(obj.to_s) || @within.include?(coerced)
|
146
|
+
return false
|
147
|
+
end
|
148
|
+
|
149
|
+
begin
|
150
|
+
return false unless @constraint.call(obj.to_s)
|
151
|
+
rescue
|
152
|
+
begin
|
153
|
+
return false unless @constraint.call(coerced)
|
154
|
+
rescue
|
155
|
+
return false
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
true
|
160
|
+
end
|
161
|
+
|
162
|
+
# Converts the given String argument to the correct type determined by the
|
163
|
+
# +:type+ passed to {#initialize}.
|
164
|
+
def coerce(str)
|
165
|
+
return str unless str.is_a?(String)
|
166
|
+
@type.typecast(str)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
class Clive
|
2
|
+
# An Array of {Clive::Argument} instances.
|
3
|
+
class Arguments < Array
|
4
|
+
|
5
|
+
# @example
|
6
|
+
#
|
7
|
+
# ArgumentList.create :args => '<a> [<b>]', :as => [Integer, String]
|
8
|
+
# #=> #<ArgumentList ...>
|
9
|
+
#
|
10
|
+
def self.create(opts)
|
11
|
+
new Parser.new(opts).to_args
|
12
|
+
end
|
13
|
+
|
14
|
+
# Zips a list of found arguments to this ArgumentList, but it also takes
|
15
|
+
# account of whether the found argument is possible and makes sure that
|
16
|
+
# optional Arguments are correctly handled.
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
#
|
20
|
+
# a = Argument.new(:a, type: Integer, constraint: :even?, optional: true)
|
21
|
+
# b = Argument.new(:b, type: Integer, constraint: :odd?)
|
22
|
+
# c = Argument.new(:c, type: Integer, constraint: :even?)
|
23
|
+
# d = Argument.new(:d, type: Integer, constraint: :odd?, optional: true)
|
24
|
+
#
|
25
|
+
# list = ArgumentList.new([a, b, c, d])
|
26
|
+
# found_args = %w(1 2 3)
|
27
|
+
#
|
28
|
+
# list.zip(found_args).map {|i| [i[0].to_s, i[1]] }
|
29
|
+
# #=> [['[<a>]', nil], ['<b>', 1], ['<c>', 2], ['[<d>]', 3]]
|
30
|
+
#
|
31
|
+
#
|
32
|
+
# @param other [Array<String>] Found list of arguments.
|
33
|
+
# @return [Array<Argument,String>]
|
34
|
+
#
|
35
|
+
def zip(other)
|
36
|
+
other = other.dup.compact
|
37
|
+
# Find the number of 'spares'
|
38
|
+
diff = other.size - find_all {|i| !i.optional? }.size
|
39
|
+
r = []
|
40
|
+
|
41
|
+
map do |arg|
|
42
|
+
if arg.possible?(other.first)
|
43
|
+
if arg.optional?
|
44
|
+
if diff > 0
|
45
|
+
[arg, other.shift]
|
46
|
+
else
|
47
|
+
[arg, nil]
|
48
|
+
end
|
49
|
+
else
|
50
|
+
[arg, other.shift]
|
51
|
+
end
|
52
|
+
else
|
53
|
+
[arg, nil]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [String]
|
59
|
+
def to_s
|
60
|
+
map {|i| i.to_s }.join(' ').gsub('] [', ' ')
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Integer] The minimum number of arguments that *must* be given.
|
64
|
+
def min
|
65
|
+
reject {|i| i.optional? }.size
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Integer] The maximum number of arguments that *can* be given.
|
69
|
+
def max
|
70
|
+
size
|
71
|
+
end
|
72
|
+
|
73
|
+
# Whether the +list+ of found arguments could possibly be the arguments for
|
74
|
+
# this option. This does not need to check the minimum length as the list
|
75
|
+
# may not be completely built, this just checks it hasn't failed completely.
|
76
|
+
#
|
77
|
+
# @param list [Array<Object>]
|
78
|
+
def possible?(list)
|
79
|
+
return true if list.empty?
|
80
|
+
i = 0
|
81
|
+
optionals = []
|
82
|
+
|
83
|
+
list.each do |item|
|
84
|
+
break if i >= size
|
85
|
+
|
86
|
+
# Either, +item+ is self[i]
|
87
|
+
if self[i].possible?(item)
|
88
|
+
i += 1
|
89
|
+
|
90
|
+
# Or, the argument is optional and there is another argument to move to
|
91
|
+
# meaning it can be skipped
|
92
|
+
elsif self[i].optional? && (i < size - 1)
|
93
|
+
i += 1
|
94
|
+
optionals << item
|
95
|
+
|
96
|
+
# Or, an optional argument has been skipped and this could be it so bring
|
97
|
+
# it back from the dead and check, if it is remove it and move on
|
98
|
+
elsif optionals.size > 0 && self[i].possible?(optionals.first)
|
99
|
+
i += 1
|
100
|
+
optionals.shift
|
101
|
+
|
102
|
+
# Problem
|
103
|
+
else
|
104
|
+
return false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
list.size <= max
|
109
|
+
end
|
110
|
+
|
111
|
+
# Whether the +list+ of found arguments is valid to be the arguments for this
|
112
|
+
# option. Here length is checked as we need to make sure enough arguments are
|
113
|
+
# present.
|
114
|
+
#
|
115
|
+
# It is important that when the arguments are put in the correct place
|
116
|
+
# that we check for missing arguments (which have been added as +nil+s)
|
117
|
+
# so compact the list _then_ check the size.
|
118
|
+
#
|
119
|
+
# @param list [Array<Object>]
|
120
|
+
def valid?(list)
|
121
|
+
zip(list).map do |a,i|
|
122
|
+
if a.optional?
|
123
|
+
nil
|
124
|
+
else
|
125
|
+
i
|
126
|
+
end
|
127
|
+
end.compact.size >= min && possible?(list)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Will fill spaces in +list+ with default values, then coerces all arguments
|
131
|
+
# to the corect types.
|
132
|
+
#
|
133
|
+
# @return [Array]
|
134
|
+
def create_valid(list)
|
135
|
+
zip(list).map {|a,r| r ? a.coerce(r) : a.coerce(a.default) }
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|