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 +4 -4
- data/bin/rubyshell +8 -0
- data/lib/rubyshell/chain_context.rb +4 -0
- data/lib/rubyshell/chainer.rb +5 -14
- data/lib/rubyshell/command.rb +45 -29
- data/lib/rubyshell/executor.rb +7 -1
- data/lib/rubyshell/results/string_result.rb +15 -0
- data/lib/rubyshell/sanitizer.rb +28 -0
- data/lib/rubyshell/terminal_executor.rb +75 -0
- data/lib/rubyshell/version.rb +1 -1
- data/lib/rubyshell.rb +13 -2
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef414945c6e9fc58ca5320103e4abf00d551737dffadd06e171e8ca37330752b
|
|
4
|
+
data.tar.gz: d1784dc8f6ba9a21ea7e0da4fbde153b4f2f6d980037b572e02fe3c8b86a2e49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/rubyshell/chainer.rb
CHANGED
|
@@ -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
|
-
|
|
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)
|
data/lib/rubyshell/command.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
data/lib/rubyshell/executor.rb
CHANGED
|
@@ -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)
|
|
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,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
|
data/lib/rubyshell/version.rb
CHANGED
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
|
|
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.
|
|
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-
|
|
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
|