quickl 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +9 -0
- data/README.md +118 -0
- data/Rakefile +64 -0
- data/bin/quickl +116 -0
- data/examples/delegate/README.md +86 -0
- data/examples/delegate/bin/delegate +9 -0
- data/examples/delegate/lib/delegate.rb +41 -0
- data/examples/delegate/lib/hello_world.rb +39 -0
- data/examples/delegate/lib/help.rb +24 -0
- data/examples/delegate/test/delegate_test.rb +68 -0
- data/examples/hello/README.md +74 -0
- data/examples/hello/hello +57 -0
- data/examples/hello/hello_test.rb +65 -0
- data/examples/helper.rb +6 -0
- data/lib/quickl.rb +47 -0
- data/lib/quickl/command.rb +154 -0
- data/lib/quickl/command/builder.rb +58 -0
- data/lib/quickl/command/delegate.rb +53 -0
- data/lib/quickl/command/options.rb +55 -0
- data/lib/quickl/command/robustness.rb +18 -0
- data/lib/quickl/command/single.rb +27 -0
- data/lib/quickl/errors.rb +196 -0
- data/lib/quickl/ext/object.rb +29 -0
- data/lib/quickl/naming.rb +53 -0
- data/lib/quickl/ruby_tools.rb +60 -0
- data/quickl.gemspec +38 -0
- data/templates/single.erb +40 -0
- data/test/command/command_name.spec +16 -0
- data/test/command/documentation.spec +23 -0
- data/test/command/overview.spec +14 -0
- data/test/command/run.spec +22 -0
- data/test/command/subcommands.spec +20 -0
- data/test/command/usage.spec +16 -0
- data/test/mini_client.rb +59 -0
- data/test/naming/command2module.spec +17 -0
- data/test/naming/module2command.spec +21 -0
- data/test/ruby_tools/class_unqualified_name.spec +28 -0
- data/test/ruby_tools/extract_file_rdoc.spec +28 -0
- data/test/ruby_tools/fixtures.rb +27 -0
- data/test/ruby_tools/fixtures/RubyTools.rdoc +12 -0
- data/test/ruby_tools/fixtures/Utils.rdoc +3 -0
- data/test/ruby_tools/optional_args_block_call.spec +37 -0
- data/test/ruby_tools/parent_module.spec +23 -0
- data/test/spec_helper.rb +13 -0
- data/test/wrapping.rb +39 -0
- metadata +129 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
module Quickl
|
2
|
+
class Command
|
3
|
+
class Builder
|
4
|
+
|
5
|
+
# Adds document tracking information
|
6
|
+
def document(file, line)
|
7
|
+
@file, @line = file, line
|
8
|
+
end
|
9
|
+
|
10
|
+
# Adds some command class modules
|
11
|
+
def class_modules(*mods)
|
12
|
+
@class_modules ||= [
|
13
|
+
Command::ClassMethods,
|
14
|
+
Command::Options::ClassMethods,
|
15
|
+
]
|
16
|
+
@class_modules += mods
|
17
|
+
end
|
18
|
+
alias :class_module class_modules
|
19
|
+
|
20
|
+
# Adds some command instance modules
|
21
|
+
def instance_modules(*mods)
|
22
|
+
@instance_modules ||= [
|
23
|
+
Command::InstanceMethods,
|
24
|
+
Command::Robustness,
|
25
|
+
Command::Options::InstanceMethods
|
26
|
+
]
|
27
|
+
@instance_modules += mods
|
28
|
+
end
|
29
|
+
alias :instance_module instance_modules
|
30
|
+
|
31
|
+
# Installs on a command subclass
|
32
|
+
def run(command)
|
33
|
+
# install class and instance methods
|
34
|
+
class_modules.each{|mod|
|
35
|
+
command.extend(mod)
|
36
|
+
}
|
37
|
+
instance_modules.each{|mod|
|
38
|
+
command.instance_eval{ include mod }
|
39
|
+
}
|
40
|
+
|
41
|
+
# install documentation
|
42
|
+
if @file and @line
|
43
|
+
command.doc_place = [@file, @line]
|
44
|
+
end
|
45
|
+
|
46
|
+
# install hierarchy
|
47
|
+
parent = RubyTools::parent_module(command)
|
48
|
+
if parent && parent.ancestors.include?(Command)
|
49
|
+
command.super_command = parent
|
50
|
+
parent.subcommands << command
|
51
|
+
end
|
52
|
+
|
53
|
+
command
|
54
|
+
end
|
55
|
+
|
56
|
+
end # class Builder
|
57
|
+
end # class Command
|
58
|
+
end # module Quickl
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Quickl
|
2
|
+
module Command::Delegate
|
3
|
+
module InstanceMethods
|
4
|
+
|
5
|
+
# Run the command by delegation
|
6
|
+
def _run(argv = [])
|
7
|
+
# My own options
|
8
|
+
my_argv = []
|
9
|
+
while argv.first =~ /^--/
|
10
|
+
my_argv << argv.shift
|
11
|
+
end
|
12
|
+
parse_options(my_argv)
|
13
|
+
|
14
|
+
# Run the subcommand now
|
15
|
+
if cmd = argv.shift
|
16
|
+
has_command!(cmd).run(argv)
|
17
|
+
else
|
18
|
+
raise Quickl::Help.new(cmd.nil? ? 0 : -1)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
module ClassMethods
|
24
|
+
|
25
|
+
def summarized_subcommands
|
26
|
+
doc = subcommands.collect{|cmd|
|
27
|
+
[cmd.command_name, cmd.overview]
|
28
|
+
}
|
29
|
+
max = doc.inject(0){|memo,pair|
|
30
|
+
l = pair.first.length
|
31
|
+
memo > l ? memo : l
|
32
|
+
}
|
33
|
+
doc.collect{|pair|
|
34
|
+
" %-#{max}s %s" % pair
|
35
|
+
}.join("\n")
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end # module Command::Delegate
|
40
|
+
|
41
|
+
#
|
42
|
+
# Create a delegate command
|
43
|
+
#
|
44
|
+
def self.Delegate(*args)
|
45
|
+
command_builder do |b|
|
46
|
+
b.document *args
|
47
|
+
b.class_module Command::Delegate::ClassMethods
|
48
|
+
b.instance_module Command::Delegate::InstanceMethods
|
49
|
+
end
|
50
|
+
Command
|
51
|
+
end
|
52
|
+
|
53
|
+
end # module Quickl
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Quickl
|
2
|
+
module Command::Options
|
3
|
+
module ClassMethods
|
4
|
+
|
5
|
+
# Installed options builders
|
6
|
+
attr_reader :option_builders
|
7
|
+
|
8
|
+
# Builds an OptionParser instance
|
9
|
+
def build_options(scope = self)
|
10
|
+
OptionParser.new do |opt|
|
11
|
+
opt.program_name = File.basename $0
|
12
|
+
if const_defined?(:VERSION)
|
13
|
+
opt.version = const_get(:VERSION)
|
14
|
+
end
|
15
|
+
opt.summary_indent = ' ' * 2
|
16
|
+
(option_builders || []).each{|b|
|
17
|
+
scope.instance_exec(opt, &b)
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Without builder nor block, returns built options.
|
23
|
+
# Otherwise, adds a new option builder
|
24
|
+
def options(builder = nil, &block)
|
25
|
+
if b = (builder || block)
|
26
|
+
@option_builders ||= []
|
27
|
+
@option_builders << b
|
28
|
+
else
|
29
|
+
build_options
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns summarized options
|
34
|
+
def summarized_options
|
35
|
+
options.summarize.join.rstrip
|
36
|
+
end
|
37
|
+
|
38
|
+
end # module ClassMethods
|
39
|
+
module InstanceMethods
|
40
|
+
|
41
|
+
# Returns OptionParser instance
|
42
|
+
def options
|
43
|
+
@options ||= self.class.build_options(self)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Parses options
|
47
|
+
def parse_options(argv)
|
48
|
+
options.parse!(argv)
|
49
|
+
rescue OptionParser::ParseError => ex
|
50
|
+
raise Quickl::InvalidOption, ex.message, ex.backtrace
|
51
|
+
end
|
52
|
+
|
53
|
+
end # module InstanceMethods
|
54
|
+
end # module Command::Options
|
55
|
+
end # module Quickl
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Quickl
|
2
|
+
class Command
|
3
|
+
module Robustness
|
4
|
+
include Naming
|
5
|
+
|
6
|
+
# Checks that a command whose name is given exists
|
7
|
+
# or raises a NoSuchCommand.
|
8
|
+
def has_command!(name, referer = self.class)
|
9
|
+
name.split(':').inject(referer){|cur,look|
|
10
|
+
cur.const_get(command2module(look))
|
11
|
+
}
|
12
|
+
rescue NameError => ex
|
13
|
+
raise NoSuchCommand, "No such command #{name}", ex.backtrace
|
14
|
+
end
|
15
|
+
|
16
|
+
end # module Robustness
|
17
|
+
end # class Command
|
18
|
+
end # module Quickl
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Quickl
|
2
|
+
|
3
|
+
# Instance method for being a single command
|
4
|
+
module Command::Single
|
5
|
+
|
6
|
+
# Command arguments after options parsing
|
7
|
+
attr_reader :args
|
8
|
+
|
9
|
+
# Run the command by delegation
|
10
|
+
def _run(argv = [])
|
11
|
+
execute(parse_options(argv))
|
12
|
+
end
|
13
|
+
|
14
|
+
end # module Command::Delegate
|
15
|
+
|
16
|
+
#
|
17
|
+
# Create a delegate command
|
18
|
+
#
|
19
|
+
def self.Command(*args)
|
20
|
+
command_builder do |b|
|
21
|
+
b.document *args
|
22
|
+
b.instance_module Command::Single
|
23
|
+
end
|
24
|
+
Command
|
25
|
+
end
|
26
|
+
|
27
|
+
end # module Quickl
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module Quickl
|
2
|
+
|
3
|
+
#
|
4
|
+
# Main class of all Quickl's errors.
|
5
|
+
#
|
6
|
+
# Quickl's errors normally occur only in the context of a command
|
7
|
+
# execution, command which can be obtained through the dedicated
|
8
|
+
# accessor.
|
9
|
+
#
|
10
|
+
# Each Quickl error comes bundled with a default reaction implemented
|
11
|
+
# by the _react!_ method. Unless you provide your own reaction via
|
12
|
+
# dedicated Command's DSL methods (react_to, no_react_to, ...), this
|
13
|
+
# reaction will be executed if the error is raised. Default reactions
|
14
|
+
# are implemented so that the command fail gracefully for the ordinary
|
15
|
+
# commandline user.
|
16
|
+
#
|
17
|
+
class Error < StandardError
|
18
|
+
|
19
|
+
# Quickl command under execution
|
20
|
+
attr_accessor :command
|
21
|
+
|
22
|
+
# Exit code
|
23
|
+
attr_reader :exit_code
|
24
|
+
|
25
|
+
# Creates an Exit instance
|
26
|
+
def initialize(*args)
|
27
|
+
super(args.find{|s| s.is_a?(String)} || "")
|
28
|
+
@exit_code = args.find{|s| s.is_a?(Integer)} || nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns true if exit code is not nil
|
32
|
+
def exit?
|
33
|
+
!exit_code.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns $stdout or $stderr according to exit code.
|
37
|
+
def error_io
|
38
|
+
exit_code == -1 ? $stderr : $stdout
|
39
|
+
end
|
40
|
+
|
41
|
+
# Print error's message on IO returned by error_io and
|
42
|
+
# make a call to Kernel.exit with required exit code.
|
43
|
+
def do_kernel_exit
|
44
|
+
error_io.puts self.message
|
45
|
+
Kernel.exit(exit_code) if exit?
|
46
|
+
end
|
47
|
+
|
48
|
+
end # class Error
|
49
|
+
|
50
|
+
#
|
51
|
+
# This error can be raised to exit command execution with a message
|
52
|
+
# and exit code.
|
53
|
+
#
|
54
|
+
# Default exit code:
|
55
|
+
# 0
|
56
|
+
#
|
57
|
+
# Default reaction:
|
58
|
+
# Prints the error message (on $stdout if exit code is 0, on $stderr
|
59
|
+
# otherwise) then invoke Kernel.exit with the exit code.
|
60
|
+
#
|
61
|
+
# Examples:
|
62
|
+
#
|
63
|
+
# # Non-failure exit, for --version, --help, --copyright and the
|
64
|
+
# # like
|
65
|
+
# raise Quickl::Exit, "a friendly message"
|
66
|
+
#
|
67
|
+
# # Failure exit, because something failed
|
68
|
+
# raise Quickl::Exit.new(-1), "Something was wrong!"
|
69
|
+
#
|
70
|
+
class Exit < Error
|
71
|
+
|
72
|
+
def initialize(*args)
|
73
|
+
super(*(args + [ 0 ]))
|
74
|
+
end
|
75
|
+
|
76
|
+
def react!
|
77
|
+
do_kernel_exit
|
78
|
+
end
|
79
|
+
|
80
|
+
end # class Exit
|
81
|
+
|
82
|
+
#
|
83
|
+
# This error can be raised to print command's help and exit.
|
84
|
+
#
|
85
|
+
# Default exit code:
|
86
|
+
# 0
|
87
|
+
#
|
88
|
+
# Default reaction:
|
89
|
+
# raise Exit.new(code), additional + "\n" + help, backtrace
|
90
|
+
#
|
91
|
+
# Examples:
|
92
|
+
#
|
93
|
+
# # Print command help on $stdout and exit with 0
|
94
|
+
# raise Quickl::Help
|
95
|
+
#
|
96
|
+
# # Print command help on $stderr and exit with -1
|
97
|
+
# raise Quickl::Help.new(-1)
|
98
|
+
#
|
99
|
+
# # Print additional message before help
|
100
|
+
# raise Quickl::Help, "Hello user, below if the help!"
|
101
|
+
#
|
102
|
+
class Help < Error
|
103
|
+
|
104
|
+
def initialize(*args)
|
105
|
+
super(*(args + [ 0 ]))
|
106
|
+
end
|
107
|
+
|
108
|
+
def react!
|
109
|
+
msg = if (self.message || "").empty?
|
110
|
+
command.help
|
111
|
+
else
|
112
|
+
self.message.to_s + "\n" + command.help
|
113
|
+
end
|
114
|
+
raise Exit.new(self.exit_code), msg, backtrace
|
115
|
+
end
|
116
|
+
|
117
|
+
end # class Help
|
118
|
+
|
119
|
+
#
|
120
|
+
# This error can be raised to indicate that some command option
|
121
|
+
# was misused. This error is automatically raised on occurence
|
122
|
+
# of an OptionParser::ParseError when options are parsed.
|
123
|
+
#
|
124
|
+
# Default exit code:
|
125
|
+
# -1
|
126
|
+
#
|
127
|
+
# Default reaction:
|
128
|
+
# raise Help.new(code), message, backtrace
|
129
|
+
#
|
130
|
+
# Examples:
|
131
|
+
#
|
132
|
+
# # Print message on $stderr and exit with -1
|
133
|
+
# raise Quickl::InvalidOption, "--no-clue option does not exists"
|
134
|
+
#
|
135
|
+
class InvalidOption < Error
|
136
|
+
|
137
|
+
def initialize(*args)
|
138
|
+
super(*(args + [ -1 ]))
|
139
|
+
end
|
140
|
+
|
141
|
+
def react!
|
142
|
+
raise Help.new(self.exit_code), self.message, backtrace
|
143
|
+
end
|
144
|
+
|
145
|
+
end # class InvalidOption
|
146
|
+
|
147
|
+
#
|
148
|
+
# This error can be raised to indicate that some command argument
|
149
|
+
# was misused.
|
150
|
+
#
|
151
|
+
# Default exit code:
|
152
|
+
# -1
|
153
|
+
#
|
154
|
+
# Default reaction:
|
155
|
+
# raise Help.new(code), message, backtrace
|
156
|
+
#
|
157
|
+
# Examples:
|
158
|
+
#
|
159
|
+
# # Print message on $stderr and exit with -1
|
160
|
+
# raise Quickl::InvalidArgument, "Missing command argument"
|
161
|
+
#
|
162
|
+
class InvalidArgument < Error
|
163
|
+
|
164
|
+
def initialize(*args)
|
165
|
+
super(*(args + [ -1 ]))
|
166
|
+
end
|
167
|
+
|
168
|
+
def react!
|
169
|
+
raise Help.new(self.exit_code), self.message, backtrace
|
170
|
+
end
|
171
|
+
|
172
|
+
end # class InvalidArgument
|
173
|
+
|
174
|
+
#
|
175
|
+
# This error can be raised to indicate that a command has not been
|
176
|
+
# found (typically by delegates).
|
177
|
+
#
|
178
|
+
# Default exit code:
|
179
|
+
# -1
|
180
|
+
#
|
181
|
+
# Default reaction:
|
182
|
+
# raise Help.new(code), message, backtrace
|
183
|
+
#
|
184
|
+
class NoSuchCommand < Error
|
185
|
+
|
186
|
+
def initialize(*args)
|
187
|
+
super(*(args + [ -1 ]))
|
188
|
+
end
|
189
|
+
|
190
|
+
def react!
|
191
|
+
raise Help.new(self.exit_code), self.message, backtrace
|
192
|
+
end
|
193
|
+
|
194
|
+
end # class NoSuchCommand
|
195
|
+
|
196
|
+
end # module Quickl
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Object
|
2
|
+
# From active_support 2.3.4
|
3
|
+
unless defined? instance_exec # 1.9
|
4
|
+
module InstanceExecMethods #:nodoc:
|
5
|
+
end
|
6
|
+
include InstanceExecMethods
|
7
|
+
|
8
|
+
# Evaluate the block with the given arguments within the context of
|
9
|
+
# this object, so self is set to the method receiver.
|
10
|
+
#
|
11
|
+
# From Mauricio's http://eigenclass.org/hiki/bounded+space+instance_exec
|
12
|
+
def instance_exec(*args, &block)
|
13
|
+
begin
|
14
|
+
old_critical, Thread.critical = Thread.critical, true
|
15
|
+
n = 0
|
16
|
+
n += 1 while respond_to?(method_name = "__instance_exec#{n}")
|
17
|
+
InstanceExecMethods.module_eval { define_method(method_name, &block) }
|
18
|
+
ensure
|
19
|
+
Thread.critical = old_critical
|
20
|
+
end
|
21
|
+
|
22
|
+
begin
|
23
|
+
send(method_name, *args)
|
24
|
+
ensure
|
25
|
+
InstanceExecMethods.module_eval { remove_method(method_name) } rescue nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Quickl
|
2
|
+
module Naming
|
3
|
+
|
4
|
+
#
|
5
|
+
# Converts a command name to a module name. Implements
|
6
|
+
# the following conversions:
|
7
|
+
#
|
8
|
+
# hello -> Hello
|
9
|
+
# hello-world -> HelloWorld
|
10
|
+
#
|
11
|
+
# This method is part of Quickl's private interface even
|
12
|
+
# if its effect are considered public.
|
13
|
+
#
|
14
|
+
def command2module(name)
|
15
|
+
case name
|
16
|
+
when String
|
17
|
+
name.gsub(/^(.)|-(.)/){|x| x.upcase}.gsub(/-/,'')
|
18
|
+
when Symbol
|
19
|
+
command2module(name.to_s).to_sym
|
20
|
+
else
|
21
|
+
raise ArgumentError, "Invalid name argument #{name.class}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Converts a module name to a command name. Implements the
|
27
|
+
# following conversions:
|
28
|
+
#
|
29
|
+
# Hello -> hello
|
30
|
+
# HelloWorld -> hello-world
|
31
|
+
#
|
32
|
+
# This method is part of Quickl's private interface even
|
33
|
+
# if its effect are considered public.
|
34
|
+
#
|
35
|
+
def module2command(mod)
|
36
|
+
case mod
|
37
|
+
when Module
|
38
|
+
name = RubyTools::class_unqualified_name(mod)
|
39
|
+
name = name.gsub(/[A-Z]/){|x| "-#{x.downcase}"}[1..-1]
|
40
|
+
when String
|
41
|
+
name = mod.to_s
|
42
|
+
name = name.gsub(/[A-Z]/){|x| "-#{x.downcase}"}[1..-1]
|
43
|
+
when Symbol
|
44
|
+
name = mod.to_s
|
45
|
+
name = name.gsub(/[A-Z]/){|x| "-#{x.downcase}"}[1..-1]
|
46
|
+
name.to_sym
|
47
|
+
else
|
48
|
+
raise ArgumentError, "Invalid module argument #{mod.class}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end # module Naming
|
53
|
+
end # module Quickl
|