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.
Files changed (46) hide show
  1. data/CHANGELOG.md +9 -0
  2. data/README.md +118 -0
  3. data/Rakefile +64 -0
  4. data/bin/quickl +116 -0
  5. data/examples/delegate/README.md +86 -0
  6. data/examples/delegate/bin/delegate +9 -0
  7. data/examples/delegate/lib/delegate.rb +41 -0
  8. data/examples/delegate/lib/hello_world.rb +39 -0
  9. data/examples/delegate/lib/help.rb +24 -0
  10. data/examples/delegate/test/delegate_test.rb +68 -0
  11. data/examples/hello/README.md +74 -0
  12. data/examples/hello/hello +57 -0
  13. data/examples/hello/hello_test.rb +65 -0
  14. data/examples/helper.rb +6 -0
  15. data/lib/quickl.rb +47 -0
  16. data/lib/quickl/command.rb +154 -0
  17. data/lib/quickl/command/builder.rb +58 -0
  18. data/lib/quickl/command/delegate.rb +53 -0
  19. data/lib/quickl/command/options.rb +55 -0
  20. data/lib/quickl/command/robustness.rb +18 -0
  21. data/lib/quickl/command/single.rb +27 -0
  22. data/lib/quickl/errors.rb +196 -0
  23. data/lib/quickl/ext/object.rb +29 -0
  24. data/lib/quickl/naming.rb +53 -0
  25. data/lib/quickl/ruby_tools.rb +60 -0
  26. data/quickl.gemspec +38 -0
  27. data/templates/single.erb +40 -0
  28. data/test/command/command_name.spec +16 -0
  29. data/test/command/documentation.spec +23 -0
  30. data/test/command/overview.spec +14 -0
  31. data/test/command/run.spec +22 -0
  32. data/test/command/subcommands.spec +20 -0
  33. data/test/command/usage.spec +16 -0
  34. data/test/mini_client.rb +59 -0
  35. data/test/naming/command2module.spec +17 -0
  36. data/test/naming/module2command.spec +21 -0
  37. data/test/ruby_tools/class_unqualified_name.spec +28 -0
  38. data/test/ruby_tools/extract_file_rdoc.spec +28 -0
  39. data/test/ruby_tools/fixtures.rb +27 -0
  40. data/test/ruby_tools/fixtures/RubyTools.rdoc +12 -0
  41. data/test/ruby_tools/fixtures/Utils.rdoc +3 -0
  42. data/test/ruby_tools/optional_args_block_call.spec +37 -0
  43. data/test/ruby_tools/parent_module.spec +23 -0
  44. data/test/spec_helper.rb +13 -0
  45. data/test/wrapping.rb +39 -0
  46. 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
@@ -0,0 +1,6 @@
1
+ begin
2
+ require 'quickl'
3
+ rescue LoadError
4
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
5
+ require 'quickl'
6
+ end
@@ -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'