diff --git a/.rubocop.yml b/.rubocop.yml index e9a9ec8..2017bc4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -28,7 +28,7 @@ Lint/ConstantDefinitionInBlock: - "spec/spec_helper.rb" Metrics/AbcSize: - Max: 18 + Max: 25 Metrics/MethodLength: Max: 30 diff --git a/examples/example1.rb b/examples/example1.rb index 4aa2048..da4776d 100755 --- a/examples/example1.rb +++ b/examples/example1.rb @@ -1,11 +1,11 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require "rubyshell" +require_relative "../lib/rubyshell" require "securerandom" -sh do - mkdir "files" +sh(debug: true) do + mkdir "-p", "files" cd "files" do 5.times do |i| diff --git a/lib/rubyshell.rb b/lib/rubyshell.rb index 1a33137..a126197 100644 --- a/lib/rubyshell.rb +++ b/lib/rubyshell.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "logger" + require_relative "rubyshell/version" require_relative "rubyshell/command" require_relative "rubyshell/chainer" @@ -12,15 +14,56 @@ require_relative "rubyshell/sanitizer" require_relative "rubyshell/parser" require_relative "rubyshell/parsers/base" +require_relative "rubyshell/debugger" + +module RubyShell + class << self + def debug=(value) + @debug_mode = !!value + end + + def debug(value = true) # rubocop:disable Style/OptionalBooleanParameter + previous_value = @debug_mode + + @debug_mode = value + + result = yield + + @debug_mode = previous_value + + result + end + + def debug? + @debug_mode == true + end + + attr_writer :logger + + def logger + @logger ||= Logger.new($stdout) + end + + def log_level=(level) + @log_level = level.to_s + end + + def log(text) + logger.send(@log_level || :info, text) + end + end +end module Kernel - def sh(command = nil, *args, &block) + def sh(command = nil, *args, **kwargs, &block) if command - RubyShell::Executor.send(command, *args) + RubyShell::Executor.send(command, *args, **kwargs) elsif block.nil? RubyShell::Executor else - RubyShell::Executor.class_eval(&block) + RubyShell.debug(kwargs[:debug]) do + RubyShell::Executor.class_eval(&block) + end end end end diff --git a/lib/rubyshell/command.rb b/lib/rubyshell/command.rb index 27f1208..a822aeb 100644 --- a/lib/rubyshell/command.rb +++ b/lib/rubyshell/command.rb @@ -16,11 +16,13 @@ def to_shell end def exec_command - result = RubyShell::TerminalExecutor.capture(to_shell, @options) + RubyShell::Debugger.run_wrapper(self, debug: @options[:_debug]) do + result = RubyShell::TerminalExecutor.capture(to_shell, @options) - result = RubyShell::Parser.parse(@options[:_parse], result) if @options[:_parse] + result = RubyShell::Parser.parse(@options[:_parse], result) if @options[:_parse] - result + result + end end alias exec exec_command diff --git a/lib/rubyshell/debugger.rb b/lib/rubyshell/debugger.rb new file mode 100644 index 0000000..0ac9f79 --- /dev/null +++ b/lib/rubyshell/debugger.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module RubyShell + module Debugger + class << self + def run_wrapper(command, debug: nil) + if debug || RubyShell.debug? + + time_one = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + result = yield + + time_two = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + RubyShell.log("Executed: #{command.to_shell.chomp}") + RubyShell.log(" Duration: #{format("%.6f", time_two - time_one)}s") + RubyShell.log(" Pid: #{result.metadata[:exit_status].pid}") + RubyShell.log(" Exit code: #{result.metadata[:exit_status].to_i}") + RubyShell.log(" Stdout: #{result.to_s.inspect}") + + result + else + yield + end + end + end + end +end diff --git a/lib/rubyshell/executor.rb b/lib/rubyshell/executor.rb index 8854365..2d02afc 100644 --- a/lib/rubyshell/executor.rb +++ b/lib/rubyshell/executor.rb @@ -10,8 +10,8 @@ def chain(&block) RubyShell::ChainContext.class_eval(&block).exec_commands end - def method_missing(method_name, *args) - command = RubyShell::Command.new(method_name.to_s.gsub(/!$/, ""), *args) + def method_missing(method_name, *args, **kwargs) + command = RubyShell::Command.new(method_name.to_s.gsub(/!$/, ""), *args, **kwargs) if method_name.to_s.match?(/!$/) command diff --git a/lib/rubyshell/results/string_result.rb b/lib/rubyshell/results/string_result.rb index 0854286..cb4096d 100644 --- a/lib/rubyshell/results/string_result.rb +++ b/lib/rubyshell/results/string_result.rb @@ -3,6 +3,14 @@ module RubyShell module Results class StringResult < String + def initialize(value, **kwargs) + @metadata = kwargs.delete(:metadata) + + super + end + + attr_reader :metadata + def inspect if $stdin.isatty to_s diff --git a/lib/rubyshell/terminal_executor.rb b/lib/rubyshell/terminal_executor.rb index 5146f78..a7d7a56 100644 --- a/lib/rubyshell/terminal_executor.rb +++ b/lib/rubyshell/terminal_executor.rb @@ -64,7 +64,10 @@ def self.capture(command, options) # rubocop:disable Metris/MethodLength,Metrics ) end - RubyShell::Results::StringResult.new(output.chomp) + RubyShell::Results::StringResult.new(output.chomp, metadata: { + command: command, + exit_status: status + }) end rescue StandardError => e raise e if e.is_a?(RubyShell::CommandError) diff --git a/spec/debugger_spec.rb b/spec/debugger_spec.rb new file mode 100644 index 0000000..9025119 --- /dev/null +++ b/spec/debugger_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +RSpec.describe RubyShell::Debugger do + around(:example) do |example| + Dir.mktmpdir do |dir| + Dir.chdir(dir) { example.run } + end + end + + describe ".run_wrapper" do + let(:log_output) { [] } + let(:logger) { double("Logger", info: nil) } + + before do + allow(logger).to receive(:info) { |msg| log_output << msg } + allow(RubyShell).to receive(:logger).and_return(logger) + end + + after do + RubyShell.debug = false + end + + context "when debug mode is disabled" do + def subject_method + sh.echo("hello") + end + + it "returns the command output" do + expect(subject_method).to eq("hello") + end + + it "does not log anything" do + subject_method + + expect(log_output).to be_empty + end + end + + context "when debug mode is enabled via block" do + def subject_method + sh(debug: true) { echo("hello") } + end + + it "returns the command output" do + expect(subject_method).to eq("hello") + end + + it "logs the command executed" do + subject_method + + expect(log_output).to include("Executed: echo hello") + end + + it "logs the duration" do + subject_method + + expect(log_output.find { |msg| msg.match?(/Duration: \d+\.\d+s/) }).not_to be_nil + end + + it "logs the pid" do + subject_method + + expect(log_output.find { |msg| msg.match?(/Pid: \d+/) }).not_to be_nil + end + + it "logs the exit code" do + subject_method + + expect(log_output).to include(" Exit code: 0") + end + + it "logs the stdout" do + subject_method + + expect(log_output).to include(' Stdout: "hello"') + end + end + + context "when debug mode is enabled via command option" do + def subject_method + sh.echo("hello", _debug: true) + end + + it "returns the command output" do + expect(subject_method).to eq("hello") + end + + it "logs the command executed" do + subject_method + + expect(log_output).to include("Executed: echo hello") + end + + it "logs the duration" do + subject_method + + expect(log_output.find { |msg| msg.match?(/Duration: \d+\.\d+s/) }).not_to be_nil + end + + it "logs the pid" do + subject_method + + expect(log_output.find { |msg| msg.match?(/Pid: \d+/) }).not_to be_nil + end + + it "logs the exit code" do + subject_method + + expect(log_output).to include(" Exit code: 0") + end + + it "logs the stdout" do + subject_method + + expect(log_output).to include(' Stdout: "hello"') + end + end + + context "when debug mode is enabled globally" do + before { RubyShell.debug = true } + + def subject_method + sh.echo("world") + end + + it "returns the command output" do + expect(subject_method).to eq("world") + end + + it "logs the command executed" do + subject_method + + expect(log_output).to include("Executed: echo world") + end + + it "logs the duration" do + subject_method + + expect(log_output.find { |msg| msg.match?(/Duration: \d+\.\d+s/) }).not_to be_nil + end + + it "logs the pid" do + subject_method + + expect(log_output.find { |msg| msg.match?(/Pid: \d+/) }).not_to be_nil + end + + it "logs the exit code" do + subject_method + + expect(log_output).to include(" Exit code: 0") + end + + it "logs the stdout" do + subject_method + + expect(log_output).to include(' Stdout: "world"') + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 463ddb2..1f606bb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require_relative "../lib/rubyshell" require "tmpdir" require "debug" +require_relative "../lib/rubyshell" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure