quickl 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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'