samovar 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a92bf8cd31c971fc9652c6cc1a1620f8587d8d41
4
+ data.tar.gz: acce28bc01d1016b2c6be77f15ea50ef88b6d576
5
+ SHA512:
6
+ metadata.gz: a127d1fea13405231ca8c3386f0c848fafa741aa328d38825187eeae1a0f728feb9374cceec196d17ebf569465cbc141e070d3b88033013d9527a0ca71f57df6
7
+ data.tar.gz: dadcdda3c02b1df2b46e2d656e4bf980a17a75bc16595410aeb195bf9131e6315395241a9f95f82d7c7b4105ef082267738ca1e52128adf9358bbdec52364041
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --warnings
@@ -0,0 +1,9 @@
1
+
2
+ SimpleCov.start do
3
+ add_filter "/spec/"
4
+ end
5
+
6
+ if ENV['TRAVIS']
7
+ require 'coveralls'
8
+ Coveralls.wear!
9
+ end
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ sudo: false
3
+ rvm:
4
+ - 2.1.8
5
+ - 2.2.4
6
+ - 2.3.0
7
+ - ruby-head
8
+ - rbx-2
9
+ env: COVERAGE=true
10
+ matrix:
11
+ fast_finish: true
12
+ allow_failures:
13
+ - rvm: ruby-head
14
+ - rvm: "rbx-2"
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in samovar.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'simplecov'
8
+ gem 'coveralls', require: false
9
+ end
@@ -0,0 +1,69 @@
1
+ # Samovar
2
+
3
+ ![Teapot](teapot.png)
4
+
5
+ Samovar is a modern framework for building command-line tools and applications. It provides a declarative class-based DSL for building command-line parsers that include automatic documentation generation `--help`. It helps you keep your functionality clean and isolated where possible.
6
+
7
+ [![Build Status](https://secure.travis-ci.org/ioquatix/samovar.svg)](http://travis-ci.org/ioquatix/samovar)
8
+ [![Code Climate](https://codeclimate.com/github/ioquatix/samovar.svg)](https://codeclimate.com/github/ioquatix/samovar)
9
+ [![Coverage Status](https://coveralls.io/repos/ioquatix/samovar/badge.svg)](https://coveralls.io/r/ioquatix/samovar)
10
+
11
+ ## Motivation
12
+
13
+ I've been using [Trollop](https://github.com/ManageIQ/trollop) and while it's not bad, it's hard to use for sub-commands in a way that generates nice documentation. It also has pretty limited support for complex command lines (e.g. nested commands, splits, matching tokens, etc). Samovar is a high level bridge between the command line and your code: it generates decent documentation, maps nicely between the command line syntax and your functions, and supports sub-commands using classes which are easy to compose.
14
+
15
+ One of the other issues I had with existing frameworks is testability. Most frameworks expect to have some pretty heavy logic directly in the binary executable, or at least don't structure your code in a way which makes testing easy. Samovar structures your command processing logic into classes which can be easily tested in isolation, which means that you can mock up and [spec your command-line executables easily](https://github.com/ioquatix/teapot/blob/master/spec/teapot/command_spec.rb).
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ gem 'samovar'
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install samovar
30
+
31
+ ## Usage
32
+
33
+ The best example of a working Samovar command line is probably [Teapot](https://github.com/ioquatix/teapot/blob/master/lib/teapot/command.rb). Please feel free to submit other examples and I will link to them here.
34
+
35
+ ## Contributing
36
+
37
+ 1. Fork it
38
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
39
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
40
+ 4. Push to the branch (`git push origin my-new-feature`)
41
+ 5. Create new Pull Request
42
+
43
+ ### Future Work
44
+
45
+ One area that I'd like to work on is line-wrapping. Right now, line wrapping is done by the terminal which is a bit ugly in some cases. There is a [half-implemented elegant solution](lib/samovar/output/line_wrapper.rb).
46
+
47
+ ## License
48
+
49
+ Released under the MIT license.
50
+
51
+ Copyright, 2016, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
52
+
53
+ Permission is hereby granted, free of charge, to any person obtaining a copy
54
+ of this software and associated documentation files (the "Software"), to deal
55
+ in the Software without restriction, including without limitation the rights
56
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
57
+ copies of the Software, and to permit persons to whom the Software is
58
+ furnished to do so, subject to the following conditions:
59
+
60
+ The above copyright notice and this permission notice shall be included in
61
+ all copies or substantial portions of the Software.
62
+
63
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
64
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
65
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
66
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
67
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
68
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
69
+ THE SOFTWARE.
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |task|
5
+ begin
6
+ require('simplecov/version')
7
+ task.rspec_opts = %w{--require simplecov} if ENV['COVERAGE']
8
+ rescue LoadError
9
+ end
10
+ end
11
+
12
+ task :default => :spec
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'samovar/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "samovar"
8
+ spec.version = Samovar::VERSION
9
+ spec.authors = ["Samuel Williams"]
10
+ spec.email = ["samuel.williams@oriontransfer.co.nz"]
11
+
12
+ spec.summary = %q{Samovar is a flexible option parser excellent support for sub-commands and help documentation.}
13
+ spec.homepage = "https://github.com/ioquatix/samovar"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "mapping", "~> 1.0.0"
22
+ spec.add_dependency "rainbow", "~> 2.0"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.11"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ end
@@ -0,0 +1,3 @@
1
+
2
+ require_relative 'samovar/version'
3
+ require_relative 'samovar/command'
@@ -0,0 +1,117 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'table'
22
+ require_relative 'options'
23
+ require_relative 'nested'
24
+ require_relative 'one'
25
+ require_relative 'many'
26
+ require_relative 'split'
27
+
28
+ require_relative 'output'
29
+
30
+ module Samovar
31
+ class IncompleteParse < StandardError
32
+ end
33
+
34
+ class Command
35
+ def self.parse(input)
36
+ command = self.new(input)
37
+
38
+ raise IncompleteParse.new("Could not parse #{input}") unless input.empty?
39
+
40
+ return command
41
+ end
42
+
43
+ def initialize(input)
44
+ self.class.table.parse(input) do |key, value|
45
+ self.send("#{key}=", value)
46
+ end
47
+ end
48
+
49
+ def [] key
50
+ @attributes[key]
51
+ end
52
+
53
+ class << self
54
+ attr_accessor :description
55
+ end
56
+
57
+ def self.table
58
+ @table ||= Table.new
59
+ end
60
+
61
+ def self.append(row)
62
+ attr_accessor(row.key) if row.respond_to?(:key)
63
+
64
+ self.table << row
65
+ end
66
+
67
+ def self.options(*args, **options, &block)
68
+ append Options.parse(*args, **options, &block)
69
+ end
70
+
71
+ def self.nested(*args, **options)
72
+ append Nested.new(*args, **options)
73
+ end
74
+
75
+ def self.one(*args, **options)
76
+ append One.new(*args, **options)
77
+ end
78
+
79
+ def self.many(*args, **options)
80
+ append Many.new(*args, **options)
81
+ end
82
+
83
+ def self.split(*args, **options)
84
+ append Split.new(*args, **options)
85
+ end
86
+
87
+ def self.usage(rows, name)
88
+ rows.nested(name, self) do |rows|
89
+ return unless @table
90
+
91
+ @table.rows.each do |row|
92
+ if row.respond_to?(:usage)
93
+ row.usage(rows)
94
+ else
95
+ rows << row
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ def self.command_line(name)
102
+ if @table
103
+ "#{name} #{@table.usage}"
104
+ else
105
+ name
106
+ end
107
+ end
108
+
109
+ def print_usage(*args, output: $stderr, formatter: Output::DetailedFormatter)
110
+ rows = Output::Rows.new
111
+
112
+ self.class.usage(rows, *args)
113
+
114
+ formatter.print(rows, output)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,102 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Samovar
22
+ class Flags
23
+ def initialize(text)
24
+ @text = text
25
+
26
+ @ordered = text.split(/\s+\|\s+/).map{|part| Flag.new(part)}
27
+ end
28
+
29
+ def each(&block)
30
+ @ordered.each(&block)
31
+ end
32
+
33
+ def first
34
+ @ordered.first
35
+ end
36
+
37
+ # Whether or not this flag should have a true/false value if not specified otherwise.
38
+ def boolean?
39
+ @ordered.count == 1 and @ordered.first.value.nil?
40
+ end
41
+
42
+ def count
43
+ return @ordered.count
44
+ end
45
+
46
+ def to_s
47
+ '[' + @ordered.join(' | ') + ']'
48
+ end
49
+
50
+ def parse(input)
51
+ @ordered.each do |flag|
52
+ if result = flag.parse(input)
53
+ return result
54
+ end
55
+ end
56
+
57
+ return nil
58
+ end
59
+ end
60
+
61
+ class Flag
62
+ def initialize(text)
63
+ @text = text
64
+
65
+ if text =~ /(.*?)\s(\<.*?\>)/
66
+ @prefix = $1
67
+ @value = $2
68
+ else
69
+ @prefix = @text
70
+ end
71
+
72
+ *@alternatives, @prefix = @prefix.split('/')
73
+ end
74
+
75
+ attr :text
76
+ attr :prefix
77
+ attr :alternatives
78
+ attr :value
79
+
80
+ def to_s
81
+ @text
82
+ end
83
+
84
+ def prefix?(token)
85
+ @prefix == token or @alternatives.include?(token)
86
+ end
87
+
88
+ def key
89
+ @key ||= @prefix.sub(/^-*/, '').gsub('-', '_').to_sym
90
+ end
91
+
92
+ def parse(input)
93
+ if prefix?(input.first)
94
+ if @value
95
+ input.shift(2).last
96
+ else
97
+ input.shift; key
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,47 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Samovar
22
+ class Many
23
+ def initialize(key, description, stop: /^-/)
24
+ @key = key
25
+ @description = description
26
+ @stop = stop
27
+ end
28
+
29
+ attr :key
30
+
31
+ def to_s
32
+ "<#{key}...>"
33
+ end
34
+
35
+ def to_a
36
+ [to_s, @description]
37
+ end
38
+
39
+ def parse(input)
40
+ if @stop and stop_index = input.index{|item| @stop === item}
41
+ input.shift(stop_index)
42
+ else
43
+ input.shift(input.size)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,56 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Samovar
22
+ class Nested
23
+ def initialize(name, commands, key: :command)
24
+ @name = name
25
+ @commands = commands
26
+ @key = key
27
+ end
28
+
29
+ attr :key
30
+
31
+ def to_s
32
+ @name
33
+ end
34
+
35
+ def to_a
36
+ [@name, "One of #{@commands.keys.join(', ')}."]
37
+ end
38
+
39
+ def parse(input)
40
+ if command = @commands[input.first]
41
+ input.shift
42
+
43
+ # puts "Instantiating #{command} with #{input}"
44
+ command.new(input)
45
+ end
46
+ end
47
+
48
+ def usage(rows)
49
+ rows << self
50
+
51
+ @commands.each do |key, klass|
52
+ klass.usage(rows, key)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,45 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Samovar
22
+ class One
23
+ def initialize(key, description, pattern: //)
24
+ @key = key
25
+ @description = description
26
+ @pattern = pattern
27
+ end
28
+
29
+ attr :key
30
+
31
+ def to_s
32
+ "<#{@key}>"
33
+ end
34
+
35
+ def to_a
36
+ [to_s, @description]
37
+ end
38
+
39
+ def parse(input)
40
+ if input.first =~ @pattern
41
+ input.shift
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,123 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'flags'
22
+
23
+ module Samovar
24
+ class Option
25
+ def initialize(flags, description, key: nil, default: nil, value: nil)
26
+ @flags = Flags.new(flags)
27
+ @description = description
28
+
29
+ if key
30
+ @key = key
31
+ else
32
+ @key = @flags.first.key
33
+ end
34
+
35
+ @default = default
36
+
37
+ @value = value
38
+ @value ||= true if @flags.boolean?
39
+ end
40
+
41
+ attr :flags
42
+ attr :description
43
+ attr :type
44
+
45
+ attr :key
46
+
47
+ def parse(input)
48
+ if result = @flags.parse(input)
49
+ @value.nil? ? result : @value
50
+ else
51
+ @default
52
+ end
53
+ end
54
+
55
+ def to_s
56
+ @flags
57
+ end
58
+
59
+ def to_a
60
+ unless @default.nil?
61
+ [@flags, @description, "Default: #{@default}"]
62
+ else
63
+ [@flags, @description]
64
+ end
65
+ end
66
+ end
67
+
68
+ class Options
69
+ def self.parse(*args, **options, &block)
70
+ options = self.new(*args, **options)
71
+
72
+ options.instance_eval(&block) if block_given?
73
+
74
+ return options
75
+ end
76
+
77
+ def initialize(title = "Options", key: :options)
78
+ @title = title
79
+ @ordered = []
80
+ @keyed = {}
81
+ @key = key
82
+ end
83
+
84
+ attr :key
85
+
86
+ def option(*args, **options)
87
+ self << Option.new(*args, **options)
88
+ end
89
+
90
+ def << option
91
+ @ordered << option
92
+ option.flags.each do |flag|
93
+ @keyed[flag.prefix] = option
94
+
95
+ flag.alternatives.each do |alternative|
96
+ @keyed[alternative] = option
97
+ end
98
+ end
99
+ end
100
+
101
+ def parse(input)
102
+ values = Hash.new
103
+
104
+ while option = @keyed[input.first]
105
+ if result = option.parse(input)
106
+ values[option.key] = result
107
+ end
108
+ end
109
+
110
+ return values
111
+ end
112
+
113
+ def to_s
114
+ @ordered.collect(&:to_s).join(' ')
115
+ end
116
+
117
+ def usage(rows)
118
+ @ordered.each do |option|
119
+ rows << option
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,166 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'mapping/model'
22
+ require 'rainbow'
23
+
24
+ module Samovar
25
+ module Output
26
+ class Header
27
+ def initialize(name, object)
28
+ @name = name
29
+ @object = object
30
+ end
31
+
32
+ attr :name
33
+ attr :object
34
+
35
+ def align(columns)
36
+ @object.command_line(@name)
37
+ end
38
+ end
39
+
40
+ class Row < Array
41
+ def initialize(object)
42
+ @object = object
43
+ super object.to_a.collect(&:to_s)
44
+ end
45
+
46
+ attr :object
47
+
48
+ def align(columns)
49
+ self.collect.with_index do |value, index|
50
+ value.ljust(columns.widths[index])
51
+ end.join(' ')
52
+ end
53
+ end
54
+
55
+ class Columns
56
+ def initialize(rows)
57
+ @rows = rows
58
+ @widths = calculate_widths(rows)
59
+ end
60
+
61
+ attr :widths
62
+
63
+ def calculate_widths(rows)
64
+ widths = []
65
+
66
+ rows.each do |row|
67
+ row.each.with_index do |column, index|
68
+ (widths[index] ||= []) << column.size
69
+ end
70
+ end
71
+
72
+ return widths.collect(&:max)
73
+ end
74
+ end
75
+
76
+ class Rows
77
+ include Enumerable
78
+
79
+ def initialize(level = 0)
80
+ @level = level
81
+ @rows = []
82
+ end
83
+
84
+ attr :level
85
+
86
+ def empty?
87
+ @rows.empty?
88
+ end
89
+
90
+ def first
91
+ @rows.first
92
+ end
93
+
94
+ def last
95
+ @rows.last
96
+ end
97
+
98
+ def indentation
99
+ @indentation ||= "\t" * @level
100
+ end
101
+
102
+ def each(ignore_nested: false, &block)
103
+ return to_enum(:each, ignore_nested: ignore_nested) unless block_given?
104
+
105
+ @rows.each do |row|
106
+ if row.is_a?(self.class)
107
+ row.each(&block) unless ignore_nested
108
+ else
109
+ yield row, self
110
+ end
111
+ end
112
+ end
113
+
114
+ def << object
115
+ @rows << Row.new(object)
116
+
117
+ return self
118
+ end
119
+
120
+ def columns
121
+ @columns ||= Columns.new(@rows.select{|row| row.is_a? Array})
122
+ end
123
+
124
+ def nested(*args)
125
+ @rows << Header.new(*args)
126
+
127
+ nested_rows = self.class.new(@level + 1)
128
+
129
+ yield nested_rows
130
+
131
+ @rows << nested_rows
132
+ end
133
+ end
134
+
135
+ class DetailedFormatter < Mapping::Model
136
+ def self.print(rows, output)
137
+ self.new(rows, output).print
138
+ end
139
+
140
+ def initialize(rows, output)
141
+ @rows = rows
142
+ @output = output
143
+ @width = 80
144
+ end
145
+
146
+ map(Header) do |header, rows|
147
+ @output.puts unless header == @rows.first
148
+ @output.puts "#{rows.indentation}#{Rainbow(header.object.command_line(header.name)).bright}"
149
+ @output.puts "#{rows.indentation}\t#{Rainbow(header.object.description).blue}"
150
+ @output.puts
151
+ end
152
+
153
+ map(Row) do |row, rows|
154
+ @output.puts "#{rows.indentation}#{row.align(rows.columns)}"
155
+ end
156
+
157
+ map(Rows) do |items|
158
+ items.collect{|row, rows| map(row, rows)}
159
+ end
160
+
161
+ def print
162
+ map(@rows)
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,87 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Samovar
22
+ module Output
23
+ # This is an incomplete implementation of an automatic line wrapping output buffer which handles any kind of output, provided it has special wrapping markers.
24
+ class LineWrapping
25
+ MARKER = "\e[0;0m".freeze
26
+
27
+ def initialize(output, wrapping_width = 80, minimum_width = 20)
28
+ @output = output
29
+ @wrapping_width = wrapping_width
30
+ @minimum_width = minimum_width
31
+ end
32
+
33
+ ESCAPE_SEQUENCE = /(.*?)(\e\[.*?m|$)/
34
+
35
+ def printable_width(text)
36
+ text.size
37
+ end
38
+
39
+ def wrap(line)
40
+ wrapping_offset = nil
41
+ offset = 0
42
+ buffer = String.new
43
+ lines = []
44
+ prefix = nil
45
+
46
+ line.scan(ESCAPE_SEQUENCE) do |text, escape_sequence|
47
+ width = printable_width(text)
48
+ next_offset = offset + printable_width
49
+
50
+ if next_offset > @wrapping_width
51
+ if wrapping_offset
52
+ text_wrap_offset = @wrapping_width - offset
53
+
54
+ # This text flows past the end of the line and we have a valid wrapping offset. We need to wrap this text.
55
+ if best_split_index = text.rindex(/\s/, text_wrap_offset) and best_split_index >= @minimum_width
56
+ # We have enough space to wrap.
57
+ buffer << text[0...best_split_index]
58
+ lines << buffer
59
+ buffer = String.new()
60
+ else
61
+ # In this case we can't really wrap on the current line. We fall back to letting the terminal wrap.
62
+ return line
63
+ end
64
+ else
65
+ # We don't have a specific wrapping offset, and the text flows longer than the wrapping width. We can't do anything - let the terminal wrap.
66
+ return line
67
+ end
68
+ else
69
+ buffer << text << escape_sequence
70
+ end
71
+
72
+ offset = next_offset
73
+
74
+ if wrapping_offset.nil? and offset < @wrapping_width and escape_sequence == MARKER
75
+ wrapping_offset = offset
76
+ end
77
+ end
78
+ end
79
+
80
+ def puts(*lines)
81
+ lines = lines.flat_map{|line| wrap(line)}
82
+
83
+ @output.puts(*lines)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,45 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Samovar
22
+ class Split
23
+ def initialize(key, description, marker: '--')
24
+ @key = key
25
+ @description = description
26
+ @marker = marker
27
+ end
28
+
29
+ attr :key
30
+
31
+ def to_s
32
+ "#{@marker} <#{@key}...>"
33
+ end
34
+
35
+ def to_a
36
+ [to_s, @description]
37
+ end
38
+
39
+ def parse(input)
40
+ if offset = input.index(@marker)
41
+ input.pop(input.size - offset).tap(&:shift)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,56 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Samovar
22
+ class Table
23
+ def initialize
24
+ @rows = []
25
+ @parser = []
26
+ end
27
+
28
+ attr :rows
29
+
30
+ def << row
31
+ @rows << row
32
+
33
+ if row.respond_to?(:parse)
34
+ @parser << row
35
+ end
36
+ end
37
+
38
+ def usage
39
+ items = Array.new
40
+
41
+ @rows.each do |row|
42
+ items << row.to_s
43
+ end
44
+
45
+ items.join(' ')
46
+ end
47
+
48
+ def parse(input)
49
+ @parser.each do |row|
50
+ if result = row.parse(input)
51
+ yield row.key, result, row
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,3 @@
1
+ module Samovar
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,48 @@
1
+
2
+ require 'samovar'
3
+ require 'stringio'
4
+
5
+ module Command
6
+ class Bottom < Samovar::Command
7
+ self.description = "Create a new teapot package using the specified repository."
8
+
9
+ one :project_name, "The name of the new project in title-case, e.g. 'My Project'."
10
+ many :packages, "Any additional packages you'd like to include in the project."
11
+ split :argv, "Additional arguments to be passed to the sub-process."
12
+ end
13
+
14
+ class Top < Samovar::Command
15
+ self.description = "A decentralised package manager and build tool."
16
+
17
+ options do
18
+ option '-c/--configuration <name>', "Specify a specific build configuration.", default: ENV['TEAPOT_CONFIGURATION']
19
+ option '-i/--in/--root <path>', "Work in the given root directory."
20
+ option '--verbose | --quiet', "Verbosity of output for debugging.", key: :logging
21
+ option '-h/--help', "Print out help information."
22
+ option '-v/--version', "Print out the application version."
23
+ end
24
+
25
+ nested '<command>',
26
+ 'bottom' => Bottom
27
+ end
28
+ end
29
+
30
+ describe Samovar do
31
+ it "should parse a simple command" do
32
+ top = Command::Top.parse(["-c", "path", "bottom", "foobar", "A", "B", "--", "args", "args"])
33
+
34
+ expect(top.options[:configuration]).to be == 'path'
35
+ expect(top.command.class).to be == Command::Bottom
36
+ expect(top.command.project_name).to be == 'foobar'
37
+ expect(top.command.packages).to be == ['A', 'B']
38
+ expect(top.command.argv).to be == ["args", "args"]
39
+ end
40
+
41
+ it "should generate documentation" do
42
+ top = Command::Top.new([])
43
+ buffer = StringIO.new
44
+ top.print_usage('top', output: buffer)
45
+
46
+ expect(buffer.string).to be_include(Command::Top.description)
47
+ end
48
+ end
Binary file
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: samovar
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-04-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mapping
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rainbow
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.11'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.11'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ description:
84
+ email:
85
+ - samuel.williams@oriontransfer.co.nz
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".simplecov"
93
+ - ".travis.yml"
94
+ - Gemfile
95
+ - README.md
96
+ - Rakefile
97
+ - flopp.gemspec
98
+ - lib/samovar.rb
99
+ - lib/samovar/command.rb
100
+ - lib/samovar/flags.rb
101
+ - lib/samovar/many.rb
102
+ - lib/samovar/nested.rb
103
+ - lib/samovar/one.rb
104
+ - lib/samovar/options.rb
105
+ - lib/samovar/output.rb
106
+ - lib/samovar/output/line_wrapper.rb
107
+ - lib/samovar/split.rb
108
+ - lib/samovar/table.rb
109
+ - lib/samovar/version.rb
110
+ - spec/samovar_spec.rb
111
+ - teapot.png
112
+ homepage: https://github.com/ioquatix/samovar
113
+ licenses:
114
+ - MIT
115
+ metadata: {}
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 2.5.1
133
+ signing_key:
134
+ specification_version: 4
135
+ summary: Samovar is a flexible option parser excellent support for sub-commands and
136
+ help documentation.
137
+ test_files:
138
+ - spec/samovar_spec.rb