sickle 0.0.1 → 0.1.0

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.
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