clin 0.2.0 → 0.3.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,134 @@
1
+ require 'clin'
2
+ require 'clin/option'
3
+
4
+ # Contains class methods to add option to a command or a General option
5
+ module Clin::CommandMixin::Options
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ self.options = []
10
+ self.general_options = {}
11
+ # Trigger when a class inherit this class
12
+ # It will clone attributes that need inheritance
13
+ # @param subclass [Clin::Command]
14
+ def self.inherited(subclass)
15
+ subclass.options = @options.clone
16
+ subclass.general_options = @general_options.clone
17
+ super
18
+ end
19
+ end
20
+
21
+ module ClassMethods # :nodoc:
22
+ attr_accessor :options
23
+ attr_accessor :general_options
24
+
25
+ # Add an option
26
+ # @param args list of arguments.
27
+ # * First argument must be the name if no block is given.
28
+ # It will set automatically read the value into the hash with +name+ as key
29
+ # * The remaining arguments are OptionsParser#on arguments
30
+ # ```
31
+ # option :require, '-r', '--require [LIBRARY]', 'Require the library'
32
+ # option '-h', '--helper', 'Show the help' do
33
+ # puts opts
34
+ # exit
35
+ # end
36
+ # ```
37
+ def opt_option(*args, &block)
38
+ add_option Clin::Option.new(*args, &block)
39
+ end
40
+
41
+ # Add an option.
42
+ # Helper method that just create a new Clin::Option with the argument then call add_option
43
+ # ```
44
+ # option :show, 'Show some message'
45
+ # # => -s --show SHOW Show some message
46
+ # option :require, 'Require a library', short: false, optional: true, argument: 'LIBRARY'
47
+ # # => --require [LIBRARY] Require a library
48
+ # option :help, 'Show the help', argument: false do
49
+ # puts opts
50
+ # exit
51
+ # end
52
+ # # => -h --help Show the help
53
+ # ```
54
+ def option(name, description, **config, &block)
55
+ add_option Clin::Option.new(name, description, **config, &block)
56
+ end
57
+
58
+ # For an option that does not have an argument
59
+ # Same as .option except it will default argument to false
60
+ # ```
61
+ # option :verbose, 'Use verbose' #=> -v --verbose will be added to the option of this command
62
+ # ```
63
+ def flag_option(name, description, **config, &block)
64
+ add_option Clin::Option.new(name, description, **config.merge(argument: false), &block)
65
+ end
66
+
67
+ # Add a list option.
68
+ # @see Clin::OptionList#initialize
69
+ def list_option(name, description, **config)
70
+ add_option Clin::OptionList.new(name, description, **config)
71
+ end
72
+
73
+ # Add a list options that don't take arguments
74
+ # Same as .list_option but set +argument+ to false
75
+ # @see Clin::OptionList#initialize
76
+ def list_flag_option(name, description, **config)
77
+ add_option Clin::OptionList.new(name, description, **config.merge(argument: false))
78
+ end
79
+
80
+ def auto_option(name, usage, &block)
81
+ add_option Clin::Option.parse(name, usage, &block)
82
+ end
83
+
84
+ # Add a new option.
85
+ # @param option [Clin::Option] option to add.
86
+ def add_option(option)
87
+ # Need to use += instead of << otherwise the parent class will also be changed
88
+ @options << option
89
+ end
90
+
91
+ # Add a general option
92
+ # @param option_cls [Class<GeneralOption>] Class inherited from GeneralOption
93
+ # @param config [Hash] General option config. Check the general option config.
94
+ def general_option(option_cls, config = {})
95
+ option_cls = option_cls.constantize if option_cls.is_a? String
96
+ @general_options[option_cls] = option_cls.new(config)
97
+ end
98
+
99
+ # Remove a general option
100
+ # Might be useful if a parent added the option but is not needed in this child.
101
+ def remove_general_option(option_cls)
102
+ option_cls = option_cls.constantize if option_cls.is_a? String
103
+ @general_options.delete(option_cls)
104
+ end
105
+
106
+ # To be called inside OptionParser block
107
+ # Extract the option in the command line using the OptionParser and map it to the out map.
108
+ # @param opts [OptionParser]
109
+ # @param out [Hash] Where the options shall be extracted
110
+ def register_options(opts, out)
111
+ @options.each do |option|
112
+ option.register(opts, out)
113
+ end
114
+
115
+ @general_options.each do |_cls, option|
116
+ option.class.register_options(opts, out)
117
+ end
118
+ end
119
+
120
+ # Call #execute on each of the general options.
121
+ # This is called during the command initialization
122
+ # e.g. A verbose general option execute would be:
123
+ # ```
124
+ # def execute(params)
125
+ # MyApp.verbose = true if params[:verbose]
126
+ # end
127
+ # ```
128
+ def execute_general_options(options)
129
+ general_options.each do |_cls, gopts|
130
+ gopts.execute(options)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,9 @@
1
+ require 'clin'
2
+
3
+ # Namespace module for all the command mixins
4
+ module Clin::CommandMixin
5
+ end
6
+
7
+ require 'clin/command_mixin/core'
8
+ require 'clin/command_mixin/dispatcher'
9
+ require 'clin/command_mixin/options'
@@ -1,7 +1,9 @@
1
1
  require 'clin'
2
2
 
3
3
  # Parent class for reusable options across commands
4
- class Clin::GeneralOption < Clin::CommandOptionsMixin
4
+ class Clin::GeneralOption
5
+ include Clin::CommandMixin::Options
6
+
5
7
  def initialize(_config = {})
6
8
  end
7
9
 
data/lib/clin/option.rb CHANGED
@@ -3,8 +3,31 @@ require 'clin'
3
3
  # Option container.
4
4
  # Prefer the `.option`, `.flag_option`,... class methods than `.add_option Option.new(...)`
5
5
  class Clin::Option
6
+ def self.parse(name, usage, &block)
7
+ long = nil
8
+ short = nil
9
+ argument = nil
10
+ desc = []
11
+ usage.split.each do |segment|
12
+ if segment.start_with? '--'
13
+ long, argument = segment.split('=', 2)
14
+ elsif segment.start_with? '-'
15
+ short = segment
16
+ else
17
+ desc << segment
18
+ end
19
+ end
20
+ argument = false if argument.nil?
21
+ new(name, desc.join(' '), short: short, long: long, argument: argument, &block)
22
+ end
23
+
6
24
  attr_accessor :name, :description, :optional_argument, :block, :type, :default
7
- attr_reader :short, :long, :argument
25
+
26
+ # Set the short name(e.g. -v for verbose)
27
+ attr_writer :short
28
+
29
+ # Set the long name(e.g. --verbose for verbose)
30
+ attr_writer :long
8
31
 
9
32
  # Create a new option.
10
33
  # @param name [String] Option name.
@@ -24,7 +47,7 @@ class Clin::Option
24
47
  @short = short
25
48
  @long = long
26
49
  @optional_argument = argument_optional
27
- @argument = argument
50
+ self.argument = argument
28
51
  @type = type
29
52
  @block = block
30
53
  @default = default
@@ -101,6 +124,18 @@ class Clin::Option
101
124
  @argument ||= default_argument
102
125
  end
103
126
 
127
+ # Set the argument
128
+ # @param value
129
+ def argument=(value)
130
+ if value
131
+ arg = Clin::Argument.new(value)
132
+ @optional_argument = true if arg.optional
133
+ @argument = arg.name
134
+ else # If false or nil
135
+ @argument = value
136
+ end
137
+ end
138
+
104
139
  def option_parser_arguments
105
140
  args = [short, long_argument, @type, description]
106
141
  args.compact
@@ -153,4 +188,3 @@ class Clin::Option
153
188
  out
154
189
  end
155
190
  end
156
-
@@ -1,8 +1,8 @@
1
1
  require 'clin'
2
2
  require 'clin/option'
3
3
 
4
+ # Option that can be used multiple time in the command line
4
5
  class Clin::OptionList < Clin::Option
5
-
6
6
  # @see Clin::Option#initialize
7
7
  def initialize(*args)
8
8
  super
data/lib/clin/shell.rb ADDED
@@ -0,0 +1,111 @@
1
+ require 'clin'
2
+
3
+ # Class the offer helper method to interact with the user using the command line
4
+ class Clin::Shell
5
+ # Input stream, default: STDIN
6
+ attr_accessor :in
7
+
8
+ # Output stream, default: STDOUT
9
+ attr_accessor :out
10
+
11
+ def initialize(input: STDIN, output: STDOUT)
12
+ @in = input
13
+ @out = output
14
+ @yes_or_no_persist = false
15
+ @override_persist = false
16
+ end
17
+
18
+ # Ask a question
19
+ # @param statement [String]
20
+ # @param default [String]
21
+ # @param autocomplete [Array|Proc] Filter for autocomplete
22
+ def ask(statement, default: nil, autocomplete: nil)
23
+ answer = scan(statement, autocomplete: autocomplete)
24
+ if answer.blank?
25
+ default
26
+ else
27
+ answer.strip
28
+ end
29
+ end
30
+
31
+ # Ask a question and expect the result to be in the list of choices
32
+ # Will continue asking until the input is correct
33
+ # or if a default value is supplied then empty will return.
34
+ # @param statement [String] Question to ask
35
+ # @param choices [Array] List of choices
36
+ # @param default [String] Default value if the user put blank value.
37
+ # @param allow_initials [Boolean] Allow the user to reply with only the initial of the choice.
38
+ # (e.g. yes/no => y/n)
39
+ # If multiple choices start with the same initial
40
+ # ONLY the first one will be able to be selected using its initial
41
+ def choose(statement, choices, default: nil, allow_initials: false)
42
+ Clin::ShellInteraction::Choose.new(self).run(statement, choices,
43
+ default: default, allow_initials: allow_initials)
44
+ end
45
+
46
+ # Expect the user the return yes or no(y/n also works)
47
+ # @param statement [String] Question to ask
48
+ # @param default [String] Default value(yes/no)
49
+ # @param persist [Boolean] Add "always" to the choices. When all is selected all the following
50
+ # call to yes_or_no with persist: true will return true instead of asking the user.
51
+ def yes_or_no(statement, default: nil, persist: false)
52
+ Clin::ShellInteraction::YesOrNo.new(self).run(statement, default: default, persist: persist)
53
+ end
54
+
55
+ # Yes or no question defaulted to yes
56
+ # @param options [Hash] Named parameters for yes_or_no
57
+ # @see #yes_or_no
58
+ def yes?(statement, options = {})
59
+ options[:default] = :yes
60
+ yes_or_no(statement, **options)
61
+ end
62
+
63
+ # Yes or no question defaulted to no
64
+ # @param options [Hash] Named parameters for yes_or_no
65
+ # @see #yes_or_no
66
+ def no?(statement, options = {})
67
+ options[:default] = :no
68
+ yes_or_no(statement, **options)
69
+ end
70
+
71
+ # File conflict helper method.
72
+ # Give the following options to the user
73
+ # - yes, Yes for this one
74
+ # - no, No for this one
75
+ # - all, Yes for all one
76
+ # - quit, Quit the program
77
+ # - diff, Diff the 2 files
78
+ # @param filename [String] Filename with the conflict
79
+ # @param block [Block] optional block that give the new content in case of diff
80
+ # @return [Boolean] If the file should be overwritten.
81
+ def file_conflict(filename, default: nil, &block)
82
+ Clin::ShellInteraction::FileConflict.new(self).run(filename, default: default, &block)
83
+ end
84
+
85
+ # File conflict question defaulted to yes
86
+ def overwrite?(filename, &block)
87
+ file_conflict(filename, default: :yes, &block)
88
+ end
89
+
90
+ # File conflict question defaulted to no
91
+ def keep?(filename, &block)
92
+ file_conflict(filename, default: :no, &block)
93
+ end
94
+
95
+ # Prompt the statement to the user and return his reply.
96
+ # @param statement [String]
97
+ # @param autocomplete [Array|Block]
98
+ protected def scan(statement, autocomplete: nil)
99
+ unless autocomplete.nil?
100
+ Readline.completion_proc = if autocomplete.is_a? Proc
101
+ autocomplete
102
+ else
103
+ proc { |s| autocomplete.grep(/^#{Regexp.escape(s)}/) }
104
+ end
105
+ end
106
+ Readline.completion_append_character = nil
107
+ Readline.readline(statement + ' ', true)
108
+ end
109
+ end
110
+
111
+ require 'clin/shell_interaction'
@@ -0,0 +1,59 @@
1
+ require 'clin'
2
+
3
+ # Handle a choose question
4
+ class Clin::ShellInteraction::Choose < Clin::ShellInteraction
5
+ def run(statement, choices, default: nil, allow_initials: false)
6
+ choices = convert_choices(choices)
7
+ question = prepare_question(statement, choices, default: default, initials: allow_initials)
8
+ loop do
9
+ answer = @shell.ask(question, default: default, autocomplete: choices.keys)
10
+ unless answer.nil?
11
+ choices.each do |choice, _|
12
+ if choice.casecmp(answer) == 0 || (allow_initials && choice[0].casecmp(answer[0]) == 0)
13
+ return choice
14
+ end
15
+ end
16
+ end
17
+ print_choices_help(choices, allow_initials: allow_initials)
18
+ end
19
+ end
20
+
21
+ protected def choice_message(choices, default: nil, initials: false)
22
+ choices = choices.keys.map { |x| x == default ? x.to_s.upcase : x }
23
+ msg = if initials
24
+ choices.map { |x| x[0] }.join('')
25
+ else
26
+ choices.join(',')
27
+ end
28
+ "[#{msg}]"
29
+ end
30
+
31
+ protected def prepare_question(statement, choices, default: nil, initials: false)
32
+ "#{statement} #{choice_message(choices, default: default, initials: initials)}"
33
+ end
34
+
35
+ # Convert the choices to a hash with key being the choice and value the description
36
+ protected def convert_choices(choices)
37
+ if choices.is_a? Array
38
+ Hash[*choices.map { |k| [k, ''] }.flatten]
39
+ elsif choices.is_a? Hash
40
+ choices
41
+ end
42
+ end
43
+
44
+ protected def print_choices_help(choices, allow_initials: false)
45
+ puts 'Choose from:'
46
+ used_initials = Set.new
47
+ choices.each do |choice, description|
48
+ suf = choice.to_s
49
+ suf += ", #{description}" unless description.blank?
50
+ line = if allow_initials && !used_initials.include?(choice[0])
51
+ used_initials << choice[0]
52
+ " #{choice[0]} - #{suf}"
53
+ else
54
+ " #{suf}"
55
+ end
56
+ puts line
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,53 @@
1
+ require 'clin'
2
+
3
+ # Handle the file_conflict interaction with the user.
4
+ class Clin::ShellInteraction::FileConflict < Clin::ShellInteraction
5
+ def run(filename, default: nil, &block)
6
+ choices = file_conflict_choices
7
+ choices = choices.except(:diff) unless block_given?
8
+ return true if persist?
9
+ result = nil
10
+ while result.nil?
11
+ choice = @shell.choose("Overwrite '#{filename}'?", choices,
12
+ default: default, allow_initials: true)
13
+ result = handle_choice(choice, filename, &block)
14
+ end
15
+ result
16
+ end
17
+
18
+ protected def handle_choice(choice, filename, &block)
19
+ case choice
20
+ when :yes
21
+ return true
22
+ when :no
23
+ return false
24
+ when :always
25
+ return persist!
26
+ when :quit
27
+ puts 'Aborting...'
28
+ fail SystemExit
29
+ when :diff
30
+ show_diff(filename, block.call)
31
+ return nil
32
+ else
33
+ return nil
34
+ end
35
+ end
36
+
37
+ protected def file_conflict_choices
38
+ {yes: 'Overwrite',
39
+ no: 'Do not Overwrite',
40
+ always: 'Override this and all the next',
41
+ quit: 'Abort!',
42
+ diff: 'Show the difference',
43
+ help: 'Show this'}
44
+ end
45
+
46
+ protected def show_diff(old_file, new_content)
47
+ Tempfile.open(File.basename(old_file)) do |f|
48
+ f.write new_content
49
+ f.rewind
50
+ system %(#{Clin.diff_cmd} "#{old_file}" "#{f.path}")
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,16 @@
1
+ require 'clin'
2
+
3
+ # Handle a simple yes/no interaction
4
+ class Clin::ShellInteraction::YesOrNo < Clin::ShellInteraction
5
+ def run(statement, default: nil, persist: false)
6
+ default = default.to_sym unless default.nil?
7
+ options = [:yes, :no]
8
+ if persist
9
+ return true if persist?
10
+ options << :always
11
+ end
12
+ choice = @shell.choose(statement, options, default: default, allow_initials: true)
13
+ return persist! if choice == :always
14
+ choice == :yes
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ require 'clin'
2
+
3
+ # Parent class for shell interaction.
4
+ class Clin::ShellInteraction
5
+ class << self
6
+ attr_accessor :persist
7
+ end
8
+
9
+ attr_accessor :shell
10
+
11
+ # @param shell [Clin::Shell] Shell starting the interaction.
12
+ def initialize(shell)
13
+ @shell = shell
14
+ self.class.persist ||= {}
15
+ end
16
+
17
+ # @return [Boolean]
18
+ def persist?
19
+ self.class.persist[@shell] ||= false
20
+ end
21
+
22
+ # Mark the current shell to persist file interaction
23
+ def persist!
24
+ self.class.persist[@shell] = persist_answer
25
+ end
26
+
27
+ def persist_answer
28
+ true
29
+ end
30
+ end
31
+
32
+ require 'clin/shell_interaction/file_conflict'
33
+ require 'clin/shell_interaction/yes_or_no'
34
+ require 'clin/shell_interaction/choose'
data/lib/clin/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # Clin version
2
2
  module Clin
3
- VERSION = '0.2.0'
3
+ VERSION = '0.3.0'
4
4
  end
data/lib/clin.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'active_support'
2
2
  require 'active_support/core_ext'
3
3
  require 'optparse'
4
+ require 'readline'
4
5
  require 'clin/version'
5
6
 
6
7
  # Clin Global module. All classes and clin modules should be inside this module
@@ -9,6 +10,9 @@ module Clin
9
10
  # Set the global exe name. `Clin.exe_name = 'git'`
10
11
  attr_writer :exe_name
11
12
 
13
+ # Set the command when comparing 2 files(Used in the shell)
14
+ attr_writer :diff_cmd
15
+
12
16
  def default_exe_name
13
17
  'command'
14
18
  end
@@ -18,15 +22,20 @@ module Clin
18
22
  def exe_name
19
23
  @exe_name ||= Clin.default_exe_name
20
24
  end
25
+
26
+ def diff_cmd
27
+ @diff_cmd ||= 'diff -u'
28
+ end
21
29
  end
22
30
  end
23
31
 
32
+ require 'clin/command_mixin'
24
33
  require 'clin/command'
25
34
  require 'clin/command_parser'
26
- require 'clin/command_options_mixin'
27
35
  require 'clin/general_option'
28
36
  require 'clin/command_dispatcher'
29
37
  require 'clin/common/help_options'
30
38
  require 'clin/errors'
31
39
  require 'clin/option'
32
40
  require 'clin/option_list'
41
+ require 'clin/shell'
@@ -43,8 +43,8 @@ RSpec.describe Clin::CommandDispatcher do
43
43
  end
44
44
 
45
45
  describe '#parse' do
46
- let(:cmd1) { double(:command, parse: 'cmd1', usage: 'cmd1 use') }
47
- let(:cmd2) { double(:command, parse: 'cmd2', usage: 'cmd1 use') }
46
+ let(:cmd1) { double(:command_mixin, parse: 'cmd1', usage: 'cmd1 use') }
47
+ let(:cmd2) { double(:command_mixin, parse: 'cmd2', usage: 'cmd1 use') }
48
48
  let(:args) { %w(some args) }
49
49
  subject { Clin::CommandDispatcher.new(cmd1, cmd2) }
50
50
  context 'when first command match' do
@@ -81,4 +81,4 @@ RSpec.describe Clin::CommandDispatcher do
81
81
  it { expect(@error.to_s).to eq(subject.help_message) }
82
82
  end
83
83
  end
84
- end
84
+ end
@@ -0,0 +1,37 @@
1
+ RSpec.describe Clin::CommandMixin::Core do
2
+ def new_subject
3
+ a = Class.new
4
+ a.include Clin::CommandMixin::Core
5
+ a
6
+ end
7
+
8
+ describe '.prioritize' do
9
+ subject { new_subject }
10
+ it 'set priority to 1 when no argument given' do
11
+ subject.prioritize
12
+ expect(subject._priority).to be 1
13
+ end
14
+
15
+ it 'set priority to 1 when no argument given' do
16
+ subject.prioritize(42)
17
+ expect(subject._priority).to be 42
18
+ end
19
+ end
20
+
21
+ describe '.priority' do
22
+ subject { new_subject }
23
+ it 'get the default priority' do
24
+ expect(subject.priority).to be subject._default_priority
25
+ end
26
+
27
+ it 'sum default and priority when subject has been prioritize' do
28
+ subject.prioritize(42)
29
+ expect(subject.priority).to be subject._default_priority + 42
30
+ end
31
+
32
+ it 'has a smaller priority when inheriting' do
33
+ child = Class.new(subject)
34
+ expect(child.priority).to be < subject.priority
35
+ end
36
+ end
37
+ end
@@ -1,9 +1,14 @@
1
1
  require 'spec_helper'
2
- require 'clin/command_options_mixin'
3
2
 
4
- RSpec.describe Clin::CommandOptionsMixin do
3
+ RSpec.describe Clin::CommandMixin::Options do
4
+ def new_subject
5
+ a = Class.new
6
+ a.include Clin::CommandMixin::Options
7
+ a
8
+ end
9
+
5
10
  describe '#add_option' do
6
- subject { Class.new(Clin::CommandOptionsMixin) }
11
+ subject { new_subject }
7
12
  let(:option) { Clin::Option.new(:name, '-n') }
8
13
  before do
9
14
  subject.add_option option
@@ -11,11 +16,10 @@ RSpec.describe Clin::CommandOptionsMixin do
11
16
 
12
17
  it { expect(subject.options.size).to be 1 }
13
18
  it { expect(subject.options.first).to eq option }
14
- it { expect(Clin::CommandOptionsMixin.options.size).to be 0 }
15
19
  end
16
20
 
17
21
  describe '#option' do
18
- subject { Class.new(Clin::CommandOptionsMixin) }
22
+ subject { new_subject }
19
23
  let(:args) { [:name, '-n'] }
20
24
  let(:option) { Clin::Option.new(*args) }
21
25
 
@@ -29,7 +33,7 @@ RSpec.describe Clin::CommandOptionsMixin do
29
33
  end
30
34
 
31
35
  describe '#general_option' do
32
- subject { Class.new(Clin::CommandOptionsMixin) }
36
+ subject { new_subject }
33
37
  let(:option) { double(:option, register_options: true, new: true) }
34
38
  before do
35
39
  subject.general_option option
@@ -37,11 +41,10 @@ RSpec.describe Clin::CommandOptionsMixin do
37
41
  it { expect(option).to have_received(:new) }
38
42
  it { expect(subject.general_options.size).to be 1 }
39
43
  it { expect(subject.general_options.values.first).to eq(true) }
40
- it { expect(Clin::CommandOptionsMixin.general_options.size).to be 0 }
41
44
  end
42
45
 
43
46
  describe '#register_options' do
44
- subject { Class.new(Clin::CommandOptionsMixin) }
47
+ subject { new_subject }
45
48
  let(:opt1) { double(:option, register: true) }
46
49
  let(:opt2) { double(:option, register: true) }
47
50
  let(:g_opt_cls) { double(:general_option_class, register_options: true) }
@@ -130,8 +130,8 @@ RSpec.describe Clin::CommandParser do
130
130
  end
131
131
 
132
132
  context 'when using commands' do
133
- let(:cmd1) { double(:command) }
134
- let(:cmd2) { double(:command) }
133
+ let(:cmd1) { double(:command_mixin) }
134
+ let(:cmd2) { double(:command_mixin) }
135
135
  before do
136
136
  @command.dispatch :args, commands: [cmd1, cmd2]
137
137
  allow_any_instance_of(Clin::CommandDispatcher).to receive(:initialize)
@@ -67,9 +67,9 @@ RSpec.describe Clin::Command do
67
67
  subject.arguments(%w(remote <args>...))
68
68
  end
69
69
 
70
- let(:cmd1) { double(:command, usage: 'cmd1') }
71
- let(:cmd2) { double(:command, usage: 'cmd2') }
72
- let(:cmd3) { double(:command, usage: 'cmd3') }
70
+ let(:cmd1) { double(:command_mixin, usage: 'cmd1') }
71
+ let(:cmd2) { double(:command_mixin, usage: 'cmd2') }
72
+ let(:cmd3) { double(:command_mixin, usage: 'cmd3') }
73
73
  let(:cmds) { [cmd1, cmd2, cmd3] }
74
74
  let(:opts) { double(:option_parser, separator: true) }
75
75
  before do