rubysh 0.2.4 → 0.3.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.
@@ -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