clive 0.8.1 → 1.0.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/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
|