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,39 @@
|
|
1
|
+
class Delegate
|
2
|
+
#
|
3
|
+
# Say hello
|
4
|
+
#
|
5
|
+
# SYNOPSIS
|
6
|
+
# #{program_name} #{command_name} [--capitalize] [WHO]
|
7
|
+
#
|
8
|
+
# OPTIONS
|
9
|
+
# #{summarized_options}
|
10
|
+
#
|
11
|
+
# DESCRIPTION
|
12
|
+
# Without any argument, says hello to the world. When a single argument
|
13
|
+
# is given says hello to the user.
|
14
|
+
#
|
15
|
+
class HelloWorld < Quickl::Command(__FILE__, __LINE__)
|
16
|
+
|
17
|
+
# Install command options
|
18
|
+
options do |opt|
|
19
|
+
|
20
|
+
# Capitalize user name?
|
21
|
+
opt.on("--capitalize", "-c", "Capitalize user name") do
|
22
|
+
@capitalize = true
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
# Execute the command on some arguments
|
28
|
+
def execute(args)
|
29
|
+
if args.size <= 1
|
30
|
+
name = args.first || "world"
|
31
|
+
name = name.capitalize if @capitalize
|
32
|
+
puts "Hello #{name}!"
|
33
|
+
else
|
34
|
+
raise Quickl::InvalidArgument, "Useless arguments: #{args.join(' ')}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end # class HelloWorld
|
39
|
+
end # class Delegate
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Delegate
|
2
|
+
#
|
3
|
+
# Show help about a specific command
|
4
|
+
#
|
5
|
+
# SYNOPSIS
|
6
|
+
# #{program_name} #{command_name} help COMMAND
|
7
|
+
#
|
8
|
+
class Help < Quickl::Command(__FILE__, __LINE__)
|
9
|
+
|
10
|
+
# Let NoSuchCommandError be passed to higher stage
|
11
|
+
no_react_to Quickl::NoSuchCommand
|
12
|
+
|
13
|
+
# Command execution
|
14
|
+
def execute(args)
|
15
|
+
if args.size != 1
|
16
|
+
puts super_command.help
|
17
|
+
else
|
18
|
+
cmd = has_command!(args.first, super_command)
|
19
|
+
puts cmd.help
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end # class Help
|
24
|
+
end # class Delegate
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'stringio'
|
3
|
+
begin
|
4
|
+
require 'quickl'
|
5
|
+
rescue LoadError
|
6
|
+
$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
|
7
|
+
require 'quickl'
|
8
|
+
end
|
9
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
10
|
+
require 'delegate'
|
11
|
+
|
12
|
+
class DelegateTest < Test::Unit::TestCase
|
13
|
+
|
14
|
+
def assert_exits(match, exit_code)
|
15
|
+
yield
|
16
|
+
assert_false true, "Expected to exit with #{match}, nothing raised"
|
17
|
+
rescue Quickl::Exit => ex
|
18
|
+
assert_equal ex.exit_code, exit_code
|
19
|
+
assert ex.message =~ match
|
20
|
+
end
|
21
|
+
|
22
|
+
def run_command(*args)
|
23
|
+
$stdout = StringIO.new
|
24
|
+
Delegate.no_react_to(Quickl::Exit)
|
25
|
+
Delegate.run args
|
26
|
+
$stdout.string
|
27
|
+
ensure
|
28
|
+
$stdout = STDOUT
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_alone
|
32
|
+
assert_exits(/Delegate execution to a sub command/, 0){ run_command }
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_help_option
|
36
|
+
assert_exits(/DESCRIPTION/, 0){
|
37
|
+
run_command("--help")
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_version_option
|
42
|
+
assert_exits(/(c)/, 0){
|
43
|
+
run_command("--version")
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_no_such_option
|
48
|
+
assert_exits(/invalid option/, -1){
|
49
|
+
run_command("--no-such-option")
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_help_delegation
|
54
|
+
assert run_command("help", "hello-world") =~ /Say hello/
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_hello_delegation
|
58
|
+
assert_equal "Hello world!\n", run_command("hello-world")
|
59
|
+
assert_equal "Hello bob!\n", run_command("hello-world", "bob")
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_hello_capitalize
|
63
|
+
assert_equal "Hello World!\n", run_command("hello-world", "--capitalize")
|
64
|
+
assert_equal "Hello Bob!\n", run_command("hello-world", "bob", "--capitalize")
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# Quickl example: hello
|
2
|
+
|
3
|
+
This example shows how to create a really simple commandline program.
|
4
|
+
|
5
|
+
## Structure
|
6
|
+
|
7
|
+
The structure it follows is simply:
|
8
|
+
|
9
|
+
#
|
10
|
+
# Short description here
|
11
|
+
#
|
12
|
+
# SYNOPSIS
|
13
|
+
# Usage: ...
|
14
|
+
#
|
15
|
+
# OPTIONS
|
16
|
+
# #{sumarized_options}
|
17
|
+
#
|
18
|
+
# DESCRIPTION
|
19
|
+
# Long description here...
|
20
|
+
#
|
21
|
+
class SimpleCommand < Quickl::Command(__FILE__, __LINE__)
|
22
|
+
|
23
|
+
# install options below
|
24
|
+
options do |opt|
|
25
|
+
# _opt_ is an OptionParser instance
|
26
|
+
end
|
27
|
+
|
28
|
+
# install the code to run the command
|
29
|
+
def execute(args)
|
30
|
+
# _args_ are non-option command line parameters
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
# To run the command, typically in a bin/simple_command
|
36
|
+
# shell file
|
37
|
+
SimpleCommand.run(ARGV)
|
38
|
+
|
39
|
+
|
40
|
+
## Example
|
41
|
+
|
42
|
+
Try the following:
|
43
|
+
|
44
|
+
./hello
|
45
|
+
# => Hello world!
|
46
|
+
|
47
|
+
./hello --capitalize
|
48
|
+
# => Hello World!
|
49
|
+
|
50
|
+
./hello bob
|
51
|
+
# => Hello bob!
|
52
|
+
|
53
|
+
./hello --capitalize bob
|
54
|
+
# => Hello Bob!
|
55
|
+
|
56
|
+
./hello --version
|
57
|
+
# => hello 0.1.0 (c) 2010, Bernard Lambeau
|
58
|
+
|
59
|
+
./hello --help
|
60
|
+
# => ...
|
61
|
+
|
62
|
+
./hello too many arguments
|
63
|
+
# => needless argument: too many arguments
|
64
|
+
# => hello [--help] [--version] [--capitalize] [WHO]
|
65
|
+
|
66
|
+
./hello --no-such-option
|
67
|
+
# invalid option: --no-such-option
|
68
|
+
# hello [--help] [--version] [--capitalize] [WHO]
|
69
|
+
|
70
|
+
## You have to known that ...
|
71
|
+
|
72
|
+
* An **instance** of command is actually executed. Therefore, it is safe to install instance variables through options and to use them in execute().
|
73
|
+
* Documentation shown with --help is the rdoc documentation evaluated in the binding of the SimpleCommand **class**. Therefore, you can use #{...} to display specific things (like #{sumarized_options}).
|
74
|
+
* Default error handlers are installed by default to catch Interrupt, Quickl::Exit and OptionParser::Error. See error_handling example to learn more about them.
|
@@ -0,0 +1,57 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require File.expand_path('../../helper', __FILE__)
|
3
|
+
|
4
|
+
#
|
5
|
+
# Say hello
|
6
|
+
#
|
7
|
+
# SYNOPSIS
|
8
|
+
# #{program_name} [--help] [--version] [--capitalize] [WHO]
|
9
|
+
#
|
10
|
+
# OPTIONS
|
11
|
+
# #{summarized_options}
|
12
|
+
#
|
13
|
+
# DESCRIPTION
|
14
|
+
# Without any argument, says hello to the world. When a single argument
|
15
|
+
# is given says hello to the user.
|
16
|
+
#
|
17
|
+
class Hello < Quickl::Command(__FILE__, __LINE__)
|
18
|
+
|
19
|
+
# Command's version
|
20
|
+
VERSION = "0.1.0"
|
21
|
+
|
22
|
+
# Install command options
|
23
|
+
options do |opt|
|
24
|
+
|
25
|
+
# Capitalize user name?
|
26
|
+
opt.on("--capitalize", "-c", "Capitalize user name") do
|
27
|
+
@capitalize = true
|
28
|
+
end
|
29
|
+
|
30
|
+
# Show the help and exit
|
31
|
+
opt.on_tail("--help", "Show help") do
|
32
|
+
raise Quickl::Help
|
33
|
+
end
|
34
|
+
|
35
|
+
# Show version and exit
|
36
|
+
opt.on_tail("--version", "Show version") do
|
37
|
+
raise Quickl::Exit, "#{program_name} #{VERSION} (c) 2010, Bernard Lambeau"
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
# Execute the command on some arguments
|
43
|
+
def execute(args)
|
44
|
+
if args.size <= 1
|
45
|
+
name = args.first || "world"
|
46
|
+
name = name.capitalize if @capitalize
|
47
|
+
puts "Hello #{name}!"
|
48
|
+
else
|
49
|
+
raise Quickl::InvalidArgument, "Useless arguments: #{args.join(' ')}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end # class Hello
|
54
|
+
|
55
|
+
if __FILE__ == $0
|
56
|
+
Hello.run(ARGV)
|
57
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'stringio'
|
3
|
+
begin
|
4
|
+
require 'quickl'
|
5
|
+
rescue LoadError
|
6
|
+
$LOAD_PATH.unshift File.expand_path('../../../lib', __FILE__)
|
7
|
+
require 'quickl'
|
8
|
+
end
|
9
|
+
Kernel.load File.expand_path('../hello', __FILE__)
|
10
|
+
|
11
|
+
class HelloTest < Test::Unit::TestCase
|
12
|
+
|
13
|
+
def assert_exits(match, exit_code)
|
14
|
+
yield
|
15
|
+
assert_false true, "Expected to exit with #{match}, nothing raised"
|
16
|
+
rescue Quickl::Exit => ex
|
17
|
+
assert_equal exit_code, ex.exit_code
|
18
|
+
assert ex.message =~ match
|
19
|
+
end
|
20
|
+
|
21
|
+
def run_command(*args)
|
22
|
+
$stdout = StringIO.new
|
23
|
+
Hello.no_react_to(Quickl::Exit)
|
24
|
+
Hello.run args
|
25
|
+
$stdout.string
|
26
|
+
ensure
|
27
|
+
$stdout = STDOUT
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_normal_runs
|
31
|
+
assert_equal "Hello world!\n", run_command
|
32
|
+
assert_equal "Hello blambeau!\n", run_command("blambeau")
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_capitalize
|
36
|
+
assert_equal "Hello World!\n", run_command("--capitalize")
|
37
|
+
assert_equal "Hello Blambeau!\n", run_command("--capitalize", "blambeau")
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_version_option
|
41
|
+
assert_exits(/(c)/, 0){
|
42
|
+
run_command("--version")
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_help_option
|
47
|
+
assert_exits(/DESCRIPTION/, 0){
|
48
|
+
run_command("--help")
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def test_no_such_option
|
54
|
+
assert_exits(/invalid option/, -1){
|
55
|
+
run_command("--no-such-option")
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_too_many_arguments
|
60
|
+
assert_exits(/Useless arguments/, -1){
|
61
|
+
run_command('hello', 'too')
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
data/examples/helper.rb
ADDED
data/lib/quickl.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'quickl/ext/object'
|
2
|
+
require 'quickl/errors'
|
3
|
+
require 'quickl/ruby_tools'
|
4
|
+
require 'quickl/naming'
|
5
|
+
require 'quickl/command'
|
6
|
+
module Quickl
|
7
|
+
|
8
|
+
# Quickl's VERSION
|
9
|
+
VERSION = '0.1.1'.freeze
|
10
|
+
|
11
|
+
# Quickl's COPYRIGHT info
|
12
|
+
COPYRIGHT = "(c) 2010, Bernard Lambeau"
|
13
|
+
|
14
|
+
#
|
15
|
+
# Yields the block with the current command builder.
|
16
|
+
# A fresh new builder is created if not already done.
|
17
|
+
#
|
18
|
+
# This method is part of Quickl's private interface.
|
19
|
+
#
|
20
|
+
def self.command_builder
|
21
|
+
@builder ||= Command::Builder.new
|
22
|
+
yield @builder if block_given?
|
23
|
+
@builder
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Builds _command_ using the current builder.
|
28
|
+
#
|
29
|
+
# The current command builder is considered consumed
|
30
|
+
# and removed as a side effect. A RuntimeError is raised
|
31
|
+
# if no builder is currently installed.
|
32
|
+
#
|
33
|
+
# Returns the command itself.
|
34
|
+
#
|
35
|
+
# This method is part of Quickl's private interface.
|
36
|
+
#
|
37
|
+
def self.build_command(command)
|
38
|
+
unless @builder
|
39
|
+
raise "No command builder currently installed"
|
40
|
+
else
|
41
|
+
@builder.run(command)
|
42
|
+
@builder = nil
|
43
|
+
command
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end # module Quickl
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
module Quickl
|
3
|
+
class Command
|
4
|
+
extend Naming
|
5
|
+
|
6
|
+
# Methods installed on all command classes
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
# The super command, if any
|
10
|
+
attr_accessor :super_command
|
11
|
+
|
12
|
+
# Command's summary
|
13
|
+
attr_accessor :doc_place
|
14
|
+
|
15
|
+
# Returns the array of defined subcommands
|
16
|
+
def subcommands
|
17
|
+
@subcommands ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns true if this command has at least one
|
21
|
+
# subcommand
|
22
|
+
def has_sub_commands?
|
23
|
+
@subcommands and !@subcommands.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
############################################### Textual information about the command
|
27
|
+
|
28
|
+
# Returns name of the program under execution
|
29
|
+
def program_name
|
30
|
+
File.basename($0)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns command name
|
34
|
+
def command_name
|
35
|
+
module2command(self)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Loads and returns the documentation source
|
39
|
+
def doc_src
|
40
|
+
@doc_src ||= unless doc_place
|
41
|
+
"no documentation available"
|
42
|
+
else
|
43
|
+
file, line = doc_place
|
44
|
+
RubyTools::extract_file_rdoc(file, line, true)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns command documentation
|
49
|
+
def documentation
|
50
|
+
@documentation ||= instance_eval("%Q{#{doc_src}}", *doc_place)
|
51
|
+
end
|
52
|
+
alias :help :documentation
|
53
|
+
|
54
|
+
# Returns command usage
|
55
|
+
def usage
|
56
|
+
doc = documentation.split("\n")
|
57
|
+
doc.each_with_index{|line,i|
|
58
|
+
case line
|
59
|
+
when /Usage:/
|
60
|
+
return line.strip
|
61
|
+
when /SYNOPSIS/
|
62
|
+
return doc[i+1].strip || "no usage available"
|
63
|
+
end
|
64
|
+
}
|
65
|
+
"no usage available"
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns command overview
|
69
|
+
def overview
|
70
|
+
doc = documentation.split("\n")
|
71
|
+
doc.find{|s| !s.strip.empty?} || "no overview available"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Runs the command
|
75
|
+
def run(*args)
|
76
|
+
self.new.run(*args)
|
77
|
+
end
|
78
|
+
|
79
|
+
############################################### Error handling
|
80
|
+
|
81
|
+
# Bypass reaction to some exceptions
|
82
|
+
def no_react_to(*args)
|
83
|
+
@no_react_to ||= []
|
84
|
+
@no_react_to += args
|
85
|
+
end
|
86
|
+
|
87
|
+
# Should I bypass reaction to a given error?
|
88
|
+
def no_react_to?(ex)
|
89
|
+
@no_react_to && @no_react_to.find{|c|
|
90
|
+
ex.is_a?(c)
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
# Should I react to a given error?
|
95
|
+
def react_to?(ex)
|
96
|
+
!no_react_to?(ex)
|
97
|
+
end
|
98
|
+
|
99
|
+
end # module ClassMethods
|
100
|
+
|
101
|
+
# Methods installed on all command instances
|
102
|
+
module InstanceMethods
|
103
|
+
|
104
|
+
# Delegate unrecognized calls to the command class
|
105
|
+
# (gives access to options, help, usage, ...)
|
106
|
+
def method_missing(name, *args, &block)
|
107
|
+
if self.class.respond_to?(name)
|
108
|
+
self.class.send(name, *args, &block)
|
109
|
+
else
|
110
|
+
super
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Handles a command error
|
115
|
+
def handle_error(ex)
|
116
|
+
if react_to?(ex)
|
117
|
+
begin
|
118
|
+
ex.command = self
|
119
|
+
ex.react!
|
120
|
+
rescue Quickl::Error => ex2
|
121
|
+
handle_error(ex2)
|
122
|
+
end
|
123
|
+
else
|
124
|
+
raise ex
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# Runs the command from a requester file with command-line
|
130
|
+
# arguments.
|
131
|
+
#
|
132
|
+
# This method is intended to be overriden and does nothing
|
133
|
+
# by default.
|
134
|
+
#
|
135
|
+
def run(argv)
|
136
|
+
_run(argv)
|
137
|
+
rescue Quickl::Error => ex
|
138
|
+
handle_error(ex)
|
139
|
+
end
|
140
|
+
|
141
|
+
end # module InstanceMethods
|
142
|
+
|
143
|
+
# Tracks child classes
|
144
|
+
def self.inherited(command)
|
145
|
+
Quickl.build_command(command)
|
146
|
+
end
|
147
|
+
|
148
|
+
end # class Command
|
149
|
+
end # module Quickl
|
150
|
+
require 'quickl/command/builder'
|
151
|
+
require 'quickl/command/robustness'
|
152
|
+
require 'quickl/command/options'
|
153
|
+
require 'quickl/command/single'
|
154
|
+
require 'quickl/command/delegate'
|