sickle 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fae05eea1313684c26edcc53f8e9058978e69aa6
4
- data.tar.gz: 3fa560a8a59862a7b981ef2cf3c3ac9f8fbd94ea
3
+ metadata.gz: 03ca9b3349e1160e67bf4a7377b1c8ecbae4994c
4
+ data.tar.gz: e9e1e24472aa2adb8187c71252b419fbffb4b13f
5
5
  SHA512:
6
- metadata.gz: 7cbea7eb809aeae6554824250dd09c7ee27f5a3bb57fd9c02c71b2aadbd7f32fdfda942d0968bdd745aea903ae4cf9c067ffcf9309150013ce0bc327897cf2b6
7
- data.tar.gz: 22aa62f4fe8b4497b399309a4b3d625595dba42749a69f0d99eb1b26432067f2ab8c54e7cf8b09ce531239d7496d75824d2fa5e3991bf62ec1f9e5e61e833ee0
6
+ metadata.gz: 1c5ee9bb8e2d698b345aa890e4f12085b2d69b38223be0fbbcc914f11d3a1b73a6c9fe2ceb59a74e1f8f3156dbc904c4f66b33c6a6dbb85074e4c451fcd1a722
7
+ data.tar.gz: 99d5204b414e825d52c9ef011c3f6490677f9d6bc32c904fb0cc05a03b9dc610ec6785d86fa1edef53c60c5be9eef0065960d7787ce2514042ecb6f9ce55ac2a
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'https://rubygems.org'
1
+ source 'http://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in sickle.gemspec
4
4
  gemspec
data/README.md CHANGED
@@ -1,24 +1,170 @@
1
1
  # Sickle
2
2
 
3
- TODO: Write a gem description
3
+ ## Description
4
4
 
5
- ## Installation
5
+ Sickle is dead simple library for building complex command line tools.
6
6
 
7
- Add this line to your application's Gemfile:
7
+ #### Features:
8
8
 
9
- gem 'sickle'
9
+ * Based on classes and modules
10
+ * Support for commands
11
+ * Support for namespaces
12
+ * Support for command options and global options
13
+ * Usage and help for free
14
+ * No external dependencies (only stdlib optparse)
10
15
 
11
- And then execute:
12
16
 
13
- $ bundle
17
+ ## Installation
14
18
 
15
- Or install it yourself as:
19
+ You are probably building command line tool that will be released as gem, just add that line to you gemspec.
16
20
 
17
- $ gem install sickle
21
+ ```ruby
22
+ spec.add_dependency 'sickle'
23
+ ```
18
24
 
19
25
  ## Usage
20
26
 
21
- TODO: Write usage instructions here
27
+ ### Basic usage
28
+
29
+ Simple create a class with methods and some options
30
+
31
+ ```ruby
32
+ require "sickle"
33
+
34
+ class App
35
+ include Sickle::Runner
36
+
37
+ global_option :verbose # global flag
38
+
39
+ desc "install one of the available apps" # command description
40
+ option :force # flag for `install` command
41
+ option :host, :default => "localhost" # option
42
+ def install(name)
43
+ if options[:host] # access options
44
+ # do something
45
+ end
46
+ # the rest
47
+ end
48
+
49
+ desc "list all apps, search is possible"
50
+ def list(search = "")
51
+ # ...
52
+ end
53
+
54
+ end
55
+
56
+ App.run(ARGV) # start parsing ARGV
57
+ ```
58
+
59
+ This will allow for execution command like:
60
+
61
+ ```bash
62
+ $ mytool install foo
63
+ $ mytool install foo --force --verbose --host 127.0.0.1
64
+ $ mytool list
65
+ $ mytool list rails --verbose
66
+ ```
67
+
68
+ Help is for free:
69
+
70
+ ```
71
+ $ mytool help
72
+ USAGE:
73
+ mytool COMMAND [ARG1, ARG2, ...] [OPTIONS]
74
+
75
+ TASKS:
76
+ help [COMMAND]
77
+ install NAME # install one of the available apps
78
+ list [SEARCH] # list all apps, search is possible
79
+
80
+ GLOBAL OPTIONS:
81
+ --verbose (default: false)
82
+ ```
83
+
84
+ There is also detailed help for command:
85
+
86
+ ```bash
87
+ $ mytool help install
88
+ USAGE:
89
+ mytool install NAME
90
+
91
+ DESCRIPTION:
92
+ install one of the available apps
93
+
94
+ OPTIONS:
95
+ --force (default: false)
96
+ --host (default: localhost)
97
+ ```
98
+
99
+
100
+ ### Advanced usage - multiple modules
101
+
102
+ ```ruby
103
+ module Users
104
+ include Sickle::Runner
105
+
106
+ desc "list all users"
107
+ def list
108
+ # ...
109
+ end
110
+
111
+ desc "create new user"
112
+ def create(name)
113
+ # ...
114
+ end
115
+ end
116
+
117
+ module Projects
118
+ include Sickle::Runner
119
+
120
+ desc "list all projects"
121
+ def list
122
+ # ...
123
+ end
124
+ end
125
+
126
+ module Global
127
+ include Sickle::Runner
128
+
129
+ desc "have some fun at top level"
130
+ def fun
131
+ # ...
132
+ end
133
+ end
134
+
135
+ class App
136
+ include Sickle::Runner
137
+
138
+ desc "top level command"
139
+ def main
140
+ # ...
141
+ end
142
+
143
+ include_modules :users => Users, # bind commands from Users module under "users" namespace
144
+ :p => Projects # bind commands from Projects module under "p" namespace
145
+
146
+ include Global # bind command from Global module at top level namespace
147
+ end
148
+
149
+ App.run(ARGV)
150
+
151
+ ```
152
+
153
+ Run `$ mytool help` to see how commands are namespaced:
154
+
155
+ ```bash
156
+ $ mytool help
157
+ USAGE:
158
+ mytool COMMAND [ARG1, ARG2, ...] [OPTIONS]
159
+
160
+ TASKS:
161
+ fun # have some fun at top level
162
+ help [COMMAND]
163
+ main # top level command
164
+ p:list # list all projects
165
+ users:create NAME # create new user
166
+ users:list # list all users
167
+ ```
22
168
 
23
169
  ## Contributing
24
170
 
data/Rakefile CHANGED
@@ -1 +1,9 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs.push "lib"
6
+ t.test_files = FileList['spec/*_spec.rb']
7
+ t.verbose = true
8
+ end
9
+
data/lib/sickle.rb CHANGED
@@ -1,5 +1,254 @@
1
1
  require "sickle/version"
2
+ require 'optparse'
2
3
 
3
4
  module Sickle
4
- # Your code goes here...
5
+ class << self
6
+ def push_desc(desc)
7
+ @__desc = desc
8
+ end
9
+
10
+ def pop_desc
11
+ d = @__desc
12
+ @__desc = nil
13
+ d
14
+ end
15
+
16
+ def push_option(name, opts)
17
+ @__options ||= {}
18
+ @__options[name] = Option.new(name, opts)
19
+ end
20
+
21
+ def pop_options
22
+ o = @__options || {}
23
+ @__options = {}
24
+ o
25
+ end
26
+
27
+ def push_namespace(n)
28
+ namespace << n
29
+ end
30
+
31
+ def pop_namespace
32
+ namespace.pop
33
+ end
34
+
35
+ def namespace
36
+ @__namespace ||= []
37
+ end
38
+ end
39
+
40
+ module Runner
41
+ def self.included(base)
42
+ base.extend(Sickle::ClassMethods)
43
+
44
+ if base.is_a?(Class)
45
+ base.send(:include, Sickle::Help)
46
+ base.method_added(:help)
47
+ end
48
+ end
49
+
50
+ def options
51
+ @__options ||= {}
52
+ end
53
+ end
54
+
55
+ class Command
56
+ attr_accessor :meth, :name, :desc, :options
57
+
58
+ def initialize(meth, name, desc, options)
59
+ @meth, @name, @desc, @options = meth, name, desc, options
60
+ end
61
+ end
62
+
63
+ class Option
64
+ attr_accessor :name, :opts, :default
65
+
66
+ def initialize(name, opts)
67
+ @name, @opts = name, opts
68
+
69
+ @default = opts[:default] || false
70
+
71
+ if @default == true || @default == false
72
+ @type = :boolean
73
+ else
74
+ @type = @default.class.to_s.downcase.to_sym
75
+ end
76
+ end
77
+
78
+ def register(parser, results)
79
+ if @type == :boolean
80
+ parser.on("--#{@name}", opts[:desc]) do
81
+ results[@name] = true
82
+ end
83
+ else
84
+ parser.on("--#{@name} #{@name.upcase}") do |v|
85
+ results[@name] = coerce(v)
86
+ end
87
+ end
88
+ end
89
+
90
+ def coerce(value)
91
+ case @default
92
+ when Fixnum
93
+ value.to_i
94
+ when Float
95
+ value.to_f
96
+ else
97
+ value
98
+ end
99
+ end
100
+
101
+ end
102
+
103
+ module Help
104
+ def help(command = nil)
105
+ if command
106
+ __display_help_for_command(command)
107
+ else
108
+ __display_help
109
+ end
110
+ end
111
+
112
+ def __display_help_for_command(name)
113
+ if cmd = self.class.__commands[name]
114
+ puts "USAGE:"
115
+ u, _ = __display_command_usage(name, cmd)
116
+ puts " #{$0} #{u}"
117
+ puts
118
+ puts "DESCRIPTION:"
119
+ puts cmd.desc.split("\n").map {|e| " #{e}"}.join("\n")
120
+ puts
121
+ unless cmd.options.empty?
122
+ puts "OPTIONS:"
123
+ cmd.options.each do |_, opt|
124
+ puts __display_option(opt)
125
+ end
126
+ end
127
+
128
+ __display_global_options
129
+ else
130
+ puts "\e[31mCommand '#{name}' not found\e[0m"
131
+ end
132
+ end
133
+
134
+ def __display_command_usage(name, command)
135
+ params = command.meth.parameters.map do |(r, p)|
136
+ r == :req ? p.upcase : "[#{p.upcase}]"
137
+ end
138
+
139
+ ["#{name} #{params.join(" ")}", command]
140
+ end
141
+
142
+ def __display_help
143
+ puts "USAGE:"
144
+ puts " #{$0} COMMAND [ARG1, ARG2, ...] [OPTIONS]"
145
+ puts
146
+
147
+ puts "TASKS:"
148
+ cmds = self.class.__commands.sort.map do |name, command|
149
+ __display_command_usage(name, command)
150
+ end
151
+ max = cmds.map {|a| a[0].length }.max
152
+ cmds.each do |(cmd, c)|
153
+ desc = c.desc ? "# #{c.desc}" : ""
154
+ puts " #{cmd.ljust(max)} #{desc}"
155
+ end
156
+
157
+ __display_global_options
158
+ end
159
+
160
+ def __display_global_options
161
+ unless self.class.__global_options.empty?
162
+ puts
163
+ puts "GLOBAL OPTIONS:"
164
+ self.class.__global_options.sort.each do |name, opt|
165
+ puts __display_option(opt)
166
+ end
167
+ end
168
+ end
169
+
170
+ def __display_option(opt)
171
+ " --#{opt.name} (default: #{opt.default})"
172
+ end
173
+ end
174
+
175
+ module ClassMethods
176
+ def included(base)
177
+ __commands.each do |name, command|
178
+ name = (Sickle.namespace + [name]).join(":")
179
+ base.__commands[name] = command
180
+ end
181
+ end
182
+
183
+ def desc(label)
184
+ Sickle.push_desc(label)
185
+ end
186
+
187
+ def global_option(name, opts = {})
188
+ __global_options[name.to_s] = Option.new(name, opts)
189
+ end
190
+
191
+ def option(name, opts = {})
192
+ Sickle.push_option(name, opts)
193
+ end
194
+
195
+ def include_modules(hash)
196
+ hash.each do |key, value|
197
+ Sickle.push_namespace(key)
198
+ send(:include, value)
199
+ Sickle.pop_namespace
200
+ end
201
+ end
202
+
203
+ def __commands
204
+ @__commands ||= {}
205
+ end
206
+
207
+ def __global_options
208
+ @__global_options ||= {}
209
+ end
210
+
211
+ def run(argv)
212
+ # puts "ARGV: #{argv.inspect}"
213
+
214
+ if command_name = argv.shift
215
+ if command = __commands[command_name]
216
+ all = __global_options.values + command.options.values
217
+
218
+ results = {}
219
+ args = OptionParser.new do |parser|
220
+ all.each do |option|
221
+ option.register(parser, results)
222
+ end
223
+ end.parse!(argv)
224
+
225
+ all.each do |o|
226
+ results[o.name] ||= o.default
227
+ end
228
+
229
+ # puts "args: #{args.inspect}"
230
+ # puts "results: #{results.inspect}"
231
+
232
+
233
+ obj = self.new
234
+ obj.instance_variable_set(:@__options, results)
235
+ command.meth.bind(obj).call(*args)
236
+ else
237
+ puts "\e[31mCommand '#{command_name}' not found\e[0m"
238
+ puts
239
+ run(["help"])
240
+ end
241
+ else
242
+ run(["help"])
243
+ end
244
+ end
245
+
246
+
247
+
248
+ def method_added(a)
249
+ meth = instance_method(a)
250
+ __commands[a.to_s] = Command.new(meth, a, Sickle.pop_desc, Sickle.pop_options)
251
+ end
252
+ end
5
253
  end
254
+
@@ -1,3 +1,3 @@
1
1
  module Sickle
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/sickle.gemspec CHANGED
@@ -20,4 +20,6 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_development_dependency "bundler", "~> 1.3"
22
22
  spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "minitest"
24
+ spec.add_development_dependency "turn"
23
25
  end
@@ -0,0 +1,68 @@
1
+ require "minitest/spec"
2
+ require "minitest/autorun"
3
+ require "turn"
4
+
5
+ require File.join(File.dirname(__FILE__), "test_app")
6
+
7
+ describe Sickle do
8
+ describe "DSL" do
9
+ it "list of global options" do
10
+ App.__global_options.keys.must_equal ["verbose", "debug"]
11
+ end
12
+
13
+ it "list of commands" do
14
+ App.__commands.keys.must_equal(
15
+ %w(task1 task2 conflict sub:sub1 sub:conflict other:other1 other:conflict nosub))
16
+ end
17
+
18
+ it "correct commands descriptions" do
19
+ App.__commands["task1"].desc.must_equal "Run task 1"
20
+ App.__commands["task2"].desc.must_equal "Run task 2"
21
+ App.__commands["sub:sub1"].desc.must_equal "Run task Sub1"
22
+ App.__commands["other:other1"].desc.must_equal "Run task other sub1"
23
+ App.__commands["nosub"].desc.must_equal "No sub for me"
24
+ end
25
+
26
+ it "correct commands options" do
27
+ App.__commands["task1"].options.keys.must_equal [:quiet]
28
+ App.__commands["task2"].options.keys.must_equal [:fast, :slow, :number]
29
+ end
30
+ end
31
+
32
+ describe "Runner" do
33
+ it "task1" do
34
+ App.run(["task1", "x", "y"]).must_equal(
35
+ ["task1", "x", "y", "def", false, false, false])
36
+ App.run(["task1", "x", "y", "z", "--verbose"]).must_equal(
37
+ ["task1", "x", "y", "z", false, true, false])
38
+ end
39
+
40
+ it "task2" do
41
+ App.run(["task2"]).must_equal(
42
+ ["task2", 10, false, false, false, false])
43
+ App.run(%w(task2 --fast)).must_equal(
44
+ ["task2", 10, true, false, false, false])
45
+ App.run(%w(task2 --slow)).must_equal(
46
+ ["task2", 10, false, true, false, false])
47
+ App.run(%w(task2 --verbose)).must_equal(
48
+ ["task2", 10, false, false, true, false])
49
+ App.run(%w(task2 --debug)).must_equal(
50
+ ["task2", 10, false, false, false, true])
51
+ App.run(%w(task2 --fast --slow --verbose)).must_equal(
52
+ ["task2", 10, true, true, true, false])
53
+ App.run(%w(task2 --number 40)).must_equal(
54
+ ["task2", 40, false, false, false, false])
55
+ end
56
+
57
+ it "sub:sub1" do
58
+ App.run(%w(sub:sub1)).must_equal(
59
+ ["sub1"])
60
+ end
61
+
62
+ it "conflict" do
63
+ App.run(%w(conflict)).must_equal ["nosub:conflict"]
64
+ App.run(%w(sub:conflict)).must_equal ["sub1:conflict"]
65
+ App.run(%w(other:conflict)).must_equal ["other1:conflict"]
66
+ end
67
+ end
68
+ end
data/spec/test_app.rb ADDED
@@ -0,0 +1,71 @@
1
+ require File.join(File.dirname(__FILE__), "..", "lib", "sickle")
2
+
3
+ module Sub
4
+ include Sickle::Runner
5
+
6
+ desc "Run task Sub1"
7
+ def sub1
8
+ p ["sub1"]
9
+ end
10
+
11
+ def conflict
12
+ p ["sub1:conflict"]
13
+ end
14
+ end
15
+
16
+ module Other
17
+ include Sickle::Runner
18
+
19
+ desc "Run task other sub1"
20
+ def other1(blah)
21
+ p ["other1", blah]
22
+ end
23
+
24
+ def conflict
25
+ p ["other1:conflict"]
26
+ end
27
+ end
28
+
29
+ module NoSub
30
+ include Sickle::Runner
31
+
32
+ desc "No sub for me"
33
+ def nosub
34
+ p ["nosub"]
35
+ end
36
+
37
+ def conflict
38
+ p ["nosub:conflict"]
39
+ end
40
+ end
41
+
42
+ class App
43
+ include Sickle::Runner
44
+
45
+ global_option :verbose
46
+ global_option :debug
47
+
48
+ desc "Run task 1"
49
+ option :quiet
50
+ def task1(a, b, c = "def")
51
+ p ["task1", a, b, c, options[:quiet], options[:verbose], options[:debug]]
52
+ end
53
+
54
+ desc "Run task 2"
55
+ option :fast
56
+ option :slow
57
+ option :number, :default => 10
58
+ def task2
59
+ p ["task2", options[:number], options[:fast], options[:slow], options[:verbose], options[:debug]]
60
+ end
61
+
62
+ def conflict
63
+ p ["app:conflict"]
64
+ end
65
+
66
+
67
+ include_modules :sub => Sub,
68
+ :other => Other
69
+
70
+ include NoSub
71
+ end
data/spec/test_run.rb ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__))
4
+ $:.unshift File.join(File.dirname(__FILE__), "..", "lib")
5
+
6
+ require "test_app"
7
+
8
+ App.run(ARGV)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sickle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tymon Tobolski
@@ -38,6 +38,34 @@ dependencies:
38
38
  - - '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: turn
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
41
69
  description: sickle
42
70
  email:
43
71
  - i@teamon.eu
@@ -53,6 +81,9 @@ files:
53
81
  - lib/sickle.rb
54
82
  - lib/sickle/version.rb
55
83
  - sickle.gemspec
84
+ - spec/runner_spec.rb
85
+ - spec/test_app.rb
86
+ - spec/test_run.rb
56
87
  homepage: ''
57
88
  licenses:
58
89
  - MIT
@@ -77,4 +108,7 @@ rubygems_version: 2.0.0
77
108
  signing_key:
78
109
  specification_version: 4
79
110
  summary: sickle
80
- test_files: []
111
+ test_files:
112
+ - spec/runner_spec.rb
113
+ - spec/test_app.rb
114
+ - spec/test_run.rb