rubyshell 1.3.5 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76b521dcdcb6c325a9b5bad95e4b5257e1c1f75068e04cfbab2b93943f97c829
4
- data.tar.gz: 85ce6c941cf287d130b95db931330b8fce358410f6b5f2c9f8061ce904ca143b
3
+ metadata.gz: ef414945c6e9fc58ca5320103e4abf00d551737dffadd06e171e8ca37330752b
4
+ data.tar.gz: d1784dc8f6ba9a21ea7e0da4fbde153b4f2f6d980037b572e02fe3c8b86a2e49
5
5
  SHA512:
6
- metadata.gz: 349e7fd9cfbec0aa350960f8e1f4e4913fc5fbf1b986a92cd8cd5898a77dce160705c5feeb61d57938fc1fc51535555e4320a9c89f4ae15412a34b67ac84dbf8
7
- data.tar.gz: 9a851930f86e7bc7652102f4698ed637ea65497e719d893e169c3fa1703643d06f5807b1d0d465742ef987f4204892f07d653fbe744eb10396326c4b30fdc1a0
6
+ metadata.gz: a993d2b94f11e041a10dafb537a4bfb6395c3d06f81ed19da5cb3fb0b1a5e290d08fe310ee34592447aa90ce0910e714b35941c2bef78f1dbe0fa8abb7692348
7
+ data.tar.gz: 146e1d266782ea61c73776799d7beadce9fcba0e1659edcf253c85e5c9ae57f143a39e27479d12394ee469bf589185a218ebb007b6b2fd7a6782606ec20a67c9
data/bin/rubyshell CHANGED
@@ -7,4 +7,12 @@ if ARGV.first&.strip&.start_with? "exec"
7
7
  extend RubyShell::Executor # rubocop:disable Style/MixinUsage
8
8
 
9
9
  load(ARGV.last)
10
+ elsif ARGV.first&.strip&.start_with? "new"
11
+ file_name = "#{ARGV.last.gsub(".rb", "")}.rb" if ARGV.size > 1
12
+ file_name ||= "new_script.rb"
13
+
14
+ sh do
15
+ touch file_name
16
+ chmod "+x", file_name
17
+ end
10
18
  end
@@ -2,6 +2,10 @@
2
2
 
3
3
  module RubyShell
4
4
  class ChainContext
5
+ def self.sh(command, *args)
6
+ method_missing(command, *args)
7
+ end
8
+
5
9
  def self.method_missing(method_name, *args, &block)
6
10
  RubyShell::Chainer.new(RubyShell::Command.new(method_name, *(args << { _manual: true }), &block))
7
11
  end
@@ -1,13 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
-
5
3
  module RubyShell
6
4
  class Chainer
7
5
  attr_reader :parts
8
6
 
9
- def initialize(command)
7
+ def initialize(command, options = {})
10
8
  @parts = [command]
9
+ @options = options
11
10
  end
12
11
 
13
12
  def handle_chain(operator, chainer)
@@ -33,19 +32,11 @@ module RubyShell
33
32
  end
34
33
 
35
34
  def exec_commands
36
- Open3.capture3(to_shell).then do |stdout, stderr, status|
37
- unless status.success?
38
- raise RubyShell::CommandError.new(command: to_shell, stdout: stdout, stderr: stderr, status: status)
39
- end
40
-
41
- stdout.chomp
42
- end
43
- rescue StandardError => e
44
- raise e if e.is_a?(RubyShell::CommandError)
45
-
46
- raise RubyShell::CommandError.new(command: to_shell, message: e.message)
35
+ RubyShell::TerminalExecutor.capture(to_shell, @options)
47
36
  end
48
37
 
38
+ alias exec exec_commands
39
+
49
40
  def to_shell
50
41
  parts.map do |part|
51
42
  if part.is_a?(RubyShell::Command)
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
-
5
3
  module RubyShell
6
4
  class Command
5
+ attr_accessor :command_name, :options
6
+
7
7
  def initialize(command_name, *args, &block)
8
8
  @command_name = command_name
9
9
  @args = args
10
+ @options = extract_options(args)
10
11
  @block = block
11
12
  end
12
13
 
@@ -15,54 +16,69 @@ module RubyShell
15
16
  end
16
17
 
17
18
  def exec_command
18
- Open3.capture3(to_shell).then do |stdout, stderr, status|
19
- unless status.success?
20
- raise RubyShell::CommandError.new(command: to_shell, stdout: stdout, stderr: stderr, status: status)
21
- end
22
-
23
- StringWrapper.new(stdout.chomp)
24
- end
25
- rescue StandardError => e
26
- raise e if e.is_a?(RubyShell::CommandError)
27
-
28
- raise RubyShell::CommandError.new(command: to_shell, message: e.message)
19
+ RubyShell::TerminalExecutor.capture(to_shell, @options)
29
20
  end
30
21
 
22
+ alias exec exec_command
23
+
31
24
  def parsed_args
32
25
  @args.map do |arg|
33
26
  case arg
34
27
  when Hash
35
28
  map_hash_arg(arg)
36
29
  else
37
- arg.to_s
30
+ RubyShell::Sanitizer.sanitize_to_shell(arg.to_s)
38
31
  end
39
32
  end.flatten
40
33
  end
41
34
 
42
35
  private
43
36
 
37
+ def method_missing(method_name, *args, &block)
38
+ if method_name.start_with?(/[^A-Za-z0-9]/)
39
+ RubyShell::Chainer.new(self).send(method_name, *args, block)
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ def respond_to_missing?(_name, _include_private)
46
+ false
47
+ end
48
+
49
+ def extract_options(args)
50
+ args.reduce({}) do |acc, value|
51
+ next acc unless value.is_a?(Hash)
52
+
53
+ acc.merge(value)
54
+ end
55
+ end
56
+
44
57
  def map_hash_arg(arg)
45
58
  arg.map do |k, v|
46
59
  next if k.start_with?("_")
47
60
 
48
- key = if k.length == 1
49
- "-#{k}"
50
- else
51
- "--#{k}"
52
- end
53
-
54
- [key, v.is_a?(TrueClass) ? nil : "'#{v}'"].compact.join(" ")
61
+ if v.is_a?(Array)
62
+ v.map do |e|
63
+ map_hash_entry_to_string(k, e)
64
+ end.join(" ")
65
+ else
66
+ map_hash_entry_to_string(k, v)
67
+ end
55
68
  end.compact
56
69
  end
57
- end
58
70
 
59
- class StringWrapper < String
60
- def inspect
61
- if $stdin.isatty
62
- to_s
63
- else
64
- super
65
- end
71
+ def map_hash_entry_to_string(key, value)
72
+ key_string = if key.length == 1
73
+ "-#{key}"
74
+ else
75
+ "--#{key}"
76
+ end
77
+
78
+ [
79
+ key_string,
80
+ value.is_a?(TrueClass) ? nil : RubyShell::Sanitizer.sanitize_to_shell("'#{value}'")
81
+ ].compact.join(" ")
66
82
  end
67
83
  end
68
84
  end
@@ -11,7 +11,13 @@ module RubyShell
11
11
  end
12
12
 
13
13
  def method_missing(method_name, *args)
14
- RubyShell::Command.new(method_name, *args).exec_command
14
+ command = RubyShell::Command.new(method_name.to_s.gsub(/!$/, ""), *args)
15
+
16
+ if method_name.to_s.match?(/!$/)
17
+ command
18
+ else
19
+ command.exec_command
20
+ end
15
21
  end
16
22
 
17
23
  def respond_to_missing?(_name, _include_private)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyShell
4
+ module Results
5
+ class StringResult < String
6
+ def inspect
7
+ if $stdin.isatty
8
+ to_s
9
+ else
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyShell
4
+ module Sanitizer
5
+ SAFE_REGEX = /"/.freeze
6
+
7
+ # Inspired on https://github.com/ruby/shellwords/blob/master/lib/shellwords.rb
8
+ def self.sanitize_to_shell(string)
9
+ return unless string
10
+
11
+ raise ArgumentError, "NUL character" if string.index("\0")
12
+
13
+ string = string.to_s.dup
14
+
15
+ if string.match?(/\A(["'])(.*)\1\z/m)
16
+ inner = string[1..-2]
17
+
18
+ inner.gsub!(SAFE_REGEX) { |ch| "\\#{ch}" }
19
+
20
+ string.replace("\"#{inner}\"")
21
+ else
22
+ string.gsub!(SAFE_REGEX) { |ch| "\\#{ch}" }
23
+ end
24
+
25
+ string
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module RubyShell
6
+ module TerminalExecutor
7
+ SELECT_TIMEOUT = Rational(1, 20).freeze
8
+
9
+ def self.capture(command, options) # rubocop:disable Metris/MethodLength,Metrics/CyclomaticComplexity,Metrix/AbcSize,Metrics/PerceivedComplexity
10
+ stdin_value = if options[:_stdin].is_a?(RubyShell::Command) || options[:_stdin].is_a?(RubyShell::Chainer)
11
+ options[:_stdin].exec
12
+ else
13
+ options[:_stdin]
14
+ end
15
+
16
+ Open3.popen3(command) do |stdin, stdout, stderr, w_thread|
17
+ stdin.write(stdin_value) if stdin_value
18
+
19
+ stdin.close
20
+
21
+ stdout.binmode
22
+ stderr.binmode
23
+
24
+ output = +""
25
+ error = +""
26
+ ios = { stdout => output, stderr => error }
27
+
28
+ status = nil
29
+
30
+ # What was my idea here:
31
+ # If has not any io readable and the program exited in the previous loop, stop
32
+ until ios.empty?
33
+ status ||= w_thread.join(0)
34
+
35
+ readable, = IO.select(ios.keys, nil, nil, 0)
36
+
37
+ break if !readable && status
38
+ next unless readable
39
+
40
+ readable.each do |io|
41
+ loop do
42
+ chunk = io.read_nonblock(4096, exception: false)
43
+ case chunk
44
+ when :wait_readable
45
+ break
46
+ when nil
47
+ ios.delete(io)
48
+ break
49
+ else
50
+ ios[io] << chunk
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ status = w_thread.value
57
+
58
+ if status && !status.success?
59
+ raise RubyShell::CommandError.new(
60
+ command: command,
61
+ stdout: output,
62
+ stderr: error,
63
+ status: status
64
+ )
65
+ end
66
+
67
+ RubyShell::Results::StringResult.new(output.chomp)
68
+ end
69
+ rescue StandardError => e
70
+ raise e if e.is_a?(RubyShell::CommandError)
71
+
72
+ raise RubyShell::CommandError.new(command: command, message: e.message)
73
+ end
74
+ end
75
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyShell
4
- VERSION = "1.3.5"
4
+ VERSION = "1.4.0"
5
5
  end
data/lib/rubyshell.rb CHANGED
@@ -7,13 +7,24 @@ require_relative "rubyshell/chain_context"
7
7
  require_relative "rubyshell/overwrited_commands"
8
8
  require_relative "rubyshell/executor"
9
9
  require_relative "rubyshell/error"
10
+ require_relative "rubyshell/results/string_result"
11
+ require_relative "rubyshell/terminal_executor"
12
+ require_relative "rubyshell/sanitizer"
10
13
 
11
14
  module Kernel
12
- def sh(&block)
13
- if block.nil?
15
+ def sh(command = nil, *args, &block)
16
+ if command
17
+ RubyShell::Executor.send(command, *args)
18
+ elsif block.nil?
14
19
  RubyShell::Executor
15
20
  else
16
21
  RubyShell::Executor.class_eval(&block)
17
22
  end
18
23
  end
19
24
  end
25
+
26
+ class String
27
+ def quoted
28
+ "\"#{self}\""
29
+ end
30
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyshell
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.5
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - albertalef
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-20 00:00:00.000000000 Z
11
+ date: 2026-01-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A rubist way to run shell commands
14
14
  email:
@@ -31,6 +31,9 @@ files:
31
31
  - lib/rubyshell/error.rb
32
32
  - lib/rubyshell/executor.rb
33
33
  - lib/rubyshell/overwrited_commands.rb
34
+ - lib/rubyshell/results/string_result.rb
35
+ - lib/rubyshell/sanitizer.rb
36
+ - lib/rubyshell/terminal_executor.rb
34
37
  - lib/rubyshell/tty/irb.rb
35
38
  - lib/rubyshell/version.rb
36
39
  homepage: https://github.com/albertalef/rubyshell