rubysh 0.0.3 → 0.0.4
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.
- data/README.md +12 -4
- data/examples/dots.sh +6 -0
- data/lib/rubysh/base_command.rb +10 -4
- data/lib/rubysh/command.rb +13 -1
- data/lib/rubysh/error.rb +1 -0
- data/lib/rubysh/redirect.rb +6 -1
- data/lib/rubysh/runner.rb +138 -17
- data/lib/rubysh/subprocess/parallel_io.rb +2 -2
- data/lib/rubysh/subprocess/pipe_wrapper.rb +20 -4
- data/lib/rubysh/subprocess.rb +2 -9
- data/lib/rubysh/triple_less_than.rb +1 -1
- data/lib/rubysh/version.rb +1 -1
- data/lib/rubysh.rb +11 -6
- metadata +4 -3
data/README.md
CHANGED
@@ -58,10 +58,18 @@ are arbitrary symbols):
|
|
58
58
|
runner.data(:stdout) # "hi\n"
|
59
59
|
runner.data(:stderr) # ""
|
60
60
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
61
|
+
## Controlled input
|
62
|
+
|
63
|
+
You can easily read and write data interactively:
|
64
|
+
|
65
|
+
>> runner = Rubysh('examples/dots.sh', Rubysh.>).run_async
|
66
|
+
=> Rubysh::Runner: examples/dots.sh >:stdout (readers: :stdout, pid: 78296)
|
67
|
+
>> runner.read(:how => :partial) # block until some output available
|
68
|
+
=> ".\n.\n.\n.\n.\n.\n.\n.\n.\n"
|
69
|
+
>> runner.read(:how => :nonblock)
|
70
|
+
=> ""
|
71
|
+
>> runner.read # block until all output available
|
72
|
+
=> [truncated]
|
65
73
|
|
66
74
|
## API
|
67
75
|
|
data/examples/dots.sh
ADDED
data/lib/rubysh/base_command.rb
CHANGED
@@ -9,6 +9,8 @@ module Rubysh
|
|
9
9
|
case arg
|
10
10
|
when BaseCommand, BaseDirective
|
11
11
|
arg.stringify
|
12
|
+
when Hash
|
13
|
+
arg.inspect
|
12
14
|
else
|
13
15
|
Shellwords.shellescape(arg.to_s)
|
14
16
|
end
|
@@ -26,12 +28,16 @@ module Rubysh
|
|
26
28
|
Runner.new(self)
|
27
29
|
end
|
28
30
|
|
29
|
-
def run
|
30
|
-
create_runner.run
|
31
|
+
def run(&blk)
|
32
|
+
create_runner.run(&blk)
|
31
33
|
end
|
32
34
|
|
33
|
-
def run_async
|
34
|
-
create_runner.run_async
|
35
|
+
def run_async(&blk)
|
36
|
+
create_runner.run_async(&blk)
|
37
|
+
end
|
38
|
+
|
39
|
+
def check_call(&blk)
|
40
|
+
create_runner.check_call(&blk)
|
35
41
|
end
|
36
42
|
|
37
43
|
def |(other)
|
data/lib/rubysh/command.rb
CHANGED
@@ -6,6 +6,7 @@ module Rubysh
|
|
6
6
|
@raw_args = args
|
7
7
|
@directives = []
|
8
8
|
@args = nil
|
9
|
+
@opts = {}
|
9
10
|
|
10
11
|
process_args
|
11
12
|
end
|
@@ -18,6 +19,9 @@ module Rubysh
|
|
18
19
|
when BaseDirective
|
19
20
|
@directives << arg
|
20
21
|
nil
|
22
|
+
when Hash
|
23
|
+
@opts.merge!(arg)
|
24
|
+
nil
|
21
25
|
else
|
22
26
|
arg.to_s
|
23
27
|
end
|
@@ -87,6 +91,14 @@ module Rubysh
|
|
87
91
|
state(runner)[:extra_directives] ||= []
|
88
92
|
end
|
89
93
|
|
94
|
+
def base_post_forks
|
95
|
+
post_forks = []
|
96
|
+
if cwd = @opts[:cwd]
|
97
|
+
post_forks << Proc.new {Dir.chdir(cwd)}
|
98
|
+
end
|
99
|
+
post_forks
|
100
|
+
end
|
101
|
+
|
90
102
|
def extra_post_forks(runner)
|
91
103
|
state(runner)[:extra_post_forks] ||= []
|
92
104
|
end
|
@@ -95,7 +107,7 @@ module Rubysh
|
|
95
107
|
# extras first because they are currently only used for
|
96
108
|
# pipeline, which should not win out over internal redirects.
|
97
109
|
directives = extra_directives(runner) + @directives
|
98
|
-
post_forks = extra_post_forks(runner)
|
110
|
+
post_forks = base_post_forks + extra_post_forks(runner)
|
99
111
|
state(runner)[:subprocess] = Subprocess.new(args, directives, post_forks, runner)
|
100
112
|
end
|
101
113
|
end
|
data/lib/rubysh/error.rb
CHANGED
data/lib/rubysh/redirect.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
1
3
|
module Rubysh
|
2
4
|
# Note that in bash, the semantics of redirection appear to be
|
3
5
|
# following (tested empirically, rather than reading a spec):
|
@@ -119,7 +121,10 @@ module Rubysh
|
|
119
121
|
:target_reading? => target_reading?,
|
120
122
|
:target => target_reading? ? pipe.reader : pipe.writer,
|
121
123
|
:complement => target_reading? ? pipe.writer : pipe.reader,
|
122
|
-
:buffer =>
|
124
|
+
:buffer => StringIO.new,
|
125
|
+
:target_name => target_name,
|
126
|
+
:read_pos => 0,
|
127
|
+
:subprocess_fd_number => Util.to_fileno(source)
|
123
128
|
}
|
124
129
|
end
|
125
130
|
|
data/lib/rubysh/runner.rb
CHANGED
@@ -14,10 +14,70 @@ module Rubysh
|
|
14
14
|
prepare!
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
state[:
|
17
|
+
def write(bytes, target=0)
|
18
|
+
raise Rubysh::Error::AlreadyRunError.new("Can only write to a runner in runner_state :started, not #{@runner_state.inspect}") unless @runner_state == :started
|
19
|
+
state = target_state(target, false)
|
20
|
+
target_name = state[:target_name]
|
21
|
+
@parallel_io.write(target_name, bytes, false)
|
22
|
+
end
|
23
|
+
|
24
|
+
# A bit of an unothordox read interface, not sure if I like
|
25
|
+
# it. Also, the target/opts magic is probably too magical (and not
|
26
|
+
# consistent with write!)
|
27
|
+
#
|
28
|
+
# You can do:
|
29
|
+
#
|
30
|
+
# read: finish the subprocess, and read from FD 1 in the child
|
31
|
+
# read(:how => :partial): wait until there are bytes on FD 1, and
|
32
|
+
# then return what you can
|
33
|
+
# read(2, :how => :partial): Do the same for FD 2
|
34
|
+
# read(:stdout, :how => :partial): Do the same with whatever the named
|
35
|
+
# descriptor :stdout
|
36
|
+
# read(:how => :nonblock): Return whatever is immediately available
|
37
|
+
def read(target=nil, opts=nil)
|
38
|
+
raise Rubysh::Error::AlreadyRunError.new("Can only read from a runner in runner_state :started or :waited, not #{@runner_state.inspect}") unless @runner_state == :started || @runner_state == :waited
|
39
|
+
|
40
|
+
if target.kind_of?(Hash)
|
41
|
+
opts = target
|
42
|
+
target = nil
|
43
|
+
end
|
44
|
+
target ||= 1
|
45
|
+
opts ||= {}
|
46
|
+
|
47
|
+
# TODO: add a stringio
|
48
|
+
state = target_state(target, true)
|
49
|
+
target_name = state[:target_name]
|
50
|
+
|
51
|
+
# Be nice to people and validate the hash
|
52
|
+
valid_keys = [:how]
|
53
|
+
extra_keys = opts.keys - valid_keys
|
54
|
+
raise raise Rubysh::Error::BaseError.new("Unrecognized keys #{extra_keys.inspect}. (Valid keys: #{valid_keys.inspect}") if extra_keys.length > 0
|
55
|
+
|
56
|
+
case how = opts[:how]
|
57
|
+
when :partial
|
58
|
+
# Read until we get some bytes
|
59
|
+
@parallel_io.run_once until state[:buffer].length != state[:read_pos]
|
60
|
+
when :nonblock
|
61
|
+
@parallel_io.read_available(state[:target])
|
62
|
+
when nil
|
63
|
+
communicate if @runner_state == :started
|
64
|
+
else
|
65
|
+
raise Rubysh::Error::BaseError.new("Invalid read directive #{how.inspect}")
|
66
|
+
end
|
67
|
+
|
68
|
+
state[:buffer].pos = state[:read_pos]
|
69
|
+
bytes = state[:buffer].read
|
70
|
+
# Could also increment by bytes, but meh.
|
71
|
+
state[:read_pos] = state[:buffer].pos
|
72
|
+
bytes
|
73
|
+
end
|
74
|
+
|
75
|
+
def communicate
|
76
|
+
raise Rubysh::Error::AlreadyRunError.new("Can only communicate with a runner in runner_state :started, not #{@runner_state.inspect}") unless @runner_state == :started
|
77
|
+
writers.each do |io, target_name|
|
78
|
+
@parallel_io.close(target_name) unless io.closed?
|
79
|
+
end
|
80
|
+
wait
|
21
81
|
end
|
22
82
|
|
23
83
|
# Ruby's Process::Status. Has fun things like pid and signaled?
|
@@ -40,6 +100,11 @@ module Rubysh
|
|
40
100
|
end
|
41
101
|
end
|
42
102
|
|
103
|
+
def exec_error
|
104
|
+
subprocess = state(@command)[:subprocess]
|
105
|
+
subprocess.exec_error
|
106
|
+
end
|
107
|
+
|
43
108
|
# API for running/waiting
|
44
109
|
def run_async
|
45
110
|
raise Rubysh::Error::AlreadyRunError.new("You have already run this #{self.class} instance. Cannot run again. You can run its command directly though, which will create a fresh #{self.class} instance.") unless @runner_state == :initialized
|
@@ -49,15 +114,22 @@ module Rubysh
|
|
49
114
|
self
|
50
115
|
end
|
51
116
|
|
52
|
-
def
|
117
|
+
def run(input={}, &blk)
|
118
|
+
run_async
|
119
|
+
blk.call(self) if blk
|
53
120
|
run_io
|
54
121
|
do_wait
|
122
|
+
self
|
55
123
|
end
|
56
124
|
|
57
|
-
def
|
58
|
-
|
59
|
-
|
60
|
-
|
125
|
+
def check_call(&blk)
|
126
|
+
run
|
127
|
+
status = full_status
|
128
|
+
unless status.success?
|
129
|
+
raise Rubysh::Error::BadExitError.new("#{@command} exited with #{rendered_status(status)}")
|
130
|
+
end
|
131
|
+
status
|
132
|
+
self
|
61
133
|
end
|
62
134
|
|
63
135
|
def readers
|
@@ -87,12 +159,12 @@ module Rubysh
|
|
87
159
|
def inspect
|
88
160
|
extras = []
|
89
161
|
valid_readers = readers.values.map(&:inspect).join(', ')
|
90
|
-
valid_writers =
|
162
|
+
valid_writers = writers.values.map(&:inspect).join(', ')
|
91
163
|
|
92
164
|
extras << "readers: #{valid_readers}" if valid_readers.length > 0
|
93
165
|
extras << "writers: #{valid_writers}" if valid_writers.length > 0
|
94
|
-
if status =
|
95
|
-
extras <<
|
166
|
+
if status = full_status
|
167
|
+
extras << rendered_status(status)
|
96
168
|
elsif mypid = pid
|
97
169
|
extras << "pid: #{pid}"
|
98
170
|
end
|
@@ -107,21 +179,54 @@ module Rubysh
|
|
107
179
|
end
|
108
180
|
|
109
181
|
# Internal helpers
|
110
|
-
def target_state(target_name)
|
111
|
-
|
182
|
+
def target_state(target_name, reading=nil)
|
183
|
+
case target_name
|
184
|
+
when Symbol
|
185
|
+
target_state = @targets[target_name]
|
186
|
+
raise Rubysh::Error::BaseError.new("Invalid target symbol: #{target_name.inspect} (valid target symbols are: #{@targets.keys.inspect})") unless target_state
|
187
|
+
when Fixnum
|
188
|
+
targets = targets_by_fd_numbers
|
189
|
+
target_state = targets[target_name]
|
190
|
+
raise Rubysh::Error::BaseError.new("Invalid target fd number: #{target_name.inspect} (valid target fd numbers are: #{targets.keys.inspect}})") unless target_state
|
191
|
+
else
|
192
|
+
raise Rubysh::Error::BaseError.new("Invalid type for target name: #{target_name.inspect} (#{target_name.class}). Valid types are Symbol and Fixnum.")
|
193
|
+
end
|
194
|
+
|
195
|
+
if reading.nil?
|
196
|
+
# No checking
|
197
|
+
elsif target_state[:target_reading?] && !reading
|
198
|
+
raise Rubysh::Error::BaseError.new("Trying to write to read pipe #{target_name}")
|
199
|
+
elsif !target_state[:target_reading?] && reading
|
200
|
+
raise Rubysh::Error::BaseError.new("Trying to read from write pipe #{target_name}")
|
201
|
+
end
|
202
|
+
|
203
|
+
target_state
|
112
204
|
end
|
113
205
|
|
114
206
|
private
|
115
207
|
|
208
|
+
def wait
|
209
|
+
run_io
|
210
|
+
do_wait
|
211
|
+
end
|
212
|
+
|
213
|
+
def targets_by_fd_numbers
|
214
|
+
@targets.inject({}) do |hash, (_, target_state)|
|
215
|
+
fd_num = target_state[:subprocess_fd_number]
|
216
|
+
hash[fd_num] = target_state
|
217
|
+
hash
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
116
221
|
def do_wait
|
117
|
-
|
222
|
+
return unless @runner_state == :parallel_io_ran
|
118
223
|
@command.wait(self)
|
119
224
|
@runner_state = :waited
|
120
225
|
self
|
121
226
|
end
|
122
227
|
|
123
228
|
def run_io
|
124
|
-
|
229
|
+
return unless @runner_state == :started
|
125
230
|
@parallel_io.run
|
126
231
|
@runner_state = :parallel_io_ran
|
127
232
|
self
|
@@ -136,11 +241,16 @@ module Rubysh
|
|
136
241
|
def prepare_io
|
137
242
|
@parallel_io = Subprocess::ParallelIO.new(readers, writers)
|
138
243
|
@parallel_io.on_read do |target_name, data|
|
244
|
+
state = @targets[target_name]
|
245
|
+
buffer = state[:buffer]
|
139
246
|
if data == Subprocess::ParallelIO::EOF
|
140
247
|
Rubysh.log.debug("EOF reached on #{target_name.inspect}")
|
248
|
+
buffer.close_write
|
141
249
|
else
|
142
250
|
Rubysh.log.debug("Just read #{data.inspect} on #{target_name.inspect}")
|
143
|
-
|
251
|
+
# Seek to end
|
252
|
+
buffer.pos = buffer.length
|
253
|
+
buffer.write(data)
|
144
254
|
end
|
145
255
|
end
|
146
256
|
|
@@ -152,5 +262,16 @@ module Rubysh
|
|
152
262
|
end
|
153
263
|
end
|
154
264
|
end
|
265
|
+
|
266
|
+
def rendered_status(status)
|
267
|
+
if exitstatus = status.exitstatus
|
268
|
+
"exitstatus: #{exitstatus}"
|
269
|
+
elsif termsig = status.termsig
|
270
|
+
name, _ = Signal.list.detect {|name, number| number == termsig}
|
271
|
+
"termsig: #{name} [signal number #{termsig}]"
|
272
|
+
else
|
273
|
+
''
|
274
|
+
end
|
275
|
+
end
|
155
276
|
end
|
156
277
|
end
|
@@ -65,8 +65,6 @@ class Rubysh::Subprocess
|
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
-
private
|
69
|
-
|
70
68
|
def run_once
|
71
69
|
potential_readers = available_readers
|
72
70
|
potential_writers = available_writers
|
@@ -97,6 +95,8 @@ class Rubysh::Subprocess
|
|
97
95
|
end
|
98
96
|
end
|
99
97
|
|
98
|
+
private
|
99
|
+
|
100
100
|
def finalize_reader(reader)
|
101
101
|
@finished_readers.add(reader)
|
102
102
|
issue_reader_callback(reader, EOF)
|
@@ -1,5 +1,17 @@
|
|
1
1
|
class Rubysh::Subprocess
|
2
2
|
class PipeWrapper
|
3
|
+
begin
|
4
|
+
require 'json'
|
5
|
+
SERIALIZER = JSON
|
6
|
+
rescue LoadError => e
|
7
|
+
if ENV['RUBYSH_ENABLE_YAML']
|
8
|
+
require 'yaml'
|
9
|
+
SERIALIZER = YAML
|
10
|
+
else
|
11
|
+
raise LoadError.new("Could not import JSON (#{e}). You should either run in an environment with rubygems and JSON, or you can set the RUBYSH_ENABLE_YAML environment variable to allow Rubysh to internally use YAML for communication rather than JSON. This is believed safe, but YAML-parsing of untrusted input is bad, so only do this if you really can't get JSON.")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
3
15
|
attr_accessor :reader, :writer
|
4
16
|
|
5
17
|
def initialize(reader_cloexec=true, writer_cloexec=true)
|
@@ -36,18 +48,22 @@ class Rubysh::Subprocess
|
|
36
48
|
end
|
37
49
|
end
|
38
50
|
|
39
|
-
def
|
51
|
+
def dump_json_and_close(msg)
|
52
|
+
dumped = SERIALIZER.dump(msg)
|
40
53
|
begin
|
41
|
-
|
54
|
+
@writer.write(dumped)
|
42
55
|
ensure
|
43
56
|
@writer.close
|
44
57
|
Rubysh.assert(@reader.closed?, "Reader should already be closed")
|
45
58
|
end
|
46
59
|
end
|
47
60
|
|
48
|
-
def
|
61
|
+
def load_json_and_close
|
62
|
+
contents = @reader.read
|
63
|
+
return false if contents.length == 0
|
64
|
+
|
49
65
|
begin
|
50
|
-
|
66
|
+
SERIALIZER.load(contents)
|
51
67
|
rescue ArgumentError => e
|
52
68
|
# e.g. ArgumentError: syntax error on line 0, col 2: `' (could
|
53
69
|
# happen if the subprocess was killed while writing a message)
|
data/lib/rubysh/subprocess.rb
CHANGED
@@ -3,13 +3,6 @@ require 'fcntl'
|
|
3
3
|
require 'timeout'
|
4
4
|
require 'thread'
|
5
5
|
|
6
|
-
# Using YAML to avoid the JSON dep. open4 uses Marshal to pass around
|
7
|
-
# the exception object, but I'm always a bit sketched by Marshal when
|
8
|
-
# it's not needed (i.e. don't want the subprocess to have the ability
|
9
|
-
# to execute code in the parent, even if it should lose that ability
|
10
|
-
# post-exec.)
|
11
|
-
require 'yaml'
|
12
|
-
|
13
6
|
require 'rubysh/subprocess/parallel_io'
|
14
7
|
require 'rubysh/subprocess/pipe_wrapper'
|
15
8
|
|
@@ -131,7 +124,7 @@ module Rubysh
|
|
131
124
|
# TODO: this may need coercion in Ruby1.9
|
132
125
|
'caller' => e.send(:caller)
|
133
126
|
}
|
134
|
-
@exec_status.
|
127
|
+
@exec_status.dump_json_and_close(msg)
|
135
128
|
# Note: atexit handlers will fire in this case. May want to do
|
136
129
|
# something about that.
|
137
130
|
raise
|
@@ -139,7 +132,7 @@ module Rubysh
|
|
139
132
|
end
|
140
133
|
|
141
134
|
def handle_exec_error
|
142
|
-
msg = @exec_status.
|
135
|
+
msg = @exec_status.load_json_and_close
|
143
136
|
|
144
137
|
case msg
|
145
138
|
when false
|
data/lib/rubysh/version.rb
CHANGED
data/lib/rubysh.rb
CHANGED
@@ -89,9 +89,14 @@ end
|
|
89
89
|
|
90
90
|
module Rubysh
|
91
91
|
# Convenience methods
|
92
|
-
def self.run(
|
93
|
-
command = Rubysh::Command.new(
|
94
|
-
command.run
|
92
|
+
def self.run(*args, &blk)
|
93
|
+
command = Rubysh::Command.new(args)
|
94
|
+
command.run(&blk)
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.check_call(*args, &blk)
|
98
|
+
command = Rubysh::Command.new(*args)
|
99
|
+
command.check_call(&blk)
|
95
100
|
end
|
96
101
|
|
97
102
|
def self.Command(*args)
|
@@ -118,15 +123,15 @@ module Rubysh
|
|
118
123
|
FD.new(2)
|
119
124
|
end
|
120
125
|
|
121
|
-
def self.>(target)
|
126
|
+
def self.>(target=:stdout)
|
122
127
|
Redirect.new(1, '>', target)
|
123
128
|
end
|
124
129
|
|
125
|
-
def self.>>(target)
|
130
|
+
def self.>>(target=:stdout)
|
126
131
|
Redirect.new(1, '>>', target)
|
127
132
|
end
|
128
133
|
|
129
|
-
def self.<(target)
|
134
|
+
def self.<(target=:stdin)
|
130
135
|
Redirect.new(0, '<', target)
|
131
136
|
end
|
132
137
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubysh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-02-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: minitest
|
@@ -55,6 +55,7 @@ files:
|
|
55
55
|
- LICENSE
|
56
56
|
- README.md
|
57
57
|
- Rakefile
|
58
|
+
- examples/dots.sh
|
58
59
|
- lib/rubysh.rb
|
59
60
|
- lib/rubysh/base_command.rb
|
60
61
|
- lib/rubysh/base_directive.rb
|
@@ -109,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
110
|
version: '0'
|
110
111
|
requirements: []
|
111
112
|
rubyforge_project:
|
112
|
-
rubygems_version: 1.8.
|
113
|
+
rubygems_version: 1.8.23
|
113
114
|
signing_key:
|
114
115
|
specification_version: 3
|
115
116
|
summary: ! 'Rubysh makes shelling out easy with a __sh__-like syntax layer for Ruby: irb
|