clin 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,115 @@
1
+ require 'clin'
2
+
3
+ # Command parser
4
+ class Clin::CommandParser
5
+ # Create the command parser
6
+ # @param command_cls [Class<Clin::Command>] Command that must be matched
7
+ # @param argv [Array<String>] List of CL arguments
8
+ # @param fallback_help [Boolean] If the parse should raise an HelpError or the real error.
9
+ def initialize(command_cls, argv = ARGV, fallback_help: true)
10
+ @command = command_cls
11
+ argv = Shellwords.split(argv) if argv.is_a? String
12
+ @argv = argv
13
+ @fallback_help = fallback_help
14
+ end
15
+
16
+ # Parse the command line.
17
+ def parse
18
+ argv = @argv.clone
19
+ error = nil
20
+ options = {}
21
+ begin
22
+ options.merge! parse_options(argv)
23
+ rescue Clin::OptionError => e
24
+ error = e
25
+ end
26
+ begin
27
+ options.merge! parse_arguments(argv)
28
+ rescue Clin::ArgumentError => e
29
+ raise e unless @fallback_help
30
+ error = e
31
+ end
32
+
33
+ return redispatch(options) if @command.redispatch?
34
+ obj = @command.new(options)
35
+ handle_error(error)
36
+ obj
37
+ end
38
+
39
+ # Parse the options in the argv.
40
+ # @return [Array] the list of argv that are not options(positional arguments)
41
+ def parse_options(argv)
42
+ out = {}
43
+ parser = @command.option_parser(out)
44
+ skipped = skipped_options
45
+ argv.reject! { |x| skipped.include?(x) }
46
+ begin
47
+ parser.parse!(argv)
48
+ rescue OptionParser::InvalidOption => e
49
+ raise Clin::OptionError, e.to_s
50
+ end
51
+ out[:skipped_options] = skipped if @command.skip_options?
52
+ out
53
+ end
54
+
55
+ # Get the options that have been skipped by options_first!
56
+ def skipped_options
57
+ return [] unless @command.skip_options?
58
+ argv = @argv.dup
59
+ skipped = []
60
+ parser = @command.option_parser
61
+ loop do
62
+ begin
63
+ parser.parse!(argv)
64
+ break
65
+ rescue OptionParser::InvalidOption => e
66
+ skipped << e.to_s.sub(/invalid option:\s+/, '')
67
+ next if argv.empty? || argv.first.start_with?('-')
68
+ skipped << argv.shift
69
+ end
70
+ end
71
+
72
+ skipped
73
+ end
74
+
75
+ # Parse the argument. The options must have been strip out first.
76
+ def parse_arguments(argv)
77
+ out = {}
78
+ @command.args.each do |arg|
79
+ value, argv = arg.parse(argv)
80
+ out[arg.name.to_sym] = value
81
+ end
82
+ out.delete_if { |_, v| v.nil? }
83
+ end
84
+
85
+ # Method called after the argument have been parsed and before creating the command
86
+ # @param params [Array<String>] Parsed params from the command line.
87
+ def redispatch(params)
88
+ commands = @command._redispatch_args.last
89
+ commands ||= @command.default_commands
90
+ dispatcher = Clin::CommandDispatcher.new(commands)
91
+ begin
92
+ dispatcher.parse(redispatch_arguments(params))
93
+ rescue Clin::HelpError
94
+ raise Clin::HelpError, @command.option_parser
95
+ end
96
+ end
97
+
98
+ # Compute the list of argument to pass to the CommandDispatcher
99
+ # @param params [Hash] Options and Arguments of the CL
100
+ def redispatch_arguments(params)
101
+ args, prefix = @command._redispatch_args
102
+ args = args.map { |x| params[x] }.flatten.compact
103
+ args = prefix.split + args unless prefix.nil?
104
+ args += params[:skipped_options] if @command.skip_options?
105
+ args
106
+ end
107
+
108
+ # Guard that check if there was an error and fail HelpError if there was
109
+ # @raise [Clin::HelpError]
110
+ def handle_error(error)
111
+ return unless error
112
+ fail Clin::HelpError, @command.option_parser if @fallback_help
113
+ fail error
114
+ end
115
+ end
@@ -17,7 +17,6 @@ class Clin::HelpOptions < Clin::GeneralOption
17
17
  @raise = raise
18
18
  end
19
19
 
20
-
21
20
  def execute(options)
22
21
  return unless @raise
23
22
  fail Clin::HelpError, options[:help] if options[:help]
data/lib/clin/errors.rb CHANGED
@@ -14,6 +14,9 @@ module Clin
14
14
 
15
15
  # Error when a fixed argument is not matched
16
16
  class FixedArgumentError < ArgumentError
17
+ # Create a new FixedArgumentError
18
+ # @param argument [String] Name of the fixed argument
19
+ # @param got [String] What argument was in place of the fixed argument
17
20
  def initialize(argument = '', got = '')
18
21
  super("Expecting '#{argument}' but got '#{got}'")
19
22
  end
@@ -21,8 +24,10 @@ module Clin
21
24
 
22
25
  # Error when a command is missing an argument
23
26
  class MissingArgumentError < ArgumentError
24
- def initialize(message = '')
25
- super("Missing argument #{message}")
27
+ # Create a new MissingArgumentError
28
+ # @param argument [String] Name of the missing argument
29
+ def initialize(argument = '')
30
+ super("Missing argument #{argument}")
26
31
  end
27
32
  end
28
33
 
@@ -1,15 +1,15 @@
1
1
  require 'clin'
2
2
 
3
+ # Parent class for reusable options across commands
3
4
  class Clin::GeneralOption < Clin::CommandOptionsMixin
4
-
5
- def initialize(config = {})
6
-
5
+ def initialize(_config = {})
7
6
  end
8
7
 
9
8
  # It get the params the general options needs and do whatever the option is suppose to do with it.
10
- # Method called in the initialize of the command. This allow general options to be extracted when parsing a command line
9
+ # Method called in the initialize of the command.
10
+ # This allow general options to be extracted when parsing a command line
11
11
  # as well as calling the command directly in the code
12
12
  # @param _params [Hash] Params got in the command
13
13
  def execute(_params)
14
14
  end
15
- end
15
+ end
data/lib/clin/option.rb CHANGED
@@ -1,24 +1,40 @@
1
1
  require 'clin'
2
2
 
3
3
  # Option container.
4
+ # Prefer the `.option`, `.flag_option`,... class methods than `.add_option Option.new(...)`
4
5
  class Clin::Option
5
- attr_accessor :name, :description, :optional_argument, :block
6
+ attr_accessor :name, :description, :optional_argument, :block, :type, :default
6
7
  attr_reader :short, :long, :argument
7
8
 
8
- def initialize(name, description, short: nil, long: nil, argument: nil, optional_argument: false, &block)
9
+ # Create a new option.
10
+ # @param name [String] Option name.
11
+ # @param description [String] Option Description.
12
+ # @param short [String|Boolean]
13
+ # @param long [String|Boolean]
14
+ # @param argument [String|Boolean]
15
+ # @param argument_optional [Boolean]
16
+ # @param type [Class]
17
+ # @param default [Class] If the option is not specified set the default value.
18
+ # If default is nil the key will not be added to the params
19
+ # @param block [Block]
20
+ def initialize(name, description, short: nil, long: nil,
21
+ argument: nil, argument_optional: false, type: nil, default: nil, &block)
9
22
  @name = name
10
23
  @description = description
11
24
  @short = short
12
25
  @long = long
13
- @optional_argument = optional_argument
26
+ @optional_argument = argument_optional
14
27
  @argument = argument
28
+ @type = type
15
29
  @block = block
30
+ @default = default
16
31
  end
17
32
 
18
33
  # Register the option to the Option Parser
19
34
  # @param opts [OptionParser]
20
35
  # @param out [Hash] Out options mapping
21
36
  def register(opts, out)
37
+ load_default(out)
22
38
  if @block.nil?
23
39
  opts.on(*option_parser_arguments) do |value|
24
40
  on(value, out)
@@ -30,14 +46,30 @@ class Clin::Option
30
46
  end
31
47
  end
32
48
 
49
+ # Default option short name.
50
+ # ```
51
+ # :verbose => '-v'
52
+ # :help => '-h'
53
+ # :Require => '-r'
54
+ # ```
33
55
  def default_short
34
56
  "-#{name[0].downcase}"
35
57
  end
36
58
 
59
+ # Default option long name.
60
+ # ```
61
+ # :verbose => '--verbose'
62
+ # :Require => '--require'
63
+ # :add_stuff => '--add-stuff'
64
+ # ```
37
65
  def default_long
38
- "--#{name.downcase}"
66
+ "--#{name.to_s.downcase.dasherize}"
39
67
  end
40
68
 
69
+ # Default argument
70
+ # ```
71
+ # :Require => 'REQUIRE'
72
+ # ```
41
73
  def default_argument
42
74
  name.to_s.upcase
43
75
  end
@@ -47,7 +79,7 @@ class Clin::Option
47
79
  # If @short is false it will return nil
48
80
  # @return [String]
49
81
  def short
50
- return nil if @short === false
82
+ return nil if @short.eql? false
51
83
  @short ||= default_short
52
84
  end
53
85
 
@@ -56,7 +88,7 @@ class Clin::Option
56
88
  # If @long is false it will return nil
57
89
  # @return [String]
58
90
  def long
59
- return nil if @long === false
91
+ return nil if @long.eql? false
60
92
  @long ||= default_long
61
93
  end
62
94
 
@@ -65,31 +97,52 @@ class Clin::Option
65
97
  # If @argument is false it will return nil
66
98
  # @return [String]
67
99
  def argument
68
- return nil if @argument === false
100
+ return nil if flag?
69
101
  @argument ||= default_argument
70
102
  end
71
103
 
72
104
  def option_parser_arguments
73
- args = [short, long_argument, description]
105
+ args = [short, long_argument, @type, description]
74
106
  args.compact
75
107
  end
76
108
 
109
+ # Function called by the OptionParser when the option is used
110
+ # If no block is given this is called otherwise it call the block
77
111
  def on(value, out)
78
112
  out[@name] = value
79
113
  end
80
114
 
115
+ # If the option is a flag option.
116
+ # i.e Doesn't accept argument.
117
+ def flag?
118
+ @argument.eql? false
119
+ end
120
+
121
+ # Init the output Hash with the default values. Must be called before parsing.
122
+ # @param out [Hash]
123
+ def load_default(out)
124
+ return if @default.nil?
125
+ begin
126
+ out[@name] = @default.clone
127
+ rescue
128
+ out[@name] = @default
129
+ end
130
+ end
131
+
81
132
  def ==(other)
82
133
  return false unless other.is_a? Clin::Option
83
- @name == other.name &&
84
- @description == other.description &&
85
- short == other.short &&
86
- long == other.long &&
87
- argument == other.argument &&
88
- @optional_argument == other.optional_argument &&
89
- @block == other.block
134
+ to_a == other.to_a
135
+ end
136
+
137
+ # Return array of the attributes
138
+ def to_a
139
+ [@name, @description, @type, short, long, argument, @optional_argument, @default, @block]
90
140
  end
91
141
 
92
- protected
142
+ # Get the long argument syntax.
143
+ # ```
144
+ # :require => '--require REQUIRE'
145
+ # ```
93
146
  def long_argument
94
147
  return nil unless long
95
148
  out = long
@@ -100,3 +153,4 @@ class Clin::Option
100
153
  out
101
154
  end
102
155
  end
156
+
@@ -0,0 +1,25 @@
1
+ require 'clin'
2
+ require 'clin/option'
3
+
4
+ class Clin::OptionList < Clin::Option
5
+
6
+ # @see Clin::Option#initialize
7
+ def initialize(*args)
8
+ super
9
+ if flag?
10
+ self.default = 0
11
+ else
12
+ self.default = []
13
+ end
14
+ end
15
+
16
+ def on(value, out)
17
+ if flag?
18
+ out[@name] ||= 0
19
+ out[@name] += 1
20
+ else
21
+ out[@name] ||= []
22
+ out[@name] << value
23
+ end
24
+ end
25
+ end
data/lib/clin/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # Clin version
2
2
  module Clin
3
- VERSION = '0.1.0'
3
+ VERSION = '0.2.0'
4
4
  end
data/lib/clin.rb CHANGED
@@ -5,25 +5,28 @@ require 'clin/version'
5
5
 
6
6
  # Clin Global module. All classes and clin modules should be inside this module
7
7
  module Clin
8
- def self.default_exe_name
9
- 'command'
10
- end
8
+ class << self
9
+ # Set the global exe name. `Clin.exe_name = 'git'`
10
+ attr_writer :exe_name
11
11
 
12
- # Global exe_name
13
- # If this is not override it will be 'command'
14
- def self.exe_name
15
- @exe_name ||= Clin.default_exe_name
16
- end
12
+ def default_exe_name
13
+ 'command'
14
+ end
17
15
 
18
- # Set the global exe name
19
- def self.exe_name=(value)
20
- @exe_name=value
16
+ # Global exe_name
17
+ # If this is not override it will be 'command'
18
+ def exe_name
19
+ @exe_name ||= Clin.default_exe_name
20
+ end
21
21
  end
22
22
  end
23
23
 
24
24
  require 'clin/command'
25
+ require 'clin/command_parser'
25
26
  require 'clin/command_options_mixin'
26
27
  require 'clin/general_option'
27
28
  require 'clin/command_dispatcher'
28
29
  require 'clin/common/help_options'
29
30
  require 'clin/errors'
31
+ require 'clin/option'
32
+ require 'clin/option_list'
@@ -0,0 +1,165 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Clin::CommandParser do
4
+ describe '#parse_options' do
5
+ before :all do
6
+ @command = Class.new(Clin::Command)
7
+ @command.add_option Clin::Option.new(:name, 'Set name')
8
+ @command.add_option Clin::Option.new(:verbose, 'Set verbose', argument: false)
9
+ @command.add_option Clin::Option.new(:echo, 'Set name', argument_optional: true)
10
+ end
11
+
12
+ subject { Clin::CommandParser.new(@command, []) }
13
+
14
+ it 'raise argument when option value is missing' do
15
+ expect { subject.parse_options(%w(--name)) }.to raise_error(OptionParser::MissingArgument)
16
+ end
17
+ it 'raise error when unknown option' do
18
+ expect { subject.parse_options(%w(--other)) }.to raise_error(Clin::OptionError)
19
+ end
20
+
21
+ it { expect(subject.parse_options(%w(--name MyName))).to eq(name: 'MyName') }
22
+ it { expect(subject.parse_options(%w(--name=MyName))).to eq(name: 'MyName') }
23
+ it { expect(subject.parse_options(%w(-nMyName))).to eq(name: 'MyName') }
24
+
25
+
26
+ it { expect(subject.parse_options(%w(-v))).to eq(verbose: true) }
27
+
28
+ it { expect(subject.parse_options(%w(--echo))).to eq(echo: nil) }
29
+ it { expect(subject.parse_options(%w(-e EchoThis))).to eq(echo: 'EchoThis') }
30
+ end
31
+
32
+
33
+ describe '#parse_arguments' do
34
+ before :all do
35
+ @command = Class.new(Clin::Command)
36
+ @command.arguments(%w(fix <var> [opt]))
37
+ end
38
+
39
+ subject { Clin::CommandParser.new(@command, []) }
40
+
41
+ it 'raise argument when fixed in different' do
42
+ expect { subject.parse_arguments(%w(other val opt)) }.to raise_error(Clin::CommandLineError)
43
+ end
44
+ it 'raise error when too few arguments' do
45
+ expect { subject.parse_arguments(['fix']) }.to raise_error(Clin::CommandLineError)
46
+ end
47
+ it 'raise error when too much argument' do
48
+ expect { subject.parse_arguments(%w(other val opt more)) }
49
+ .to raise_error(Clin::CommandLineError)
50
+ end
51
+
52
+ it 'map arguments' do
53
+ expect(subject.parse_arguments(%w(fix val opt))).to eq(fix: 'fix', var: 'val', opt: 'opt')
54
+ end
55
+
56
+ it 'opt argument is nil when not provided' do
57
+ expect(subject.parse_arguments(%w(fix val))).to eq(fix: 'fix', var: 'val')
58
+ end
59
+ end
60
+
61
+ describe '#skipped_options' do
62
+ def skipped_options(argv)
63
+ Clin::CommandParser.new(@command, argv).skipped_options
64
+ end
65
+
66
+ before :all do
67
+ @command = Class.new(Clin::Command)
68
+ @command.skip_options true
69
+ end
70
+
71
+ context 'when all options should be skipped' do
72
+ it { expect(skipped_options(%w(pos arg))).to eq([]) }
73
+
74
+ it { expect(skipped_options(%w(pos arg --ignore -t))).to eq(%w(--ignore -t)) }
75
+
76
+ it { expect(skipped_options(%w(pos arg --ignore value -t))).to eq(%w(--ignore value -t)) }
77
+
78
+ end
79
+ context 'when option are define they should not be skipped' do
80
+ before :all do
81
+ @command.flag_option :verbose, 'Verbose'
82
+ end
83
+
84
+ it { expect(skipped_options(%w(pos arg --ignore value -t -v))).to eq(%w(--ignore value -t)) }
85
+
86
+ it do
87
+ expect(skipped_options(%w(pos arg --verbose --ignore value -t)))
88
+ .to eq(%w(--ignore value -t))
89
+ end
90
+
91
+ it do
92
+ expect(skipped_options(%w(pos arg --ignore value --verbose -t)))
93
+ .to eq(%w(--ignore value -t))
94
+ end
95
+ end
96
+ end
97
+
98
+ describe '.handle_dispatch' do
99
+ let(:args) { [Faker::Lorem.word, Faker::Lorem.word] }
100
+ before :all do
101
+ @command = Class.new(Clin::Command)
102
+ @command.arguments(%w(remote <args>...))
103
+ end
104
+
105
+ before do
106
+ allow_any_instance_of(Clin::CommandDispatcher).to receive(:parse)
107
+ end
108
+
109
+ subject { Clin::CommandParser.new(@command, []) }
110
+
111
+ context 'when only dispatching arguments' do
112
+ before do
113
+ @command.dispatch :args
114
+ end
115
+ it 'call the command dispatcher with the right arguments' do
116
+ expect_any_instance_of(Clin::CommandDispatcher).to receive(:parse).once.with(args)
117
+ subject.redispatch(remote: 'remote', args: args)
118
+ end
119
+ end
120
+
121
+ context 'when using prefix' do
122
+ let(:prefix) { 'remote' }
123
+ before do
124
+ @command.dispatch :args, prefix: prefix
125
+ end
126
+ it 'call the command dispatcher with the right arguments' do
127
+ expect_any_instance_of(Clin::CommandDispatcher).to receive(:parse).once.with([prefix] + args)
128
+ subject.redispatch(remote: 'remote', args: args)
129
+ end
130
+ end
131
+
132
+ context 'when using commands' do
133
+ let(:cmd1) { double(:command) }
134
+ let(:cmd2) { double(:command) }
135
+ before do
136
+ @command.dispatch :args, commands: [cmd1, cmd2]
137
+ allow_any_instance_of(Clin::CommandDispatcher).to receive(:initialize)
138
+ end
139
+ it 'call the command dispatcher with the right arguments' do
140
+ expect_any_instance_of(Clin::CommandDispatcher).to receive(:initialize).once.with([cmd1, cmd2])
141
+ subject.redispatch(remote: 'remote', args: args)
142
+ end
143
+ end
144
+
145
+ context 'when dispatcher raise HelpError' do
146
+ let(:new_message) { Faker::Lorem.sentence }
147
+ before do
148
+ @command.dispatch :args
149
+ allow_any_instance_of(Clin::CommandDispatcher).to receive(:initialize)
150
+ allow_any_instance_of(Clin::CommandDispatcher).to receive(:parse) do
151
+ fail Clin::HelpError, 'Dispatcher error'
152
+ end
153
+ allow(@command).to receive(:option_parser).and_return(new_message)
154
+ end
155
+ it do
156
+ expect { subject.redispatch(remote: 'remote', args: args) }
157
+ .to raise_error(Clin::HelpError)
158
+ end
159
+ it do
160
+ expect { subject.redispatch(remote: 'remote', args: args) }
161
+ .to raise_error(new_message)
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+ require 'clin/command'
3
+
4
+ RSpec.describe Clin::Command do
5
+ describe '#arguments=' do
6
+ subject { Class.new(Clin::Command) }
7
+ let(:args) { %w(fix <var> [opt]) }
8
+ before do
9
+ allow(Clin::Argument).to receive(:new)
10
+ end
11
+ context 'when using string to set arguments' do
12
+ before do
13
+ subject.arguments (args.join(' '))
14
+ end
15
+ it { expect(subject.args.size).to eq(args.size) }
16
+ it { expect(Clin::Argument).to have_received(:new).exactly(args.size).times }
17
+ end
18
+
19
+ context 'when using array to set arguments' do
20
+ before do
21
+ subject.arguments (args)
22
+ end
23
+ it { expect(subject.args.size).to eq(args.size) }
24
+ it { expect(Clin::Argument).to have_received(:new).exactly(args.size).times }
25
+ end
26
+
27
+ context 'when using array that contains multiple arguments to set arguments' do
28
+ before do
29
+ subject.arguments ([args[0], args[1..-1]])
30
+ end
31
+ it { expect(subject.args.size).to eq(args.size) }
32
+ it { expect(Clin::Argument).to have_received(:new).exactly(args.size).times }
33
+ end
34
+
35
+ end
36
+
37
+ describe '#banner' do
38
+ subject { Class.new(Clin::Command) }
39
+ context 'when exe is defined' do
40
+ let(:exe) { Faker::Lorem.word }
41
+ before do
42
+ subject.exe_name(exe)
43
+ end
44
+
45
+ it { expect(subject.banner).to eq("Usage: #{exe} [Options]") }
46
+ end
47
+
48
+ context 'when exe is not defined' do
49
+ it { expect(subject.banner).to eq('Usage: command [Options]') }
50
+ end
51
+
52
+ context 'when arguments are defined' do
53
+ let(:arguments) { '<some> [Value]' }
54
+ before do
55
+ subject.arguments(arguments)
56
+ end
57
+
58
+ it { expect(subject.banner).to eq("Usage: #{Clin.default_exe_name} #{arguments} [Options]") }
59
+ end
60
+ end
61
+
62
+
63
+
64
+ describe '.dispatch_doc' do
65
+ subject { Class.new(Clin::Command) }
66
+ before do
67
+ subject.arguments(%w(remote <args>...))
68
+ end
69
+
70
+ let(:cmd1) { double(:command, usage: 'cmd1') }
71
+ let(:cmd2) { double(:command, usage: 'cmd2') }
72
+ let(:cmd3) { double(:command, usage: 'cmd3') }
73
+ let(:cmds) { [cmd1, cmd2, cmd3] }
74
+ let(:opts) { double(:option_parser, separator: true) }
75
+ before do
76
+ subject.dispatch :args, commands: cmds
77
+ allow_any_instance_of(Clin::CommandDispatcher).to receive(:initialize)
78
+ subject.dispatch_doc(opts)
79
+ end
80
+ it { expect(opts).to have_received(:separator).at_least(cmds.size).times }
81
+ end
82
+
83
+ describe '.subcommands' do
84
+ before do
85
+ @cmd1 = Class.new(Clin::Command)
86
+ @cmd2 = Class.new(Clin::Command)
87
+ @abstract_cmd = Class.new(Clin::Command) { abstract true }
88
+ end
89
+
90
+ it { expect(Clin::Command.subcommands).to include(@cmd1) }
91
+ it { expect(Clin::Command.subcommands).to include(@cmd2) }
92
+ it { expect(Clin::Command.subcommands).not_to include(@abstract_cmd) }
93
+ end
94
+
95
+ describe '.exe_name' do
96
+ context 'when not setting the exe_name' do
97
+ subject { Class.new(Clin::Command) }
98
+
99
+ it { expect(subject.exe_name).to eq(Clin.exe_name) }
100
+ end
101
+
102
+ context 'when setting the exe_name' do
103
+ let(:name) { Faker::Lorem.word }
104
+ subject { Class.new(Clin::Command) }
105
+ before do
106
+ subject.exe_name(name)
107
+ end
108
+ it { expect(subject.exe_name).to eq(name) }
109
+ end
110
+ end
111
+
112
+ end