quickl 0.1.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/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
|