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 CHANGED
@@ -58,10 +58,18 @@ are arbitrary symbols):
58
58
  runner.data(:stdout) # "hi\n"
59
59
  runner.data(:stderr) # ""
60
60
 
61
- Support for controlled input isn't quite ready, but the syntax will be
62
- similar to the above. I want to support interactivity (so being able
63
- to write data, read some data, and then write more data), and haven't
64
- quite decided on the right API for this yet.
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
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+
3
+ for i in $(seq 1 100); do
4
+ echo .
5
+ sleep 1
6
+ done
@@ -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)
@@ -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
@@ -16,5 +16,6 @@ module Rubysh
16
16
  class UnreachableError < BaseError; end
17
17
  class AlreadyClosedError < BaseError; end
18
18
  class AlreadyRunError < BaseError; end
19
+ class BadExitError < BaseError; end
19
20
  end
20
21
  end
@@ -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 data(target_name)
18
- state = target_state(target_name)
19
- raise Rubysh::Error::BaseError.new("Can only access data for readable FDs") unless state[:target_reading?]
20
- state[:buffer].join
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 wait
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 run(input={})
58
- run_async
59
- run_io
60
- do_wait
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 = readers.values.map(&:inspect).join(', ')
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 = exitstatus
95
- extras << "exitstatus: #{status}"
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
- @targets[target_name] || raise(Rubysh::Error::BaseError.new("Invalid target: #{target_name.inspect} (valid targets are: #{@targets.keys.inspect})"))
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
- raise Rubysh::Error::AlreadyRunError.new("You must run parallel io before waiting. (Perhaps you want to use the 'run' method, which takes care of the plumbing for you?)") unless @runner_state == :parallel_io_ran
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
- raise Rubysh::Error::AlreadyRunError.new("You must start the subprocesses before running parallel io. (Perhaps you want to use the 'run' method, which takes care of the plumbing for you?)") unless @runner_state == :started
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
- @targets[target_name][:buffer] << data
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 dump_yaml_and_close(msg)
51
+ def dump_json_and_close(msg)
52
+ dumped = SERIALIZER.dump(msg)
40
53
  begin
41
- YAML.dump(msg, @writer)
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 load_yaml_and_close
61
+ def load_json_and_close
62
+ contents = @reader.read
63
+ return false if contents.length == 0
64
+
49
65
  begin
50
- YAML.load(@reader)
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)
@@ -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.dump_yaml_and_close(msg)
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.load_yaml_and_close
135
+ msg = @exec_status.load_json_and_close
143
136
 
144
137
  case msg
145
138
  when false
@@ -8,7 +8,7 @@ module Rubysh
8
8
  @fd = fd
9
9
  end
10
10
 
11
- def <(literal)
11
+ def <(literal=:stdin)
12
12
  TripleLessThan.new(@fd, literal)
13
13
  end
14
14
 
@@ -1,3 +1,3 @@
1
1
  module Rubysh
2
- VERSION = '0.0.3'
2
+ VERSION = '0.0.4'
3
3
  end
data/lib/rubysh.rb CHANGED
@@ -89,9 +89,14 @@ end
89
89
 
90
90
  module Rubysh
91
91
  # Convenience methods
92
- def self.run(command)
93
- command = Rubysh::Command.new(command) unless command.kind_of?(Rubysh::Command)
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.3
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: 2012-10-17 00:00:00.000000000 Z
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.24
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