clingon 0.0.1

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: 5abedd0654d50025ee7f73c7167dc19a37013edb
4
+ data.tar.gz: b84f98b0327be4704dd3d7269b9116e57606805c
5
+ SHA512:
6
+ metadata.gz: 40db83ed840a2090c225a728a47d2a054f672dad3baf60a283b85d8a1e4fbe6861c3ba7c6891e84b60cf51499a154c2e995a44d44fcbc1cb20bd5598942db120
7
+ data.tar.gz: 51578191d232fd15be7397712ee13203261d8dfc9318672d81409db4554e497751db9f0b3e5866fcb805f555283c2f1d081a2272c066bdbe17e6ffde3b65121e
@@ -0,0 +1,218 @@
1
+ # Clingon
2
+
3
+ [![Gem](https://img.shields.io/gem/v/clingon.svg?style=flat-square)](https://rubygems.org/gems/clingon)
4
+ [![GitHub issues](https://img.shields.io/github/issues/mardotio/clingon.svg?style=flat-square)](https://github.com/mardotio/clingon/issues)
5
+ [![Gem](https://img.shields.io/gem/dtv/clingon.svg?style=flat-square)](https://rubygems.org/gems/clingon)
6
+
7
+ ## Overview
8
+
9
+ The clingon gem can be used to easily parse command line input from a user.
10
+ It can also be configured so that you can validate the inputs you receive. This
11
+ gem can be used to parse something like this:
12
+
13
+ ```
14
+ --first_name bob --last_name smith --email bob.smith@email.com -h
15
+ ```
16
+
17
+ into this:
18
+
19
+ ```ruby
20
+ [
21
+ {
22
+ :name => 'first_name',
23
+ :value => 'bob'
24
+ },{
25
+ :name => 'last_name',
26
+ :value => 'smith'
27
+ },{
28
+ :name => 'email',
29
+ :value => 'bob.smith@email.com'
30
+ },{
31
+ :name => 'help',
32
+ :value => true
33
+ }
34
+ ]
35
+ ```
36
+
37
+ ## Table of Contents
38
+
39
+ <!-- TOC -->
40
+
41
+ - [Clingon](#clingon)
42
+ - [Overview](#overview)
43
+ - [Installation](#installation)
44
+ - [Setup](#setup)
45
+ - [Name](#name)
46
+ - [Short Name](#short-name)
47
+ - [Required](#required)
48
+ - [Empty](#empty)
49
+ - [Type](#type)
50
+ - [Check](#check)
51
+ - [Values](#values)
52
+ - [Use](#use)
53
+ - [Configuration](#configuration)
54
+ - [Parsing](#parsing)
55
+ - [Accessing Values](#accessing-values)
56
+ - [Examples](#examples)
57
+
58
+ <!-- /TOC -->
59
+
60
+ ## Installation
61
+
62
+ To install simply use Ruby's gem installer:
63
+
64
+ ```
65
+ gem install clingon
66
+ ```
67
+
68
+ ## Setup
69
+
70
+ In order to use the library, you must provide a structure that defines all
71
+ of the values you expect. This structure is expected to be an array of hashes
72
+ containing all the options you want. Each hash can contain any of the following
73
+ options:
74
+
75
+ |Option|Expected Value|Required|
76
+ |------|--------------|:------:|
77
+ |name|string|*|
78
+ |short_name|string||
79
+ |required|boolean||
80
+ |empty|boolean||
81
+ |type|string (see below)||
82
+ |check|regex/string||
83
+ |values|array||
84
+
85
+ **All keys in each hash must be ruby symbols**
86
+
87
+ `values`, `type`, and `check` are all used to validate that the received values
88
+ match what you were expecting. The parser will only use one of these settings to
89
+ validate an input, so you should only define the of those values per input. If
90
+ more than one of these values are defined, the parser will use them in the
91
+ following order: `values`, `type`, `check`.
92
+
93
+ ### Name
94
+
95
+ This value will be used as the long name for the command line flag. This value
96
+ must be defined in your structure for any value that you want to parse. This
97
+ value should be a `string`.
98
+
99
+ ### Short Name
100
+
101
+ The value will be used to create a shortened version of the flag. It is an
102
+ optional value, and it is recommended that it be a single character if possible,
103
+ but there is no limit on length.
104
+
105
+ ### Required
106
+
107
+ Specifies if the flag must be present. If the flag is not found when parsing,
108
+ it will throw an error. This value is optional, but if defined, value must
109
+ be either `true` or `false`; defaults to `false`.
110
+
111
+ ### Empty
112
+
113
+ This can be used if the flag does not require any value and is simply an option
114
+ marker (i.e -r for recursive or -h for help). This value is optional, but if
115
+ defined, value must be either `true` or `false`; defaults to `false`.
116
+
117
+ The `empty` flag is used for flags that don't need an additional value. It is
118
+ essentially a boolean value (`false` if absent `true` is present). If a value is
119
+ required it should not have the `empty` flag. Required values that have and
120
+ `empty` flag will simply ignore the `empty` option.
121
+
122
+ ### Type
123
+
124
+ This option allows you to define what type of value the flag is expecting. If
125
+ the received value does not match the expected type an error will be thrown.
126
+ Currently supported types are:
127
+
128
+ |Type|Description|
129
+ |----|--------|
130
+ |num|Any number, integer or float|
131
+ |int|Any integer|
132
+ |float|A decimal number|
133
+ |bool|`true` or `false`|
134
+
135
+ Additionally, when the values are parsed, the user input will be converted to
136
+ the specified type (int, float, bool). If `num` is specified, input will be
137
+ converted to either `float` or `int`. When specified in your structure, the type
138
+ must be specified as a string.
139
+
140
+ ### Check
141
+
142
+ Regular expression that should be used to check the input value against. This
143
+ can be a string (it will be converted to a regular expression), or a a regular
144
+ expression (i.e. `/^\d+$/`). If the received value does not match the specified
145
+ regex, an error will be thrown.
146
+
147
+ **If using a YAML configuration file, it is recommended that you use single
148
+ quotes (`'`) when specifing a regular expression. Not doing so may cause the
149
+ YAML parser to interpret some of the charaters as escape charaters.**
150
+
151
+ ### Values
152
+
153
+ An array of values that are acceptable for the flag. All elements of the array
154
+ should be strings since all inputs from a terminal are received by ruby as
155
+ strings. If the received value is not a member of the array, an error will be
156
+ thown.
157
+
158
+ ## Use
159
+
160
+ ### Configuration
161
+
162
+ To use the parser, you must first configure the library with your structure and
163
+ the inputs you wish to parse. There are four values you can configure.
164
+
165
+ |Setting|Description|Value|Required|
166
+ |-------|-----------|-----|:------:|
167
+ |structure|The structure to be used|Array|*|
168
+ |inputs|The inputs you need to parse|Array|*|
169
+ |delimiter|Delimiter for the flags (defaults to `-`)|String||
170
+ |strict|Whether parser should accept inputs that look like flags|Bool||
171
+
172
+ Optionally, you can use a YAML configuration file to set some or all of these
173
+ values (at the minimum, the file should contain the structure). To use this
174
+ option, pass a relative or absolute path to a YAML file. The parser expects to
175
+ find the same values as the table above as sybols (i.e `:structure`, `:strict`).
176
+ The only value that cannot be configured through the file are the inputs. To
177
+ use a file to configure the parser, just do:
178
+
179
+ ```ruby
180
+ Clingon.configure do |c|
181
+ c.conf_file = 'conf_file.yaml'
182
+ end
183
+ ```
184
+
185
+ ### Parsing
186
+
187
+ After you have configured the parser, you simply need to call the `parse`
188
+ method.
189
+
190
+ ```ruby
191
+ Clingon.parse
192
+ ```
193
+
194
+ ### Accessing Values
195
+
196
+ The parser has a `fetch` method that allows you to retrieve one or all of the
197
+ parsed values. To access the parsed values, simply call the method with the
198
+ value you want, or leave the arguments empty if you want to retrive all values.
199
+
200
+ if you query for a specific value, you must use the `name` that was used in the
201
+ configuration structure.
202
+
203
+ ```ruby
204
+ # This will return all parsed values as an array of hashes
205
+ all_values = Clingon.fetch
206
+
207
+ # This will return a hash containing the value that was requested, or nil if the
208
+ # value was not found
209
+ name = Clingon.fetch('name')
210
+ ```
211
+
212
+ ## Examples
213
+
214
+ [`structure.yaml`](/examples/structure.yaml) contains a structure in YAML
215
+ format. [`example_1.rb`](/examples/example_1.rb) makes use of the YAML file
216
+ configuration file. If you are not interested in using a YAML configuration
217
+ file, and instead want to define your structure within your script,
218
+ [`example_2.rb`](/examples/example_2.rb) defines an inline structure.
@@ -0,0 +1,151 @@
1
+ require 'clingon/errors'
2
+ require 'clingon/version'
3
+ require 'clingon/helpers/input_store'
4
+ require 'clingon/helpers/structure_checker'
5
+ require 'clingon/checks/checks'
6
+ require 'clingon/helpers/parser_configuration'
7
+ require 'yaml'
8
+
9
+ module Clingon
10
+ class << self
11
+ attr_accessor :conf, :store, :reserved
12
+ end
13
+
14
+ def self.configure
15
+ self.store ||= InputStore.new
16
+ self.conf ||= ParserConfiguration.new
17
+ yield(conf)
18
+ self.reserved = conf.structure.inject([]) do |all, current|
19
+ arr = ["#{conf.delimiter * 2}#{current[:name]}"]
20
+ arr << "#{conf.delimiter}#{current[:short_name]}" if current[:short_name]
21
+ all + arr
22
+ end
23
+ end
24
+
25
+ def self.fetch(value = nil)
26
+ if value
27
+ store.fetch(value)
28
+ else
29
+ store.inputs
30
+ end
31
+ end
32
+
33
+ def self.strict_parse
34
+ cli_inputs = conf.inputs.clone
35
+ cli_inputs.each do |input|
36
+ if input =~ /^#{conf.delimiter}{1,2}/ && !Clingon.reserved?(input)
37
+ raise(ReservedKeywordError.new(received: input, reserved: [/^-{1,2}/]))
38
+ end
39
+ end
40
+ end
41
+
42
+ def self.parse
43
+ Clingon.strict_parse if conf.strict
44
+ required_values = Clingon.get_required
45
+ Clingon.parse_required(required_values)
46
+ optional_values = Clingon.get_optional
47
+ Clingon.parse_optional(optional_values)
48
+ end
49
+
50
+ def self.get_required_value(flag)
51
+ name = "#{conf.delimiter * 2}#{flag[:name]}"
52
+ short_name = "#{conf.delimiter}#{flag[:short_name]}" if flag[:short_name]
53
+ index = conf.inputs.index(short_name) if short_name
54
+ index ||= conf.inputs.index(name)
55
+ raise(MissingArgumentError.new(name: name, short_name: short_name)) unless index
56
+ conf.inputs[index + 1]
57
+ end
58
+
59
+ def self.get_optional_value(flag)
60
+ empty = flag[:empty]
61
+ name = "#{conf.delimiter * 2}#{flag[:name]}"
62
+ short_name = "#{conf.delimiter}#{flag[:short_name]}" if flag[:short_name]
63
+ index = conf.inputs.index(short_name) if short_name
64
+ index ||= conf.inputs.index(name)
65
+ if index && empty
66
+ true
67
+ elsif empty
68
+ false
69
+ elsif index
70
+ conf.inputs[index + 1]
71
+ else
72
+ nil
73
+ end
74
+ end
75
+
76
+ def self.get_required
77
+ conf.structure.select { |flag| flag[:required] }
78
+ end
79
+
80
+ def self.get_optional
81
+ conf.structure.reject { |flag| flag[:required] }
82
+ end
83
+
84
+ def self.reserved?(value)
85
+ reserved.include?(value)
86
+ end
87
+
88
+ def self.convert_to_type(value, type)
89
+ value_to_convert = value.to_s
90
+ case type
91
+ when 'int'
92
+ value_to_convert.to_i
93
+ when 'float'
94
+ value_to_convert.to_f
95
+ when 'num'
96
+ if value_to_convert =~ /^\d+$/
97
+ value_to_convert.to_i
98
+ else
99
+ value_to_convert.to_f
100
+ end
101
+ when 'bool'
102
+ value_to_convert == 'true'
103
+ else
104
+ value_to_convert
105
+ end
106
+ end
107
+
108
+ def self.parse_required(required_structure)
109
+ required_structure.each do |flag|
110
+ check = flag[:check]
111
+ type = flag[:type]
112
+ allowed_values = flag[:values]
113
+ user_input = Clingon.get_required_value(flag)
114
+ if Clingon.reserved?(user_input)
115
+ raise(ReservedKeywordError.new(received: user_input, reserved: reserved))
116
+ end
117
+ if allowed_values
118
+ Clingon.check_allowed_value(user_input, allowed_values)
119
+ elsif type
120
+ Clingon.check_against_type(user_input, type)
121
+ elsif check
122
+ Clingon.check_against_regex(user_input, check)
123
+ end
124
+ store.store(flag[:name], user_input)
125
+ end
126
+ end
127
+
128
+ def self.parse_optional(optional_structure)
129
+ optional_structure.each do |flag|
130
+ check = flag[:check]
131
+ type = flag[:type]
132
+ allowed_values = flag[:values]
133
+ empty = flag[:empty]
134
+ user_input = Clingon.get_optional_value(flag)
135
+ if user_input && !empty
136
+ if Clingon.reserved?(user_input)
137
+ raise(ReservedKeywordError.new(received: user_input, reserved: reserved))
138
+ end
139
+ if allowed_values
140
+ Clingon.check_allowed_value(user_input, allowed_values)
141
+ elsif type
142
+ Clingon.check_against_type(user_input, type)
143
+ user_input = Clingon.convert_to_type(user_input, type)
144
+ elsif check
145
+ Clingon.check_against_regex(user_input, check)
146
+ end
147
+ end
148
+ store.store(flag[:name], user_input)
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,34 @@
1
+ require 'clingon/checks/type_regex'
2
+
3
+ module Clingon
4
+ def self.check_against_type(value, type)
5
+ case type
6
+ when 'int'
7
+ check = Clingon::INT.dup
8
+ when 'float'
9
+ check = Clingon::FLOAT.dup
10
+ when 'num'
11
+ check = Clingon::NUM.dup
12
+ when 'bool'
13
+ check = Clingon::BOOL.dup
14
+ else
15
+ raise(UnexpectedTypeError.new(received: type))
16
+ end
17
+
18
+ value_to_check = value.to_s
19
+
20
+ return if value_to_check =~ check
21
+ raise(TypeMatchError.new(expected: type, received: value))
22
+ end
23
+
24
+ def self.check_against_regex(value, check)
25
+ regex_check = Regexp.new(check)
26
+ return if value =~ regex_check
27
+ raise(MatchError.new(expected: regex_check, received: value))
28
+ end
29
+
30
+ def self.check_allowed_value(value, allowed)
31
+ return if allowed.index(value)
32
+ raise(UnexpectedValueError.new(expected: allowed, received: value))
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ module Clingon
2
+ INT = /^\d+$/
3
+ FLOAT = /^\d*\.\d+$/
4
+ NUM = /^(\d+|\d*\.\d+)$/
5
+ BOOL = /^(false|true)$/
6
+ end
@@ -0,0 +1,68 @@
1
+ module Clingon
2
+ class ConfigurationFileError < StandardError
3
+ end
4
+
5
+ class YAMLSyntaxError < StandardError
6
+ end
7
+
8
+ class MatchError < StandardError
9
+ attr_reader :expected, :received
10
+ def initialize(payload)
11
+ @expected = payload[:expected]
12
+ @received = payload[:received]
13
+ msg = "Value #{received} does not match #{expected}"
14
+ super(msg)
15
+ end
16
+ end
17
+
18
+ class MissingArgumentError < StandardError
19
+ attr_reader :name, :short_name
20
+ def initialize(payload)
21
+ @name = payload[:name]
22
+ @short_name = payload[:short_name]
23
+ name_arr = [name]
24
+ name_arr << short_name if short_name
25
+ msg = "Missing required input #{name_arr.join('/')}"
26
+ super(msg)
27
+ end
28
+ end
29
+
30
+ class UnexpectedValueError < StandardError
31
+ attr_reader :expected, :received
32
+ def initialize(payload)
33
+ @expected = payload[:expected]
34
+ @received = payload[:received]
35
+ msg = "Received #{received}, expected one of (#{expected.join(', ')})"
36
+ super(msg)
37
+ end
38
+ end
39
+
40
+ class UnexpectedTypeError < StandardError
41
+ attr_reader :received
42
+ def initialize(payload)
43
+ @received = payload[:received]
44
+ msg = "Type #{received} is not valid"
45
+ super(msg)
46
+ end
47
+ end
48
+
49
+ class TypeMatchError < StandardError
50
+ attr_reader :expected, :received
51
+ def initialize(payload)
52
+ @expected = payload[:expected]
53
+ @received = payload[:received]
54
+ msg = "Received #{received}, expected type #{expected}"
55
+ super(msg)
56
+ end
57
+ end
58
+
59
+ class ReservedKeywordError < StandardError
60
+ attr_reader :received, :reserved
61
+ def initialize(payload)
62
+ @received = payload[:received]
63
+ @reserved = payload[:reserved]
64
+ msg = "Value #{received} is a reserved keyword"
65
+ super(msg)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,23 @@
1
+ class InputStore
2
+ attr_reader :inputs
3
+ def initialize
4
+ @inputs = []
5
+ end
6
+
7
+ def store(name, value)
8
+ @inputs << {
9
+ name: name,
10
+ value: value
11
+ }
12
+ end
13
+
14
+ def fetch(value)
15
+ inputs.inject(nil) do |val, current|
16
+ if current[:name] == value
17
+ current
18
+ else
19
+ val
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ module Clingon
2
+ class ParserConfiguration
3
+ attr_accessor :inputs, :delimiter, :strict
4
+ attr_reader :conf_file, :structure
5
+
6
+ def initialize
7
+ @delimiter = '-'
8
+ @strict = true
9
+ end
10
+
11
+ def conf_file=(file)
12
+ unless File.exist?(file)
13
+ msg = "Configuration file (#{file}) does not exist"
14
+ raise(Clingon::ConfigurationFileError, msg)
15
+ end
16
+ if File.directory?(file)
17
+ msg = "Configuration file (#{file}) is a directory"
18
+ raise(Clingon::ConfigurationFileError, msg)
19
+ end
20
+ if File.zero?(file)
21
+ msg = "Configuration file (#{file}) is empty"
22
+ raise(Clingon::ConfigurationFileError, msg)
23
+ end
24
+ begin
25
+ yaml_contents = YAML.load_file(file)
26
+ rescue Psych::SyntaxError => e
27
+ raise(Clingon::YAMLSyntaxError, e)
28
+ end
29
+ if yaml_contents.key?(:structure)
30
+ self.structure = yaml_contents[:structure]
31
+ else
32
+ msg = "Configuration file (#{file}) must contain :structure key"
33
+ raise(Clingon::ConfigurationFileError, msg)
34
+ end
35
+ self.strict = yaml_contents[:strict] if yaml_contents.key?(:strict)
36
+ self.delimiter = yaml_contents[:delimiter] if yaml_contents.key?(:delimiter)
37
+ end
38
+
39
+ def structure=(struct)
40
+ @structure = StructureChecker.check(struct)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,24 @@
1
+ module StructureChecker
2
+ def self.check(structure)
3
+ raise('Structure must be an array') unless structure.instance_of?(Array)
4
+ StructureChecker.verify_contents(structure)
5
+ structure
6
+ end
7
+
8
+ def self.verify_contents(structure)
9
+ structure.each do |el|
10
+ raise('Each element of structure must contain name key') unless el[:name]
11
+ if el[:type]
12
+ valid = [
13
+ 'int',
14
+ 'float',
15
+ 'num',
16
+ 'bool'
17
+ ]
18
+ unless valid.include?(el[:type])
19
+ raise("Valid types are: #{valid.join(', ')}")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ module Clingon
2
+ VERSION = '0.0.1'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clingon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Mario Lopez
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-06 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |-
14
+ Clingon is a parser for command line inputs. It can help you parse flags,
15
+ and options for your script, as well as convert user inputs to specific ruby
16
+ types. With clingon you can forget about dealing with user inputs, and focus
17
+ on adding functionality to your scripts.
18
+ email: lopezrobles.mario@gmail.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files:
22
+ - README.md
23
+ files:
24
+ - README.md
25
+ - lib/clingon.rb
26
+ - lib/clingon/checks/checks.rb
27
+ - lib/clingon/checks/type_regex.rb
28
+ - lib/clingon/errors.rb
29
+ - lib/clingon/helpers/input_store.rb
30
+ - lib/clingon/helpers/parser_configuration.rb
31
+ - lib/clingon/helpers/structure_checker.rb
32
+ - lib/clingon/version.rb
33
+ homepage: https://github.com/mardotio/clingon
34
+ licenses:
35
+ - MIT
36
+ metadata: {}
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 2.4.5.2
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Flexible command line parser.
57
+ test_files: []