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,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'
|