Skip to content

Commit

Permalink
Cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
timotheeguerin committed Jun 9, 2015
1 parent c8231cf commit 31cbad0
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 94 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,10 @@
## 0.4.0
Features:
- Command line parsing is now done internally, i.e. removed optparse (#6).

Bug fix:
- OptParse was hijacking the -v --version.

## 0.3.0
Features:
- Added a shell class for any user interaction.
Expand Down
1 change: 0 additions & 1 deletion benchmarks/bench.rb
Expand Up @@ -3,7 +3,6 @@
$LOAD_PATH.push File.expand_path('../../lib', __FILE__)
require 'clin'


Benchmark.ips do |x|
x.report('simple') do
require 'examples/simple'
Expand Down
16 changes: 0 additions & 16 deletions examples/test.rb
@@ -1,19 +1,3 @@
$LOAD_PATH.push File.expand_path('../../lib', __FILE__)
require 'clin'
require 'json'

def decode(str)
str.gsub(/([^\\](?:\\\\)*\\u[\da-f]{4})/i) do |m|
m[0...-6] + [m[-4..-1].to_i(16)].pack('U')
end
end

a = []
a << decode('1 This string does not have a unicode \\u escape.')
a << decode('2 This string does not have a unicode \u005Cu escape.')
a << decode('3 This string does not have a unicode \\\\u0075 escape.')
a << decode('4 This string does not have a unicode \\\\\\u0075 escape.')
a << JSON.parse('{"name": "This string does not have a unicode \\\\u0075 escape."}')
a << JSON.parse('{"name": "This string does not have a unicode \\\\\\u0075 escape."}')
a << JSON.parse(%({"value": #{'"some val \\a"'}}))['value']
puts a
1 change: 1 addition & 0 deletions lib/clin.rb
Expand Up @@ -30,6 +30,7 @@ def diff_cmd

require 'clin/command_mixin'
require 'clin/command'
require 'clin/option_parser'
require 'clin/command_parser'
require 'clin/general_option'
require 'clin/command_dispatcher'
Expand Down
8 changes: 4 additions & 4 deletions lib/clin/command_mixin/options.rb
Expand Up @@ -116,13 +116,13 @@ def execute_general_options(options)
end
end

def find(value)
find_by(name: value)
def find_option(value)
find_option_by(name: value)
end

def find_by(hash)
def find_option_by(hash)
key, value = hash.first
self.options.select { |x| x.send(key) == value }.first
options.find { |x| x.send(key) == value }
end

def option_help
Expand Down
79 changes: 8 additions & 71 deletions lib/clin/command_parser.rb
Expand Up @@ -2,6 +2,9 @@

# Command parser
class Clin::CommandParser
# List of errors that have occurred during the parsing
attr_reader :errors

# Create the command parser
# @param command_cls [Class<Clin::Command>] Command that must be matched
# @param argv [Array<String>] List of CL arguments
Expand Down Expand Up @@ -40,77 +43,15 @@ def parse
obj
end

LONG_OPTION_REGEX = /\A(?<name>--[^=]*)(?:=(?<value>.*))?/m
SHORT_OPTION_REGEX = /\A(?<name>-.)(?<value>(=).*|.+)?/m

def parse_options(argv)
arguments = []
while (arg = argv.shift)
case arg
when LONG_OPTION_REGEX
name = Regexp.last_match[:name]
value = Regexp.last_match[:value]
option = @command.find_by(long: name)
parse_option(option, name, value, argv, false)
when SHORT_OPTION_REGEX
name = Regexp.last_match[:name]
value = Regexp.last_match[:value]
option = @command.find_by(short: name)
parse_option(option, name, value, argv, true)
else
arguments << arg
end
end
argv.replace(arguments)
parser = Clin::OptionParser.new(@command, argv)
@options.merge! parser.parse
@skipped_options = parser.skipped_options
@errors += parser.errors
argv.replace(parser.arguments)
@options
end

def parse_option(option, name, value, argv, short)
if option.nil?
handle_unknown_option(name, value, argv)
return
end
if option.flag?
if value.nil?
option.trigger(self, @options, true)
elsif not short
add_error Clin::OptionUnexpectedArgumentError.new(option, value)
else
option.trigger(self, @options, true)
# -abc multiple flag options
value.each_char do |s|
option = @command.find_by(short: "-#{s}")
if option && !option.flag?
message = "Cannot combine short options that expect argument: #{option}"
add_error Clin::OptionError.new(message, option)
return
end
parse_option(option, "-#{s}", nil, [], true)
end
end
return
end
if value.nil? && argv.any? && !argv.first.start_with?('-')
value = argv.shift
end
if value.nil? && !option.argument_optional?
add_error Clin::MissingOptionArgumentError.new(option)
end
value ||= true
option.trigger(self, @options, value)
end

def handle_unknown_option(name, value, argv)
unless @command.skip_options?
add_error Clin::UnknownOptionError.new(name)
return
end
if value.nil? && argv.any? && !argv.first.start_with?('-')
value = argv.shift
end
@skipped_options += [name, value]
end

def add_error(err)
@errors << err
end
Expand Down Expand Up @@ -151,10 +92,6 @@ def redispatch_arguments(params)
args
end

def errors
@errors
end

def valid?
@errors.empty?
end
Expand Down
3 changes: 3 additions & 0 deletions lib/clin/errors.rb
Expand Up @@ -55,13 +55,15 @@ def initialize(message, option)
end
end

# Error when undefined options are found in argv
class UnknownOptionError < OptionError
def initialize(option)
message = "Unknown option #{option}"
super(message, option)
end
end

# Error when a flag option has an unexpected argument
class OptionUnexpectedArgumentError < OptionError
def initialize(option, value)
@value = value
Expand All @@ -70,6 +72,7 @@ def initialize(option, value)
end
end

# When a option is missing it's required argument
class MissingOptionArgumentError < OptionError
def initialize(option)
message = "Missing argument for option #{option}"
Expand Down
1 change: 0 additions & 1 deletion lib/clin/option.rb
Expand Up @@ -62,7 +62,6 @@ def trigger(opts, out, value)
end
end


# Default option short name.
# ```
# :verbose => '-v'
Expand Down
125 changes: 125 additions & 0 deletions lib/clin/option_parser.rb
@@ -0,0 +1,125 @@
require 'clin'

# Class that handler the option parsing part of command parsing.
# It separate the options from the arguments
class Clin::OptionParser
LONG_OPTION_REGEX = /\A(?<name>--[^=]*)(?:=(?<value>.*))?/m
SHORT_OPTION_REGEX = /\A(?<name>-.)(?<value>(=).*|.+)?/m

attr_reader :arguments
attr_reader :errors
attr_reader :options
attr_reader :skipped_options

def initialize(command, argv)
@errors = []
@command = command
@options = {}
@original_argv = argv
@argv = argv.clone
@arguments = []
@skipped_options = []
end

def parse
while parse_next
end
@options
end

# Extract the option
def parse_next
return false if @argv.empty?
case (arg = @argv.shift)
when LONG_OPTION_REGEX
name = Regexp.last_match[:name]
value = Regexp.last_match[:value]
parse_long(name, value)
when SHORT_OPTION_REGEX
name = Regexp.last_match[:name]
value = Regexp.last_match[:value]
parse_short(name, value)
else
@arguments << arg
end
true
end

# Parse a long option
# @param name [String] name of the option(--verbose)
# @param value [String] value of the option
# If the value is nil and the option allow argument it will try to use the next argument
def parse_long(name, value)
option = @command.find_option_by(long: name)
parse_option(option, name, value, false)
end

# Parse a long option
# @param name [String] name of the option(-v)
# @param value [String] value of the option
# If the value is nil and the option allow argument it will try to use the next argument
def parse_short(name, value)
option = @command.find_option_by(short: name)
parse_option(option, name, value, true)
end

def parse_option(option, name, value, short)
return handle_unknown_option(name, value) if option.nil?
return parse_flag_option(option, value, short) if option.flag?

value = complete(value)
if value.nil? && !option.argument_optional?
add_error Clin::MissingOptionArgumentError.new(option)
end
value ||= true
option.trigger(self, @options, value)
end

# Get the next possible argument in the list if the value is nil.
# @param value [String] current option value.
# Only get the next argument in the list if:
# - value is nil
# - the next argument is not an option(start with '-')
def complete(value)
if value.nil? && @argv.any? && !@argv.first.start_with?('-')
@argv.shift
else
value
end
end

def parse_flag_option(option, value, short)
return option.trigger(self, @options, true) if value.nil?
unless short # Short can also have the format -abc
add_error Clin::OptionUnexpectedArgumentError.new(option, value)
return
end

option.trigger(self, @options, true)
# The value is expected to be other flag options
value.each_char do |s|
option = @command.find_option_by(short: "-#{s}")
if option && !option.flag?
message = "Cannot combine short options that expect argument: #{option}"
add_error Clin::OptionError.new(message, option)
break
end
parse_flag_option(option, nil, true)
end
end

def handle_unknown_option(name, value)
unless @command.skip_options?
add_error Clin::UnknownOptionError.new(name)
return
end
if value.nil? && @argv.any? && !@argv.first.start_with?('-')
value = @argv.shift
end
@skipped_options += [name, value]
end

def add_error(err)
@errors << err
end
end
4 changes: 3 additions & 1 deletion spec/examples/list_option_spec.rb
Expand Up @@ -6,7 +6,9 @@
it { expect(ListCommand.parse('').params).to eq(echo: [], line: 0) }
it { expect(ListCommand.parse('--echo msg').params).to eq(echo: ['msg'], line: 0) }
it { expect(ListCommand.parse('--line').params).to eq(echo: [], line: 1) }
it { expect(ListCommand.parse('--line --line').params).to eq(echo: [], line: 2) }
it {
expect(ListCommand.parse('--line --line').params).to eq(echo: [], line: 2)
}
it {
expect(ListCommand.parse('-lll').params).to eq(echo: [], line: 3)
}
Expand Down

0 comments on commit 31cbad0

Please sign in to comment.