clin 0.1.0 → 0.2.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,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