From 6ffdea2349e9dfc276cccc2eebc5d00ca41a53b4 Mon Sep 17 00:00:00 2001 From: Aaron Allen Date: Thu, 24 Jul 2025 16:56:01 -0500 Subject: [PATCH 1/3] Expose `stderr` and `stdout` for commands Introduce `@stderr` and `@stdout` instance variables to make error and output streams accessible within commands. This enables more flexible error handling and message output customization. --- lib/dry/cli.rb | 7 +++++++ lib/dry/cli/command.rb | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/dry/cli.rb b/lib/dry/cli.rb index 5f938605..44b5d267 100644 --- a/lib/dry/cli.rb +++ b/lib/dry/cli.rb @@ -97,6 +97,10 @@ def call(arguments: ARGV, out: $stdout, err: $stderr) # @api private def perform_command(arguments) command, args = parse(kommand, arguments, []) + + command.instance_variable_set(:@err, err) + command.instance_variable_set(:@out, out) + command.call(**args) end @@ -113,6 +117,9 @@ def perform_registry(arguments) command, args = parse(result.command, result.arguments, result.names) + command.instance_variable_set(:@err, err) + command.instance_variable_set(:@out, out) + result.before_callbacks.run(command, args) command.call(**args) result.after_callbacks.run(command, args) diff --git a/lib/dry/cli/command.rb b/lib/dry/cli/command.rb index 3e6a6010..07afe771 100644 --- a/lib/dry/cli/command.rb +++ b/lib/dry/cli/command.rb @@ -379,6 +379,39 @@ def self.superclass_options optional_arguments subcommands ] => "self.class" + + protected + + # The error output used to print error messaging + # + # @example + # class MyCommand + # def call + # out.puts "Hello World!" + # exit(0) + # rescue StandardError => e + # err.puts "Uh oh: #{e.message}" + # exit(1) + # end + # end + # + # @since unreleased + # @return [IO] + attr_reader :err + + # The standard output object used to print messaging + # + # @example + # class MyCommand + # def call + # out.puts "Hello World!" + # exit(0) + # end + # end + # + # @since unreleased + # @return [IO] + attr_reader :out end end end From 3317d11a1c81a455e04116418c45b1b1e8957117 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Wed, 31 Dec 2025 10:24:15 +1100 Subject: [PATCH 2/3] Respect existing instance variables --- lib/dry/cli.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/dry/cli.rb b/lib/dry/cli.rb index 44b5d267..127b78b4 100644 --- a/lib/dry/cli.rb +++ b/lib/dry/cli.rb @@ -98,8 +98,8 @@ def call(arguments: ARGV, out: $stdout, err: $stderr) def perform_command(arguments) command, args = parse(kommand, arguments, []) - command.instance_variable_set(:@err, err) - command.instance_variable_set(:@out, out) + command.instance_variable_set(:@err, err) unless command.instance_variable_defined?(:@err) + command.instance_variable_set(:@out, out) unless command.instance_variable_defined?(:@out) command.call(**args) end @@ -117,8 +117,8 @@ def perform_registry(arguments) command, args = parse(result.command, result.arguments, result.names) - command.instance_variable_set(:@err, err) - command.instance_variable_set(:@out, out) + command.instance_variable_set(:@err, err) unless command.instance_variable_defined?(:@err) + command.instance_variable_set(:@out, out) unless command.instance_variable_defined?(:@out) result.before_callbacks.run(command, args) command.call(**args) From b569ed57d0da88abb7ba502c14cd64391c714ac0 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Wed, 31 Dec 2025 10:51:45 +1100 Subject: [PATCH 3/3] Add test --- spec/unit/dry/cli/cli_spec.rb | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/spec/unit/dry/cli/cli_spec.rb b/spec/unit/dry/cli/cli_spec.rb index d51d3fcc..b48a73ce 100644 --- a/spec/unit/dry/cli/cli_spec.rb +++ b/spec/unit/dry/cli/cli_spec.rb @@ -183,5 +183,45 @@ ) end end + + it "exposes @out and @err to command without overriding pre-existing ivars" do + # @out and @err are exposed by default + command_class = Class.new(Dry::CLI::Command) do + def call(**) + @out.puts "out" + @err.puts "err" + end + end + cli = Dry.CLI(command_class.new) + + default_out = StringIO.new + default_err = StringIO.new + cli.call(arguments: [], out: default_out, err: default_err) + + expect(default_out.string).to eq("out\n") + expect(default_err.string).to eq("err\n") + + # @out and @err do not override pre-existing ivars + custom_command_class = Class.new(command_class) do + define_method(:initialize) do |out, err| + super() + @out = out + @err = err + end + end + + custom_out = StringIO.new + custom_err = StringIO.new + cli = Dry.CLI(custom_command_class.new(custom_out, custom_err)) + + default_out = StringIO.new + default_err = StringIO.new + cli.call(arguments: [], out: default_out, err: default_err) + + expect(custom_out.string).to eq("out\n") + expect(custom_err.string).to eq("err\n") + expect(default_out.string).to eq("") + expect(default_err.string).to eq("") + end end end