samovar 1.0.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.
@@ -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