rubysh 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ === 0.3.0 2014-05-25
2
+
3
+ * Add block execution support to Rubysh. De-support old semantics here
4
+ providing a block was a wrapper for generating an argument array.
data/README.md CHANGED
@@ -58,6 +58,12 @@ are arbitrary symbols):
58
58
  runner.read(:stdout) # "hi\n"
59
59
  runner.read(:stderr) # ""
60
60
 
61
+ You can also have your child run a Ruby block rather than execute a command:
62
+
63
+ command = Rubysh(Rubysh.stdout > :stdout) {puts "hi from child"}
64
+ runner = command.run
65
+ runner.read(:stdout) # "hi from child\n"
66
+
61
67
  ## Controlled input
62
68
 
63
69
  You can easily read and write data interactively:
@@ -67,9 +73,26 @@ You can easily read and write data interactively:
67
73
  >> runner.read(:how => :partial) # block until some output available
68
74
  => ".\n.\n.\n.\n.\n.\n.\n.\n.\n"
69
75
  >> runner.read(:how => :nonblock)
70
- => ""
76
+ => nil
71
77
  >> runner.read # block until all output available
72
- => [truncated]
78
+ => ".\n[...]"
79
+
80
+ ## Reactive output
81
+
82
+ You can also receive real-time notifications as data becomes available:
83
+
84
+ >> runner = Rubysh(
85
+ 'examples/on_read_example.sh',
86
+ Rubysh.stdout > :stdout, Rubysh.stderr > :stderr,
87
+ on_read: Proc.new {|target, data| puts "[#{target}]: #{data}"}
88
+ )
89
+ => Command: examples/on_read_example.sh >:stdout 2>:stderr {:on_read=>#<Proc:0x007f8ad3bc5790@(irb):4>}
90
+ >> runner.run
91
+ [stdout]: [1] Hello from stdout
92
+ [stderr]: [1] Hello from stderr
93
+ [stdout]: [2] Hello from stdout
94
+ [stderr]: [2] Hello from stderr
95
+ [...]
73
96
 
74
97
  ## API
75
98
 
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ for i in {1..10}; do
4
+ echo "[$i] Hello from stdout"
5
+ echo >&2 "[$i] Hello from stderr"
6
+ sleep 1
7
+ done
@@ -62,23 +62,12 @@ require 'rubysh/util'
62
62
  # Rubysh('ls', '/tmp', Rubysh.&)
63
63
  # => Command: ls /tmp &
64
64
 
65
- # Either create a new Rubysh command:
65
+ # You can then create a new Rubysh command:
66
66
  #
67
67
  # command = Rubysh('ls')
68
68
  # command.run
69
- #
70
- # Or use the block syntax to create and run one:
71
- #
72
- # Rubysh {'ls'}
73
69
  def Rubysh(*args, &blk)
74
- if blk
75
- raise Rubysh::Error::BaseError.new("Can't provide arguments and a block") if args.length > 0
76
- command = blk.call
77
- command = Rubysh::Command.new(command) unless command.kind_of?(Rubysh::Command)
78
- command.run
79
- else
80
- Rubysh::Command.new(args)
81
- end
70
+ Rubysh::Command.new(args, &blk)
82
71
  end
83
72
 
84
73
  def AliasRubysh(name)
@@ -90,22 +79,22 @@ end
90
79
  module Rubysh
91
80
  # Convenience methods
92
81
  def self.run(*args, &blk)
93
- command = Rubysh::Command.new(args)
94
- command.run(&blk)
82
+ command = Rubysh::Command.new(args, &blk)
83
+ command.run
95
84
  end
96
85
 
97
86
  def self.run_async(*args, &blk)
98
- command = Rubysh::Command.new(args)
99
- command.run_async(&blk)
87
+ command = Rubysh::Command.new(args, &blk)
88
+ command.run_async
100
89
  end
101
90
 
102
91
  def self.check_call(*args, &blk)
103
- command = Rubysh::Command.new(args)
104
- command.check_call(&blk)
92
+ command = Rubysh::Command.new(args, &blk)
93
+ command.check_call
105
94
  end
106
95
 
107
- def self.Command(*args)
108
- Command.new(*args)
96
+ def self.Command(*args, &blk)
97
+ Command.new(*args, &blk)
109
98
  end
110
99
 
111
100
  def self.Pipeline(*args)
@@ -1,16 +1,17 @@
1
1
  module Rubysh
2
2
  class Command < BaseCommand
3
- attr_accessor :raw_args, :directives, :args
3
+ attr_accessor :raw_args, :directives, :args, :blk
4
4
 
5
- def initialize(args)
5
+ def initialize(args, &blk)
6
6
  if args.length == 1 && args[0].kind_of?(Array)
7
- raise "It looks like you created a Rubysh::Command with a singleton nested array: #{args.inspect}. That'll never be runnable, and probably indicates you forgot a splat somewhere."
7
+ raise Rubysh::Error::BaseError.new("It looks like you created a Rubysh::Command with a singleton nested array: #{args.inspect}. That'll never be runnable, and probably indicates you forgot a splat somewhere.")
8
8
  end
9
9
 
10
10
  @raw_args = args
11
11
  @directives = []
12
12
  @args = nil
13
13
  @opts = {}
14
+ @blk = blk
14
15
 
15
16
  process_args
16
17
  end
@@ -32,6 +33,10 @@ module Rubysh
32
33
  arg.to_s
33
34
  end
34
35
  end.compact
36
+
37
+ if @args.length > 0 && @blk
38
+ raise Rubysh::Error::BaseError.new("You can't provide command-line arguments and a block at the same time.")
39
+ end
35
40
  end
36
41
 
37
42
  def stringify
@@ -123,7 +128,7 @@ module Rubysh
123
128
  # pipeline, which should not win out over internal redirects.
124
129
  directives = extra_directives(runner) + @directives
125
130
  post_forks = base_post_forks + extra_post_forks(runner)
126
- state(runner)[:subprocess] = Subprocess.new(args, directives, post_forks, runner)
131
+ state(runner)[:subprocess] = Subprocess.new(args, blk, directives, post_forks, runner)
127
132
  end
128
133
  end
129
134
  end
@@ -14,11 +14,18 @@ module Rubysh
14
14
 
15
15
  # TODO: switch directives over to an OrderedHash of some form? Really
16
16
  # want to preserve the semantics here.
17
- def initialize(args, directives=[], post_fork=[], runner=nil)
17
+ def initialize(args, blk=nil, directives=[], post_fork=[], runner=nil)
18
18
  raise ArgumentError.new("Must provide an array (#{args.inspect} provided)") unless args.kind_of?(Array)
19
- raise ArgumentError.new("No command specified (#{args.inspect} provided)") unless args.length > 0
19
+
20
+ if args.length > 0 && blk
21
+ raise ArgumentError.new("Provided both arguments (#{args.inspect}) and a block (#{blk.inspect}). You can only provide one.")
22
+ elsif args.length == 0 && !blk
23
+ raise ArgumentError.new("No command specified (#{args.inspect} provided)")
24
+ end
25
+
20
26
  @command = args[0]
21
27
  @args = args[1..-1]
28
+ @blk = blk
22
29
  @directives = directives
23
30
  @runner = runner
24
31
 
@@ -35,7 +42,7 @@ module Rubysh
35
42
  end
36
43
 
37
44
  def to_s
38
- "Subprocess: command=#{@command.inspect} args=#{@args.inspect} directives: #{@directives.inspect}"
45
+ "Subprocess: command=#{@command.inspect} blk=#{@blk.inspect} args=#{@args.inspect} directives: #{@directives.inspect}"
39
46
  end
40
47
 
41
48
  def run
@@ -93,7 +100,12 @@ module Rubysh
93
100
  @exec_status.write_only
94
101
  run_post_fork
95
102
  apply_directives_child
96
- exec_program
103
+
104
+ if @blk
105
+ run_blk
106
+ else
107
+ exec_program
108
+ end
97
109
  end
98
110
 
99
111
  def run_post_fork
@@ -120,6 +132,21 @@ module Rubysh
120
132
  end
121
133
  end
122
134
 
135
+ def run_blk
136
+ # Close the writing end of the pipe
137
+ @exec_status.read_only
138
+
139
+ begin
140
+ # Run the actual block
141
+ @blk.call
142
+ rescue Exception => e
143
+ render_exception(e)
144
+ hard_exit(e)
145
+ else
146
+ hard_exit(nil)
147
+ end
148
+ end
149
+
123
150
  def exec_program
124
151
  begin
125
152
  Kernel.exec([command, command], *args)
@@ -131,9 +158,9 @@ module Rubysh
131
158
  'caller' => e.send(:caller)
132
159
  }
133
160
  @exec_status.dump_json_and_close(msg)
134
- # Note: atexit handlers will fire in this case. May want to do
135
- # something about that.
136
- exit(1)
161
+ # Abort without running at_exit handlers or giving the user a
162
+ # chance to accidentally catch the exit.
163
+ hard_exit(e)
137
164
  else
138
165
  raise Rubysh::Error::UnreachableError.new("This code should be unreachable. If you are seeing this exception, it means someone overrode Kernel.exec. That's not very nice of them.")
139
166
  end
@@ -153,5 +180,17 @@ module Rubysh
153
180
 
154
181
  raise @exec_error if @exec_error
155
182
  end
183
+
184
+ def render_exception(e)
185
+ $stderr.print("[Rubysh subprocess #{$$}] #{e.class}: #{e.message}\n\t")
186
+ $stderr.print(e.backtrace.join("\n\t"))
187
+ $stderr.print("\n")
188
+ end
189
+
190
+ # Broken out for the tests
191
+ def hard_exit(exception)
192
+ status = exception ? 1 : 0
193
+ exit!(status)
194
+ end
156
195
  end
157
196
  end
@@ -1,3 +1,3 @@
1
1
  module Rubysh
2
- VERSION = '0.2.4'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -0,0 +1,26 @@
1
+ require File.expand_path('../_lib', File.dirname(__FILE__))
2
+
3
+ module RubyshTest::Functional
4
+ class EnvTest < FunctionalTest
5
+ describe 'when executing a block' do
6
+ it 'runs the expected code' do
7
+ result = Rubysh.run(Rubysh.stdout > :stdout) {puts "hi"}
8
+
9
+ assert_equal(0, result.exitstatus)
10
+ output = result.read(:stdout)
11
+ assert_equal("hi\n", output)
12
+ end
13
+
14
+ it 'hard exits from the subprocess' do
15
+ Rubysh::Subprocess.any_instance.stubs(:render_exception)
16
+
17
+ begin
18
+ result = Rubysh.run {raise "this should in fact get printed"}
19
+ rescue Exception
20
+ end
21
+
22
+ assert_equal(1, result.exitstatus)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -2,10 +2,19 @@ require File.expand_path('../../_lib', File.dirname(__FILE__))
2
2
 
3
3
  module RubyshTest::Unit
4
4
  class SubprocessTest < UnitTest
5
+ before do
6
+ Rubysh::Subprocess.any_instance.stubs(:hard_exit).with do |e|
7
+ raise e if e
8
+ true
9
+ end
10
+ end
11
+
5
12
  describe 'when running a command' do
6
13
  it 'calls exec with the expected arguments' do
7
14
  read_fd, write_fd = stub_pipe
8
- Kernel.expects(:exec).with(['cmd', 'cmd'], 'arg1', 'arg2')
15
+ Kernel.expects(:exec).with do |*args|
16
+ assert_equal([['cmd', 'cmd'], 'arg1', 'arg2'], args)
17
+ end
9
18
 
10
19
  proc = Rubysh::Subprocess.new(['cmd', 'arg1', 'arg2'])
11
20
  proc.send(:open_exec_status)
@@ -19,13 +28,15 @@ module RubyshTest::Unit
19
28
  runner = mock
20
29
 
21
30
  read_fd, write_fd = stub_pipe
22
- Kernel.expects(:exec).with(['cmd', 'cmd'], 'arg1', 'arg2')
31
+ Kernel.expects(:exec).with do |*args|
32
+ assert_equal([['cmd', 'cmd'], 'arg1', 'arg2'], args)
33
+ end
23
34
 
24
35
  redirect = Rubysh::Redirect.new(2, '>', 1)
25
36
  redirect.expects(:apply!).with(runner)
26
37
 
27
38
  proc = Rubysh::Subprocess.new(['cmd', 'arg1', 'arg2'],
28
- [redirect], [], runner)
39
+ nil, [redirect], [], runner)
29
40
  proc.send(:open_exec_status)
30
41
  assert_raises(Rubysh::Error::UnreachableError) do
31
42
  proc.send(:do_run_child)
@@ -33,5 +44,18 @@ module RubyshTest::Unit
33
44
  end
34
45
  end
35
46
  end
47
+
48
+ describe 'with a block' do
49
+ it 'executes the block and then calls exit!' do
50
+ called = false
51
+ blk = Proc.new {called = true}
52
+
53
+ proc = Rubysh::Subprocess.new([], blk)
54
+ proc.send(:open_exec_status)
55
+ proc.send(:do_run_child)
56
+
57
+ assert(called, "Did not call the block")
58
+ end
59
+ end
36
60
  end
37
61
  end
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.2.4
4
+ version: 0.3.0
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: 2014-01-12 00:00:00.000000000 Z
12
+ date: 2014-05-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -84,10 +84,12 @@ extra_rdoc_files: []
84
84
  files:
85
85
  - .gitignore
86
86
  - Gemfile
87
+ - History.txt
87
88
  - LICENSE
88
89
  - README.md
89
90
  - Rakefile
90
91
  - examples/dots.sh
92
+ - examples/on_read_example.sh
91
93
  - examples/self_tee_example.rb
92
94
  - lib/rubysh.rb
93
95
  - lib/rubysh/base_command.rb
@@ -111,6 +113,7 @@ files:
111
113
  - test/_lib.rb
112
114
  - test/functional/_lib.rb
113
115
  - test/functional/lib/env.rb
116
+ - test/functional/lib/execute_block.rb
114
117
  - test/functional/lib/fd-lister
115
118
  - test/functional/lib/kill.rb
116
119
  - test/functional/lib/leaked_fds.rb
@@ -143,7 +146,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
143
146
  version: '0'
144
147
  segments:
145
148
  - 0
146
- hash: -1239245545634239752
149
+ hash: 3769802053247508870
147
150
  required_rubygems_version: !ruby/object:Gem::Requirement
148
151
  none: false
149
152
  requirements:
@@ -152,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
155
  version: '0'
153
156
  segments:
154
157
  - 0
155
- hash: -1239245545634239752
158
+ hash: 3769802053247508870
156
159
  requirements: []
157
160
  rubyforge_project:
158
161
  rubygems_version: 1.8.23
@@ -166,6 +169,7 @@ test_files:
166
169
  - test/_lib.rb
167
170
  - test/functional/_lib.rb
168
171
  - test/functional/lib/env.rb
172
+ - test/functional/lib/execute_block.rb
169
173
  - test/functional/lib/fd-lister
170
174
  - test/functional/lib/kill.rb
171
175
  - test/functional/lib/leaked_fds.rb