This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
.DS_STORE
|
||||
*.swp
|
||||
*.sass-cache
|
||||
pkg/
|
||||
Gemfile.lock
|
||||
.bundle/
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
source 'https://rubygems.org'
|
||||
gemspec
|
||||
gem 'minitest', '>=5.8.0'
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2016 Mickael Riga
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
Key Value Parser
|
||||
================
|
||||
|
||||
The primary purpose of `KeyValueParser` is to have a
|
||||
simple class to be able to parse command line arguments
|
||||
which are all formated as `key=value`.
|
||||
|
||||
For example you could call a script like this:
|
||||
|
||||
```
|
||||
$ ruby my-script.rb port=3000 host=localhost user=bob
|
||||
```
|
||||
|
||||
Then in your script you would do:
|
||||
|
||||
```ruby
|
||||
# my-script.rb
|
||||
|
||||
require 'key_value_parser'
|
||||
|
||||
options = KeyValueParser.new.parse(ARGV)
|
||||
options[:port] == 3000
|
||||
options[:host] == 'localhost'
|
||||
options[:user] == 'bob'
|
||||
```
|
||||
|
||||
Since `KeyValueParser` is quite opinionated, it is absolutely
|
||||
not a replacement for `OptionParser` which aim is to be able to
|
||||
parse Unix style arguments.
|
||||
|
||||
Also `KeyValueParser` is not limited to command line arguments.
|
||||
It can potentially parse any array which is a list of Key/Value
|
||||
strings. For example you can parse a list of HTTP header lines.
|
||||
|
||||
The following sections are list of features in order to understand
|
||||
what `KeyValueParser` can and cannot do.
|
||||
|
||||
Default Values
|
||||
--------------
|
||||
|
||||
When you create the parser instance, you can pass a `Hash` with
|
||||
default values.
|
||||
|
||||
```ruby
|
||||
# $ ruby my-script.rb host=www.domain.com
|
||||
parser = KeyValueParser.new host: 'localhost', port: 3000
|
||||
options = parser.parse(ARGV)
|
||||
options[:port] == 3000
|
||||
options[:host] == 'www.domain.com'
|
||||
```
|
||||
|
||||
Typecasted Values
|
||||
-----------------
|
||||
|
||||
As you may have noticed in previous examples, values are
|
||||
typecasted. So far it does typecast integers, floats and booleans.
|
||||
Also if you don't give a value, the parser will interpret
|
||||
it as a flag and set it to `true`.
|
||||
|
||||
```ruby
|
||||
# $ ruby myscript.rb port=3000 broadcast=false running
|
||||
options = KeyValueParser.new.parse(ARGV)
|
||||
options[:port] == 3000
|
||||
options[:broadcast] == false
|
||||
options[:running] == true
|
||||
```
|
||||
|
||||
If you don't want to cast values, you can use the second argument of
|
||||
`KeyValueParse#parse` which is a list of options. Just set
|
||||
`typecast_values` to `false`.
|
||||
|
||||
```ruby
|
||||
parser = KeyValueParser.new({}, {typecast_values: false})
|
||||
```
|
||||
|
||||
Typecasting is still very basic and will most likely evolve,
|
||||
but it will move in this direction.
|
||||
|
||||
Normalized Keys
|
||||
---------------
|
||||
|
||||
Keys of the resulting `Hash` are normalized. Dash in the middle
|
||||
of words are replaced by underscores. Double dash in front of a
|
||||
word are also removed. It allows you to have unix style double dash
|
||||
arguments.
|
||||
|
||||
```ruby
|
||||
# $ ruby my-script.rb --user-name="bob mould"
|
||||
options = KeyValueParser.new.parse(ARGV)
|
||||
options[:user_name] == 'bob'
|
||||
```
|
||||
|
||||
Because of the way command line arguments are created for `ARGV`
|
||||
you can even surround an argument with quotes in order to have
|
||||
spaces in the argument's value.
|
||||
|
||||
There is nothing yet for unix style single dash arguments.
|
||||
I could remove the dash and treat it like a single letter key,
|
||||
but the true purpose of single letter arguments is to be an
|
||||
alternative to a longer argument name. If you have a simple idea
|
||||
to implement this without too much hassle, please send me a pull
|
||||
request.
|
||||
|
||||
If you don't want the keys to be normalized, there is an option
|
||||
for this.
|
||||
|
||||
```ruby
|
||||
# $ ruby my-script.rb --user-name=bob
|
||||
parser = KeyValueParser.new({}, {normalize_keys: false})
|
||||
options = parser.parse(ARGV)
|
||||
options['--user-name'] == 'bob'
|
||||
```
|
||||
|
||||
Array Arguments
|
||||
---------------
|
||||
|
||||
You can have array values if you chain more than one `=<value>`
|
||||
after the key. And the values are still typecasted unless you
|
||||
disable it.
|
||||
|
||||
```ruby
|
||||
# $ ruby my-script.rb heroes=batman=robin ids=1=2
|
||||
options = KeyValueParser.new.parse(ARGV)
|
||||
options[:heroes] == ['batman', 'robin']
|
||||
options[:ids] == [1, 2]
|
||||
```
|
||||
|
||||
You can also disable this option:
|
||||
|
||||
```ruby
|
||||
# $ ruby my-script.rb heroes=batman=robin
|
||||
parser = KeyValueParser.new({}, {array_values: false})
|
||||
options = parser.parse(ARGV)
|
||||
options[:heroes] == 'batman=robin'
|
||||
```
|
||||
|
||||
Please note that so far this is the only way to make an array.
|
||||
Setting multiple times the same value would just result
|
||||
in the key being set to the last value.
|
||||
|
||||
```ruby
|
||||
# $ ruby my-script.rb heroes=batman heroes=robin
|
||||
options = KeyValueParser.new.parse(ARGV)
|
||||
options[:heroes] == 'robin'
|
||||
```
|
||||
|
||||
Parsing More Than ARGV
|
||||
----------------------
|
||||
|
||||
By default, the separator allows you to have an equal sign `=`
|
||||
or a colon `:`. And the regexp allows spaces around the separator.
|
||||
The surrounded spaces are if you want to parse a list of keys/values
|
||||
from a file for example. Let say you have a file containing a
|
||||
key/value per line.
|
||||
|
||||
```
|
||||
user: Bob Mould
|
||||
number: 42
|
||||
```
|
||||
|
||||
Which looks like a subset of YAML.
|
||||
Then you can parse the file for settings.
|
||||
|
||||
```ruby
|
||||
parser = KeyValueParser.new
|
||||
settings = parser.parse File.readlines('settings.conf')
|
||||
```
|
||||
Everything works the same, typecasting, normalizing keys, defaults, arrays...
|
||||
You can also set another separator. Anything which works with
|
||||
`String#split` will do.
|
||||
|
||||
```ruby
|
||||
# settings.conf:
|
||||
# author | Kevin Smith
|
||||
# characters | Jay | Silent Bob
|
||||
|
||||
parser = KeyValueParser.new({stars: 5}, {separator: /\s*\|\s*/})
|
||||
settings = parser.parse File.readlines('settings.conf')
|
||||
settings[:author] == 'Kevin Smith'
|
||||
settings[:characters] == ['Jay', 'Silent Bob']
|
||||
settings[:stars] == 5
|
||||
```
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Gem::Specification.new do |s|
|
||||
|
||||
s.authors = ["Mickael Riga"]
|
||||
s.email = ["mig@mypeplum.com"]
|
||||
s.homepage = "https://github.com/mig-hub/key-value-parser"
|
||||
s.licenses = ['MIT']
|
||||
|
||||
s.name = 'key-value-parser'
|
||||
s.version = '0.0.2'
|
||||
s.summary = "Key Value Parser"
|
||||
s.description = "KeyValueParser parses arrays of string options like 'machine=coconut'. It is mainly done for parsing ARGV, but can be used for other things."
|
||||
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.files = `git ls-files`.split("\n").sort
|
||||
s.test_files = s.files.grep(/^test\//)
|
||||
s.require_paths = ['lib']
|
||||
|
||||
end
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
class KeyValueParser
|
||||
|
||||
def initialize defaults={}, options={}
|
||||
@defaults = defaults
|
||||
@options = {
|
||||
separator: /\s*[=:]\s*/,
|
||||
normalize_keys: true,
|
||||
typecast_values: true,
|
||||
array_values: true
|
||||
}.merge(options)
|
||||
end
|
||||
|
||||
def parse kvs
|
||||
@defaults.merge(
|
||||
Hash[
|
||||
kvs.map do |a|
|
||||
k, *v = a.split(@options[:separator], @options[:array_values] ? 0 : 2)
|
||||
if v.size==1
|
||||
v = v[0]
|
||||
elsif v.size==0
|
||||
v = nil
|
||||
end
|
||||
[
|
||||
@options[:normalize_keys] ? normalize_key(k) : k,
|
||||
@options[:typecast_values] ? typecast(v) : v
|
||||
]
|
||||
end
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_key k
|
||||
k.sub(/^--/,'').gsub(/[\s\-]+/, '_').to_sym
|
||||
end
|
||||
|
||||
def typecast v
|
||||
if v.is_a? Array
|
||||
return v.map{|item| typecast(item)}
|
||||
end
|
||||
if v=='true' or v.nil?
|
||||
v = true
|
||||
elsif v=='false'
|
||||
v = false
|
||||
elsif v=~/^-?\d*\.\d+$/
|
||||
v = v.to_f
|
||||
elsif v=~/^-?\d+$/
|
||||
v = v.to_i
|
||||
else
|
||||
v
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
require 'minitest/autorun'
|
||||
require 'key_value_parser'
|
||||
|
||||
class TestKeyValueParser < Minitest::Test
|
||||
|
||||
def setup
|
||||
@parser = KeyValueParser.new
|
||||
end
|
||||
|
||||
def test_parses_kvs
|
||||
kvs = ['user:mig', 'machine:coconut']
|
||||
expected = {user: 'mig', machine: 'coconut'}
|
||||
assert_equal expected, @parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_equal_works_as_separator
|
||||
kvs = ['user=mig', 'machine=coconut']
|
||||
expected = {user: 'mig', machine: 'coconut'}
|
||||
assert_equal expected, @parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_spaces_work_in_separator
|
||||
kvs = ['user= mig', 'machine = coconut']
|
||||
expected = {user: 'mig', machine: 'coconut'}
|
||||
assert_equal expected, @parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_booleans_are_typecasted
|
||||
kvs = ['dead:false', 'running:true']
|
||||
expected = {dead: false, running: true}
|
||||
assert_equal expected, @parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_integers_are_typecasted
|
||||
kvs = ['machine:1coconut', 'size:11', 'negative:-5']
|
||||
expected = {machine: '1coconut', size: 11, negative: -5}
|
||||
assert_equal expected, @parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_floats_are_typecasted
|
||||
kvs = [
|
||||
'machine:5.0coconut',
|
||||
'float1:99.99', 'float2:-5.0', 'float3:.4'
|
||||
]
|
||||
expected = {
|
||||
machine: '5.0coconut',
|
||||
float1: 99.99, float2: -5.0, float3: 0.4
|
||||
}
|
||||
assert_equal expected, @parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_nil_value_typecasted_to_true
|
||||
kvs = ['running']
|
||||
expected = {running: true}
|
||||
assert_equal expected, @parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_parser_can_have_defaults
|
||||
kvs = ['user:mig']
|
||||
expected = {user: 'mig', role: 'dev'}
|
||||
parser = KeyValueParser.new user: 'marty', role: 'dev'
|
||||
assert_equal expected, parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_separator_can_be_changed
|
||||
kvs = ['user|mig', 'machine|coconut']
|
||||
expected = {user: 'mig', machine: 'coconut'}
|
||||
wrong_parser = KeyValueParser.new
|
||||
refute_equal expected, wrong_parser.parse(kvs)
|
||||
parser = KeyValueParser.new({}, separator: '|')
|
||||
assert_equal expected, parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_keys_are_normalized
|
||||
kvs = ['user-name=mig', 'machine_name=coconut', 'why not=yes']
|
||||
expected = {user_name: 'mig', machine_name: 'coconut', why_not: 'yes'}
|
||||
assert_equal expected, @parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_unix_double_dash_keys_are_normalized
|
||||
kvs = ['--user-name=mig', '--running']
|
||||
expected = {user_name: 'mig', running: true}
|
||||
assert_equal expected, @parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_value_is_updated_if_key_is_referenced_multiple_times
|
||||
kvs = ['user:bad', 'user:mig']
|
||||
expected = {user: 'mig'}
|
||||
assert_equal expected, @parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_arrays
|
||||
kvs = ['users:mig:why', 'numbers:3:5', 'request:false:POST:500']
|
||||
expected = {
|
||||
users: ['mig', 'why'],
|
||||
numbers: [3,5],
|
||||
request: [false, 'POST', 500]
|
||||
}
|
||||
assert_equal expected, @parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_normalize_key_can_be_skipped
|
||||
kvs = ['--user-name=mig', '--running']
|
||||
expected = {'--user-name' => 'mig', '--running' => true}
|
||||
parser = KeyValueParser.new({}, {normalize_keys: false})
|
||||
assert_equal expected, parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_typecast_can_be_skipped
|
||||
kvs = ['user:mig', 'size:5', 'crazy:false:maybe', 'dev']
|
||||
expected = {user: 'mig', size: '5', crazy: ['false','maybe'], dev: nil}
|
||||
parser = KeyValueParser.new({}, {typecast_values: false})
|
||||
assert_equal expected, parser.parse(kvs)
|
||||
end
|
||||
|
||||
def test_arrays_can_be_avoided
|
||||
kvs = ['users:mig=why', 'size:5:6', 'running']
|
||||
expected = {users: 'mig=why', size: '5:6', running: true}
|
||||
parser = KeyValueParser.new({}, {array_values: false})
|
||||
assert_equal expected, parser.parse(kvs)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user