shake 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,229 @@
1
+ Shake
2
+ =====
3
+
4
+ **Simple command runner.**
5
+
6
+ Goals:
7
+
8
+ * Shake lets you create extendable CLI runners very easily.
9
+
10
+ * Shake intends to replicate Thor/Rake's basic functionality in one very small package.
11
+
12
+ Why not Rake or Thor?
13
+
14
+ * Shake starts up way faster than Thor and Rake.
15
+ * Rake doesn't support arguments.
16
+ * Rake can't be extended easily to support anything more than `rake` + `Rakefile`.
17
+ * Thor can be too huge for your purposes.
18
+
19
+ Shake was made with the idea of being easily-embeddable into your projects
20
+ for your command line runners. It's a single ~4kb Ruby file.
21
+
22
+ # Shakefile
23
+ # Place this file in your project somewhere
24
+ class Shake
25
+ task(:start) {
26
+ puts "Starting '#{params.join(' ')}'..."
27
+ }
28
+
29
+ task.description = "Starts something"
30
+
31
+ task(:stop) {
32
+ puts "Stopping..."
33
+ }
34
+
35
+ task.description = "Stops it"
36
+ end
37
+
38
+ In your shell:
39
+
40
+ $ shake start server
41
+ Starting 'server'...
42
+
43
+ $ shake stop
44
+ Stopping...
45
+
46
+ $ shake help
47
+ Commands:
48
+ start Starts something
49
+ stop Stops it
50
+ help Shows a list of commands
51
+
52
+ Usage
53
+ =====
54
+
55
+ ### Using Shakefiles
56
+
57
+ Using the command `shake` will load your project's `Shakefile`.
58
+
59
+ # ~/project/Shakefile
60
+ class Shake
61
+ task(:deploy) do
62
+ puts "Deploying..."
63
+ system "ssh admin@server.com git pull && thin restart"
64
+ end
65
+ end
66
+
67
+ And in your shell:
68
+
69
+ $ cd ~/project
70
+ $ shake deploy
71
+ Deploying...
72
+
73
+ ### Parameters
74
+
75
+ Get the parameters with `params` (an array). Verify parameters with `wrong_usage`.
76
+
77
+ task(:init) do
78
+ wrong_usage if params.empty?
79
+ system "wget #{params.first}"
80
+ end
81
+
82
+ Example:
83
+
84
+ $ shake init
85
+ Invalid usage.
86
+ See `shake help` for more information.
87
+
88
+ ### Parameter options and flags
89
+
90
+ You may get params from it with `params.extract` or `params.delete`. Doing `extract`
91
+ will remove it from params.
92
+
93
+ task(:create) do
94
+ type = params.extract('-t') || 'default'
95
+ quiet = params.delete('-q')
96
+ file = params.shift
97
+ wrong_usage if params.any?
98
+
99
+ puts "Creating '#{file}' (quiet: #{!!quiet}, type: #{type})"
100
+ end
101
+
102
+ Example:
103
+
104
+ $ shake create #=> Invalid
105
+ $ shake create foobar #=> Creating 'foobar' (quiet: false, type: default)
106
+ $ shake create foobar -q #=> Creating 'foobar' (quiet: true, type: default)
107
+ $ shake create foobar -t xyz #=> Creating 'foobar' (quiet: false, type: xyz)
108
+
109
+ # Common commands
110
+
111
+ Use `err` to print something to STDERR. Use `pass` to halt execution.
112
+
113
+ task(:delete) do
114
+ unless File.exists?(params.first)
115
+ err 'You can't delete something that doesn't exist!'
116
+ pass
117
+ end
118
+
119
+ FileUtils.rm_rf params.first
120
+ end
121
+
122
+ You may also pass parameters to `pass` to have it printed out before halting.
123
+
124
+ pass 'The target already exists.' if File.exists?(target)
125
+
126
+ ### Default tasks
127
+
128
+ Use `default` to specify a default task. (The default task is usually `help`)
129
+
130
+ class Shake
131
+ task(:test) do
132
+ Dir['test/**/*_test.rb'].each { |f| load f }
133
+ end
134
+
135
+ default :test
136
+ end
137
+
138
+ # Typing `shake` will be the same as `shake test`
139
+
140
+ ### Invalid commands
141
+
142
+ Use `invalid` to define what happens when
143
+
144
+ class Shake
145
+ invalid {
146
+ err "Invalid command. What's wrong with you?"
147
+ }
148
+ end
149
+
150
+ In your shell:
151
+
152
+ $ shake foobar
153
+ Invalid command. What's wrong with you?
154
+
155
+ ### Defining helpers
156
+
157
+ Tasks are executed in the class's context, so just define your helpers like so:
158
+
159
+ module Helpers
160
+ def say_status(what, str)
161
+ puts "%15s %s" % [ what, str ]
162
+ end
163
+ end
164
+
165
+ class Shake
166
+ extend Helpers
167
+ end
168
+
169
+ Then use them in your tasks.
170
+
171
+ class Shake
172
+ task(:info) do
173
+ say_status :info, "It's a fine day"
174
+ end
175
+ end
176
+
177
+ ### Manual invocation
178
+
179
+ You can use shake in your projects without using the `shake` command. (recommended!)
180
+
181
+ require 'shake'
182
+
183
+ # If you want to load your own project file (optional)
184
+ file = Shake.find_in_project('Projectfile') and load file
185
+
186
+ # Now define some tasks, and then:
187
+ Shake.run!
188
+
189
+ ### Subclassing Shake
190
+
191
+ You may subclass shake for your own project.
192
+
193
+ By default, it will not have any of the default tasks (that is, `shake help`, and
194
+ the "invalid command" message). Use `include Defaults` if you want this behavior.
195
+
196
+ require 'shake'
197
+
198
+ class CLI < Shake
199
+ include Defaults # optional, see above
200
+
201
+ task(:flip) do
202
+ what = rand < 0.5 ? "heads" : "tails"
203
+ puts "The coin says #{what}"
204
+ end
205
+ end
206
+
207
+ CLI.run!
208
+
209
+ ### Defining tasks
210
+
211
+ # In your Shakefile or something
212
+ class Shake
213
+ task(:reset) do
214
+ # ...
215
+ end
216
+
217
+ task.description = "Resets passwords."
218
+ end
219
+
220
+ Alternatively:
221
+
222
+ class Shake
223
+ task(:reset) do
224
+ # ...
225
+ end
226
+
227
+ task(:reset).description = "Resets passwords."
228
+ end
229
+
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ task :test do
2
+ Dir['test/**/*_test.rb'].each { |f| load f }
3
+ end
4
+
5
+ task :default => :test
data/examples/example ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+ require 'shake'
3
+
4
+ ## Example
5
+ # This is a very simple example of using Shake with a subclass.
6
+
7
+ class Monk < Shake
8
+ # Include the Shake default commands (like `help` and stuff)
9
+ # This is optional, but you probably want this for most cases.
10
+ include Defaults
11
+
12
+ # Define a task like this.
13
+ # The other command line options will be accesible as the
14
+ # `params` array. Pass it onto Clap or something.
15
+ task(:start) {
16
+ puts "Running..."
17
+ puts "Your options: #{params.inspect}"
18
+ }
19
+
20
+ # Just `task` will return the last defined task.
21
+ # By the way, only `help` cares about the description (skip
22
+ # it if you wish), and you can add as much metadata to the
23
+ # tasks as you need.
24
+ task.description = "Starts the server"
25
+
26
+ # You may also define task metadata like this.
27
+ # Note that there are no namespaces, but feel free to use
28
+ # the colon anyway.
29
+ task(:'redis:start', :description => "Runs redis") {
30
+ puts "Starting redis..."
31
+ }
32
+
33
+ # Or this way:
34
+ task(:stop) {
35
+ puts "Stopping..."
36
+ }
37
+
38
+ task(:stop).description = "Stops the server"
39
+ end
40
+
41
+ # This gets ARGV and dispatches the appropriate task
42
+ Monk.run!
43
+
44
+ # Example output:
45
+ #
46
+ # $ ./monk
47
+ # Usage: monk <command>
48
+ #
49
+ # Commands:
50
+ # help Shows a list of commands
51
+ # start Starts the server
52
+ # redis:start Runs redis
53
+ # stop Stops the server
54
+ #
55
+ # $ ./monk start production
56
+ # Running...
57
+ # Your options: ['production']
58
+ #
59
+ # $ ./monk aaaa
60
+ # Unknown command: aaaa
61
+ # See `monk help` for a list of commands.
data/examples/example2 ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ require 'shake'
3
+
4
+ ## Shake
5
+ # Shake is a very simple Thor/Rake replacement. It intends
6
+ # to replicate thor's basic functionality in <100loc.
7
+ #
8
+ # This is a very simple example.
9
+ #
10
+ Shake.task(:hello) {
11
+ puts "hi!"
12
+ }
13
+
14
+ Shake.run!
15
+
16
+ # Example output:
17
+ #
18
+ # $ ./example
19
+ # Usage: example <command>
20
+ #
21
+ # Commands:
22
+ # help
23
+ # hello
24
+ #
25
+ # $ ./example hello
26
+ # hi!
27
+ #
28
+ # $ ./example aaaa
29
+ # Unknown command: aaaa
30
+ # See `example help` for a list of commands.
data/lib/shake.rb ADDED
@@ -0,0 +1,186 @@
1
+ require 'ostruct'
2
+
3
+ class Shake
4
+ VERSION = "0.1.0"
5
+
6
+ Abort = Class.new(StandardError)
7
+
8
+ class << self
9
+ attr_reader :params
10
+ attr_reader :command
11
+
12
+ # By the way: every task gets evaluated in the context of the class.
13
+ # Just make more static methods (`def self.monkey`) if you need them
14
+ # in your tasks.
15
+ #
16
+ # This also means that all the methods here are available for use
17
+ # in your tasks.
18
+
19
+ # Returns a list of tasks.
20
+ # It gives out a hash with symbols as keys, and openstructs as values.
21
+ def tasks
22
+ @tasks ||= Hash.new
23
+ end
24
+
25
+ # Sets or retrieves a task.
26
+ # If no arguments are given, it returns the last task defined.
27
+ #
28
+ # Examples:
29
+ #
30
+ # task(:start) { ... }
31
+ # task(:start, description: 'Starts it') { ... }
32
+ # task(:start)
33
+ # task.description = "Starts it"
34
+ #
35
+ def task(what=nil, options={}, &blk)
36
+ return @last if what.nil?
37
+
38
+ key = what.downcase.to_sym
39
+ @last = tasks[key] = new_task(options, &blk) if block_given?
40
+ tasks[key]
41
+ end
42
+
43
+ # Sets or retrieves the default task.
44
+ #
45
+ # Examples:
46
+ #
47
+ # default :default_task
48
+ # default { ... }
49
+ #
50
+ def default(what=nil, &blk)
51
+ @default = what || blk || @default
52
+ end
53
+
54
+ # Sets or retrieves the 'invalid command' task.
55
+ # See `default` for examples.
56
+ def invalid(what=nil, &blk)
57
+ @invalid = what || blk || @invalid
58
+ end
59
+
60
+ # Invokes a task with the given arguments.
61
+ # You may even nest multiple task invocations.
62
+ #
63
+ # Examples:
64
+ #
65
+ # invoke(:start)
66
+ # invoke(:start, 'nginx', 'memcache')
67
+ #
68
+ def invoke(what, *args)
69
+ old, @params = @params, args.extend(Params)
70
+ return if what.nil?
71
+
72
+ begin
73
+ return instance_eval(&what) if what.is_a?(Proc)
74
+
75
+ task = task(what) or return nil
76
+ instance_eval &task.proc
77
+ true
78
+ rescue Abort
79
+ true
80
+ ensure
81
+ @params = old
82
+ end
83
+ end
84
+
85
+ # Stops the execution of a task.
86
+ def pass(msg=nil)
87
+ err msg unless msg.nil?
88
+ raise Abort
89
+ end
90
+
91
+ # Halts a task because of wrong usage.
92
+ #
93
+ # Example:
94
+ #
95
+ # wrong_usage if params.any?
96
+ #
97
+ def wrong_usage
98
+ invoke(invalid) and pass
99
+ end
100
+
101
+ # Runs with the given arguments and dispatches the appropriate task.
102
+ # Use `run!` if you want to go with command line arguments.
103
+ #
104
+ # Example:
105
+ #
106
+ # # This is equivalent to `invoke(:start, 'nginx')`
107
+ # run 'start', 'nginx'
108
+ #
109
+ def run(*argv)
110
+ return invoke(default) if argv.empty?
111
+
112
+ @command = argv.shift
113
+ invoke(@command, *argv) or invoke(invalid, *argv)
114
+ end
115
+
116
+ def run!
117
+ run *ARGV
118
+ end
119
+
120
+ def executable
121
+ File.basename($0)
122
+ end
123
+
124
+ def err(str="")
125
+ $stderr.write "#{str}\n"
126
+ end
127
+
128
+ # Traverse back it's parents and find the file called `file`.
129
+ def find_in_project(file)
130
+ dir = Dir.pwd
131
+ while true
132
+ path = File.join(dir, file)
133
+ return path if File.exists?(path)
134
+
135
+ parent = File.expand_path(File.join(dir, '..'))
136
+ return nil if parent == dir
137
+
138
+ dir = parent
139
+ end
140
+ end
141
+
142
+ protected
143
+ def new_task(options={}, &blk)
144
+ t = OpenStruct.new(options)
145
+ t.proc = blk
146
+ t
147
+ end
148
+ end
149
+
150
+ module Params
151
+ def extract(what)
152
+ i = index(what) and slice!(i, 2)[1]
153
+ end
154
+ end
155
+
156
+ # This is a list of default commands.
157
+ # The class Shake itself uses this, but if you subclass it,
158
+ # you will have to do `include Defaults` yourself.
159
+ module Defaults
160
+ def self.included(to)
161
+ to.default :help
162
+
163
+ to.task(:help) {
164
+ err "Usage: #{executable} <command>"
165
+ err
166
+ err "Commands:"
167
+ tasks.each { |name, task| err " %-20s %s" % [ name, task.description ] }
168
+ }
169
+
170
+ to.task(:help).description = "Shows a list of commands"
171
+
172
+ to.invalid {
173
+ if task(command)
174
+ err "Invalid usage."
175
+ err "See `#{executable} help` for more info."
176
+ else
177
+ err "Unknown command: #{command}"
178
+ err "See `#{executable} help` for a list of commands."
179
+ end
180
+ }
181
+ end
182
+ end
183
+
184
+ include Defaults
185
+ end
186
+
data/test/mock.rb ADDED
@@ -0,0 +1,9 @@
1
+ class Shake
2
+ def self.puts(str='')
3
+ $out << "#{str}\n"
4
+ end
5
+
6
+ def self.err(str='')
7
+ $err << "#{str}\n"
8
+ end
9
+ end
@@ -0,0 +1,47 @@
1
+ $: << File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'shake'
4
+ require 'contest'
5
+ require 'fileutils'
6
+
7
+ require File.expand_path('../mock', __FILE__)
8
+
9
+ class Test::Unit::TestCase
10
+ def temp_path(*a)
11
+ root %w(test tmp) + a
12
+ end
13
+
14
+ def root(*a)
15
+ File.join File.expand_path('../../', __FILE__), *a
16
+ end
17
+
18
+ setup do
19
+ $out, $err = '', ''
20
+ FileUtils.rm_rf temp_path if File.directory?(temp_path)
21
+ FileUtils.mkdir_p temp_path
22
+ Dir.chdir temp_path
23
+ end
24
+
25
+ teardown do
26
+ FileUtils.rm_rf temp_path
27
+ end
28
+
29
+ def cout
30
+ $out
31
+ end
32
+
33
+ def cerr
34
+ $err
35
+ end
36
+
37
+ # Simulate bin/shake
38
+ def shake(*args)
39
+ ARGV.slice! 0, ARGV.length
40
+ args.each { |a| ARGV << a }
41
+ load File.expand_path('../../bin/shake', __FILE__)
42
+ end
43
+
44
+ def shakefile(str)
45
+ File.open('Shakefile', 'w') { |f| f.write str.strip }
46
+ end
47
+ end
@@ -0,0 +1,72 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class ParamsTest < Test::Unit::TestCase
4
+ class Bake < ::Shake
5
+ task :heat do
6
+ wrong_usage if params.size != 1
7
+ puts "OK"
8
+ end
9
+
10
+ task :cool do
11
+ quiet = params.delete('-q')
12
+ type = params.extract('-t') || params.extract('--type') || 'default'
13
+ wrong_usage unless params.empty?
14
+
15
+ puts "Quiet #{!!quiet}, type #{type}"
16
+ end
17
+
18
+ invalid do
19
+ err "What?"
20
+ end
21
+ end
22
+
23
+ test 'wrong usage' do
24
+ Bake.run *%w{heat}
25
+ assert cerr == "What?\n"
26
+ end
27
+
28
+ test 'wrong usage 2' do
29
+ Bake.run *%w{heat x y}
30
+ assert cerr == "What?\n"
31
+ end
32
+
33
+ test 'proper usage' do
34
+ Bake.run *%w{heat x}
35
+ assert cout == "OK\n"
36
+ end
37
+
38
+ test 'cool' do
39
+ Bake.run 'cool'
40
+ assert cout == "Quiet false, type default\n"
41
+ end
42
+
43
+ test 'wrong usage' do
44
+ Bake.run *%w{cool down}
45
+ assert cerr == "What?\n"
46
+ end
47
+
48
+ test 'cool 2' do
49
+ Bake.run *%w{cool -q}
50
+ assert cout == "Quiet true, type default\n"
51
+ end
52
+
53
+ test 'cool 3' do
54
+ Bake.run *%w{cool -t special}
55
+ assert cout == "Quiet false, type special\n"
56
+ end
57
+
58
+ test 'cool 4' do
59
+ Bake.run *%w{cool -t special -q}
60
+ assert cout == "Quiet true, type special\n"
61
+ end
62
+
63
+ test 'cool 5' do
64
+ Bake.run *%w{cool -q -t}
65
+ assert cout == "Quiet true, type default\n"
66
+ end
67
+
68
+ test 'wrong usage again' do
69
+ Bake.run *%w{cool -q xyz}
70
+ assert cerr == "What?\n"
71
+ end
72
+ end
@@ -0,0 +1,44 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class ShakefileTest < Test::Unit::TestCase
4
+ test 'no shakefile' do
5
+ shake
6
+ assert cout.empty?
7
+ assert cerr.include?('No Shakefile')
8
+ end
9
+
10
+ context 'basics' do
11
+ setup do
12
+ shakefile %{
13
+ class Shake
14
+ task :start do
15
+ puts "Starting..."
16
+ puts params.inspect if params.any?
17
+ end
18
+ task.description = 'Starts the server'
19
+ end
20
+ }
21
+ end
22
+
23
+ test 'help' do
24
+ shake 'help'
25
+ assert cout.empty?
26
+ assert cerr =~ /^ start *Starts the server/
27
+ end
28
+
29
+ test 'invalid command' do
30
+ shake 'stop'
31
+ assert cerr.include?('Unknown command: stop')
32
+ end
33
+
34
+ test 'run' do
35
+ shake 'start'
36
+ assert_equal "Starting...\n", cout
37
+ end
38
+
39
+ test 'run with args' do
40
+ shake *%w(start x)
41
+ assert_equal "Starting...\n[\"x\"]\n", cout
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,29 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class SubclassTest < Test::Unit::TestCase
4
+ class Bake < ::Shake
5
+ task :melt do
6
+ puts "Working..."
7
+ end
8
+
9
+ invalid do
10
+ err "What?"
11
+ end
12
+ end
13
+
14
+ test 'subclassing' do
15
+ Bake.run 'melt'
16
+ assert cout == "Working...\n"
17
+ end
18
+
19
+ test 'nothing to do' do
20
+ Bake.run
21
+ assert cerr.empty?
22
+ assert cout.empty?
23
+ end
24
+
25
+ test 'invalid command' do
26
+ Bake.run 'foobar'
27
+ assert cerr == "What?\n"
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class TraverseTest < Test::Unit::TestCase
4
+ test 'traverse backwards' do
5
+ shakefile %{
6
+ Shake.task(:create) { puts 'Creating...' }
7
+ }
8
+
9
+ Dir.mkdir 'xxx'
10
+ Dir.chdir 'xxx'
11
+
12
+ shake 'create'
13
+ assert_equal "Creating...\n", cout
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shake
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Rico Sta. Cruz
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-02-15 00:00:00 +08:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: Shake is a simple replacement for Thor/Rake.
18
+ email:
19
+ - rico@sinefunc.com
20
+ executables: []
21
+
22
+ extensions: []
23
+
24
+ extra_rdoc_files: []
25
+
26
+ files:
27
+ - lib/shake.rb
28
+ - test/mock.rb
29
+ - test/test_helper.rb
30
+ - test/unit/params_test.rb
31
+ - test/unit/shakefile_test.rb
32
+ - test/unit/subclass_test.rb
33
+ - test/unit/traverse_test.rb
34
+ - examples/example
35
+ - examples/example2
36
+ - README.md
37
+ - Rakefile
38
+ has_rdoc: true
39
+ homepage: http://github.com/rstacruz/shake
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options: []
44
+
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.5.0
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: Simple command line runner.
66
+ test_files: []
67
+