clin 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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