Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Lint/ConstantDefinitionInBlock:
- "spec/spec_helper.rb"

Metrics/AbcSize:
Max: 18
Max: 25

Metrics/MethodLength:
Max: 30
6 changes: 3 additions & 3 deletions examples/example1.rb
Original file line number Diff line number Diff line change
@@ -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|
Expand Down
49 changes: 46 additions & 3 deletions lib/rubyshell.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "logger"

require_relative "rubyshell/version"
require_relative "rubyshell/command"
require_relative "rubyshell/chainer"
Expand All @@ -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
Expand Down
8 changes: 5 additions & 3 deletions lib/rubyshell/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions lib/rubyshell/debugger.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions lib/rubyshell/executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions lib/rubyshell/results/string_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion lib/rubyshell/terminal_executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
161 changes: 161 additions & 0 deletions spec/debugger_spec.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down