rubysh 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +88 -0
- data/Rakefile +10 -0
- data/lib/rubysh/base_command.rb +65 -0
- data/lib/rubysh/base_directive.rb +24 -0
- data/lib/rubysh/command.rb +102 -0
- data/lib/rubysh/error.rb +20 -0
- data/lib/rubysh/fd.rb +43 -0
- data/lib/rubysh/pipe.rb +4 -0
- data/lib/rubysh/pipeline.rb +70 -0
- data/lib/rubysh/redirect.rb +181 -0
- data/lib/rubysh/runner.rb +156 -0
- data/lib/rubysh/subprocess/parallel_io.rb +184 -0
- data/lib/rubysh/subprocess/pipe_wrapper.rb +61 -0
- data/lib/rubysh/subprocess.rb +154 -0
- data/lib/rubysh/triple_less_than.rb +65 -0
- data/lib/rubysh/util.rb +55 -0
- data/lib/rubysh/version.rb +3 -0
- data/lib/rubysh.rb +149 -0
- data/rubysh.gemspec +26 -0
- data/test/_lib.rb +25 -0
- data/test/functional/_lib.rb +7 -0
- data/test/functional/lib/fd-lister +2 -0
- data/test/functional/lib/leaked_fds.rb +83 -0
- data/test/functional/lib/redirect_ordering.rb +15 -0
- data/test/functional/lib/triple_less_than.rb +16 -0
- data/test/integration/_lib.rb +7 -0
- data/test/integration/lib/rubysh.rb +6 -0
- data/test/rubysh +47 -0
- data/test/unit/_lib.rb +7 -0
- data/test/unit/lib/rubysh/command.rb +20 -0
- data/test/unit/lib/rubysh/pipeline.rb +108 -0
- data/test/unit/lib/rubysh/redirect.rb +44 -0
- data/test/unit/lib/rubysh/runner.rb +16 -0
- data/test/unit/lib/rubysh/subprocess/parallel_io.rb +233 -0
- data/test/unit/lib/rubysh/subprocess.rb +37 -0
- data/test/unit/lib/rubysh.rb +74 -0
- metadata +149 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Greg Brockman
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# Rubysh
|
2
|
+
|
3
|
+
Rubysh: Ruby subprocesses made easy
|
4
|
+
|
5
|
+
Rubysh makes shelling out easy with a __sh__-like syntax layer for Ruby:
|
6
|
+
|
7
|
+
irb -r rubysh
|
8
|
+
>> command = Rubysh('echo', 'hello-from-Rubysh') | Rubysh('grep', '--color', 'Rubysh')
|
9
|
+
>> command.run
|
10
|
+
hello-from-Rubysh
|
11
|
+
=> Rubysh::Runner: echo hello-from-Rubysh | grep --color Rubysh (exitstatus: 0)
|
12
|
+
|
13
|
+
Rubysh philosophy is to make simple tasks simple and complex tasks
|
14
|
+
possible.
|
15
|
+
|
16
|
+
## Motivation
|
17
|
+
|
18
|
+
Existing Ruby shell libaries make it very difficult to do tasks that
|
19
|
+
are simple in __sh__, such as:
|
20
|
+
|
21
|
+
- piping the output from one program to another
|
22
|
+
- redirecting a program's output to a file
|
23
|
+
- use a pre-tokenized array of arguments
|
24
|
+
|
25
|
+
Rubysh tries to emulate __sh__'s interface and semantics as closely as
|
26
|
+
possible.
|
27
|
+
|
28
|
+
## Features
|
29
|
+
|
30
|
+
Redirecting a file descriptor to a file:
|
31
|
+
|
32
|
+
# echo hello-from-Rubysh >/tmp/file.txt
|
33
|
+
Rubysh('echo', 'hello-from-Rubysh', Rubysh.stdout > '/tmp/file.txt')
|
34
|
+
Rubysh('echo', 'hello-from-Rubysh', Rubysh::FD(1) > '/tmp/file.txt')
|
35
|
+
|
36
|
+
Redirecting a file descriptor to another file descriptor:
|
37
|
+
|
38
|
+
# echo hello-from-Rubysh 2>&1
|
39
|
+
Rubysh('echo', 'hello-from-Rubysh', Rubysh.stderr > Rubysh.stdout)
|
40
|
+
|
41
|
+
Feeding standard input with a string literal:
|
42
|
+
|
43
|
+
# cat <<< "hello there"
|
44
|
+
Rubysh('cat', Rubysh.<<< 'hello there')
|
45
|
+
|
46
|
+
Rubysh has been written to work with arbitrary file descriptors, so
|
47
|
+
you can do the same advanced FD redirection magic you can in __sh__:
|
48
|
+
|
49
|
+
# cat 3<<< "hello there" <&3
|
50
|
+
Rubysh('cat', Rubysh::FD(3).<<< 'hello there', Rubysh.stdin < Rubysh::FD(3))
|
51
|
+
|
52
|
+
You can also capture output to a named target (here :stdout, :stderr
|
53
|
+
are arbitrary symbols):
|
54
|
+
|
55
|
+
command = Rubysh('echo', 'hi', Rubysh.stdout > :stdout, Rubysh.stderr > :stderr)
|
56
|
+
runner = command.run
|
57
|
+
runner.data(:stdout) # "hi\n"
|
58
|
+
runner.data(:stderr) # ""
|
59
|
+
|
60
|
+
Support for controlled input isn't quite ready, but the syntax will be
|
61
|
+
similar to the above. I want to support interactivity (so being able
|
62
|
+
to write data, read some data, and then write more data), and haven't
|
63
|
+
quite decided on the right API for this yet.
|
64
|
+
|
65
|
+
## API
|
66
|
+
|
67
|
+
The Rubysh helper function produces instances of __BaseCommand__. You
|
68
|
+
can run __run__ on these to spawn a subprocess and then __wait__ for
|
69
|
+
it to complete. Alternatively, you can do:
|
70
|
+
|
71
|
+
command = Rubysh('ls')
|
72
|
+
runner = command.run_async
|
73
|
+
runner.wait
|
74
|
+
|
75
|
+
## Installation
|
76
|
+
|
77
|
+
Rubysh is hosted on Rubygems. You can install by adding this line to
|
78
|
+
your application's Gemfile:
|
79
|
+
|
80
|
+
gem 'rubysh'
|
81
|
+
|
82
|
+
Or by installing directly via
|
83
|
+
|
84
|
+
$ gem install rubysh
|
85
|
+
|
86
|
+
## Contributing
|
87
|
+
|
88
|
+
Patches welcome! I'm happy to merge pull requests.
|
data/Rakefile
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
module Rubysh
|
4
|
+
# TODO:
|
5
|
+
#
|
6
|
+
# - freeze after initialize?
|
7
|
+
class BaseCommand
|
8
|
+
def stringify_arg(arg)
|
9
|
+
case arg
|
10
|
+
when BaseCommand, BaseDirective
|
11
|
+
arg.stringify
|
12
|
+
else
|
13
|
+
Shellwords.shellescape(arg.to_s)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
"Command: #{stringify}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_runner
|
26
|
+
Runner.new(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
create_runner.run
|
31
|
+
end
|
32
|
+
|
33
|
+
def run_async
|
34
|
+
create_runner.run_async
|
35
|
+
end
|
36
|
+
|
37
|
+
def |(other)
|
38
|
+
raise NotImplementedError.new("Override in subclass")
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(args)
|
42
|
+
raise NotImplementedError.new("Override in subclass")
|
43
|
+
end
|
44
|
+
|
45
|
+
def start_async(runner)
|
46
|
+
raise NotImplementedError.new("Override in subclass")
|
47
|
+
end
|
48
|
+
|
49
|
+
def wait(runner)
|
50
|
+
raise NotImplementedError.new("Override in subclass")
|
51
|
+
end
|
52
|
+
|
53
|
+
def stringify
|
54
|
+
raise NotImplementedError.new("Override in subclass")
|
55
|
+
end
|
56
|
+
|
57
|
+
def stdout=(value)
|
58
|
+
raise NotImplementedError.new("Override in subclass")
|
59
|
+
end
|
60
|
+
|
61
|
+
def stdin=(value)
|
62
|
+
raise NotImplementedError.new("Override in subclass")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Rubysh
|
2
|
+
class BaseDirective
|
3
|
+
def stringify
|
4
|
+
raise NotImplementedError.new("Override in subclass")
|
5
|
+
end
|
6
|
+
|
7
|
+
def prepare!(runner)
|
8
|
+
raise NotImplementedError.new("Override in subclass")
|
9
|
+
end
|
10
|
+
|
11
|
+
def apply_parent!(runner)
|
12
|
+
raise NotImplementedError.new("Override in subclass")
|
13
|
+
end
|
14
|
+
|
15
|
+
def apply!(runner)
|
16
|
+
raise NotImplementedError.new("Override in subclass")
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO: DRY up?
|
20
|
+
def state(runner)
|
21
|
+
runner.state(self)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Rubysh
|
2
|
+
class Command < BaseCommand
|
3
|
+
attr_accessor :raw_args, :directives, :args
|
4
|
+
|
5
|
+
def initialize(args)
|
6
|
+
@raw_args = args
|
7
|
+
@directives = []
|
8
|
+
@args = nil
|
9
|
+
|
10
|
+
process_args
|
11
|
+
end
|
12
|
+
|
13
|
+
def process_args
|
14
|
+
@args = @raw_args.map do |arg|
|
15
|
+
case arg
|
16
|
+
when BaseCommand
|
17
|
+
raise NotImplementedError.new('Not ready for subshells yet')
|
18
|
+
when BaseDirective
|
19
|
+
@directives << arg
|
20
|
+
nil
|
21
|
+
else
|
22
|
+
arg.to_s
|
23
|
+
end
|
24
|
+
end.compact
|
25
|
+
end
|
26
|
+
|
27
|
+
def stringify
|
28
|
+
@raw_args.map do |arg|
|
29
|
+
stringify_arg(arg)
|
30
|
+
end.join(' ')
|
31
|
+
end
|
32
|
+
|
33
|
+
def |(other)
|
34
|
+
Pipeline.new([self, other])
|
35
|
+
end
|
36
|
+
|
37
|
+
def post_fork(runner, &blk)
|
38
|
+
extra_post_forks(runner) << blk
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_stdout(runner, value)
|
42
|
+
directive = FD.new(:stdout) > value
|
43
|
+
add_directive(runner, directive)
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_stdin(runner, value)
|
47
|
+
directive = FD.new(:stdin) < value
|
48
|
+
add_directive(runner, directive)
|
49
|
+
end
|
50
|
+
|
51
|
+
def status(runner)
|
52
|
+
state(runner)[:subprocess].status
|
53
|
+
end
|
54
|
+
|
55
|
+
def pid(runner)
|
56
|
+
state(runner)[:subprocess].pid
|
57
|
+
end
|
58
|
+
|
59
|
+
def prepare!(runner)
|
60
|
+
@directives.each {|directive| directive.prepare!(runner)}
|
61
|
+
end
|
62
|
+
|
63
|
+
def start_async(runner)
|
64
|
+
# Need to call this *after* we've set up pipeline
|
65
|
+
# PipeWrappers. Would prefer to call it in prepare!, but then
|
66
|
+
# we'd have to take care of closing the FDs in the parent
|
67
|
+
# process here anyway.
|
68
|
+
prepare_subprocess(runner)
|
69
|
+
state(runner)[:subprocess].run
|
70
|
+
end
|
71
|
+
|
72
|
+
def wait(runner)
|
73
|
+
state(runner)[:subprocess].wait
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def add_directive(runner, directive)
|
79
|
+
extra_directives(runner) << directive
|
80
|
+
end
|
81
|
+
|
82
|
+
def state(runner)
|
83
|
+
runner.state(self)
|
84
|
+
end
|
85
|
+
|
86
|
+
def extra_directives(runner)
|
87
|
+
state(runner)[:extra_directives] ||= []
|
88
|
+
end
|
89
|
+
|
90
|
+
def extra_post_forks(runner)
|
91
|
+
state(runner)[:extra_post_forks] ||= []
|
92
|
+
end
|
93
|
+
|
94
|
+
def prepare_subprocess(runner)
|
95
|
+
# extras first because they are currently only used for
|
96
|
+
# pipeline, which should not win out over internal redirects.
|
97
|
+
directives = extra_directives(runner) + @directives
|
98
|
+
post_forks = extra_post_forks(runner)
|
99
|
+
state(runner)[:subprocess] = Subprocess.new(args, directives, post_forks, runner)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/rubysh/error.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rubysh
|
2
|
+
module Error
|
3
|
+
class BaseError < Exception; end
|
4
|
+
|
5
|
+
class ExecError < BaseError
|
6
|
+
# Exception klass and caller from the child process
|
7
|
+
attr_accessor :klass, :caller
|
8
|
+
|
9
|
+
def initialize(message, klass, caller)
|
10
|
+
super(message)
|
11
|
+
@klass = klass
|
12
|
+
@caller = caller
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class UnreachableError < BaseError; end
|
17
|
+
class AlreadyClosedError < BaseError; end
|
18
|
+
class AlreadyRunError < BaseError; end
|
19
|
+
end
|
20
|
+
end
|
data/lib/rubysh/fd.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Rubysh
|
2
|
+
class FD
|
3
|
+
attr_accessor :fileno
|
4
|
+
|
5
|
+
def initialize(fileno)
|
6
|
+
case fileno
|
7
|
+
when Integer
|
8
|
+
# pass
|
9
|
+
when :stdin
|
10
|
+
fileno = 0
|
11
|
+
when :stdout
|
12
|
+
fileno = 1
|
13
|
+
when :stderr
|
14
|
+
fileno = 2
|
15
|
+
else
|
16
|
+
raise Rubysh::Error::BaseError.new("Fileno must be an integer or one of :stdin, :stdout, :stderr, not #{fileno.inspect}")
|
17
|
+
end
|
18
|
+
|
19
|
+
@fileno = fileno
|
20
|
+
end
|
21
|
+
|
22
|
+
def >(target)
|
23
|
+
Redirect.new(self, '>', target)
|
24
|
+
end
|
25
|
+
|
26
|
+
def <(target)
|
27
|
+
Redirect.new(self, '<', target)
|
28
|
+
end
|
29
|
+
|
30
|
+
def <<
|
31
|
+
Rubysh.<<(self)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
"FD: #{@fileno}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def ==(other)
|
39
|
+
self.class == other.class &&
|
40
|
+
self.fileno == other.fileno
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/rubysh/pipe.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Rubysh
|
2
|
+
class Pipeline < BaseCommand
|
3
|
+
attr_accessor :pipeline
|
4
|
+
|
5
|
+
def initialize(pipeline)
|
6
|
+
raise Rubysh::Error::BaseError.new("Cannot create an empty pipeline") if pipeline.length == 0
|
7
|
+
@pipeline = pipeline
|
8
|
+
end
|
9
|
+
|
10
|
+
# sh semantics are that your exitstatus is that of the last in the
|
11
|
+
# pipeline
|
12
|
+
def status(runner)
|
13
|
+
@pipeline[-1].status(runner)
|
14
|
+
end
|
15
|
+
|
16
|
+
def pid(runner)
|
17
|
+
@pipeline[-1].pid(runner)
|
18
|
+
end
|
19
|
+
|
20
|
+
def prepare!(runner)
|
21
|
+
@pipeline.each {|command| command.prepare!(runner)}
|
22
|
+
end
|
23
|
+
|
24
|
+
def pipeline_pairs
|
25
|
+
@pipeline[0...-1].zip(@pipeline[1..-1])
|
26
|
+
end
|
27
|
+
|
28
|
+
def stringify
|
29
|
+
@pipeline.map {|cmd| cmd.stringify}.join(' | ')
|
30
|
+
end
|
31
|
+
|
32
|
+
def |(other)
|
33
|
+
self.class.new(pipeline + [other])
|
34
|
+
end
|
35
|
+
|
36
|
+
def start_async(runner)
|
37
|
+
last_pipe = nil
|
38
|
+
|
39
|
+
pipeline_pairs.each do |left, right|
|
40
|
+
# TODO: maybe create an object to represent the pipe
|
41
|
+
# relationship, instead of manually assembling here.
|
42
|
+
#
|
43
|
+
# Don't want to have more than 2 pipes open at a time, so need
|
44
|
+
# to #run_async and #close here.
|
45
|
+
pipe = Subprocess::PipeWrapper.new
|
46
|
+
setup_pipe(runner, pipe, left, right)
|
47
|
+
|
48
|
+
left.start_async(runner)
|
49
|
+
last_pipe.close if last_pipe
|
50
|
+
last_pipe = pipe
|
51
|
+
end
|
52
|
+
|
53
|
+
@pipeline[-1].start_async(runner)
|
54
|
+
last_pipe.close if last_pipe
|
55
|
+
end
|
56
|
+
|
57
|
+
def setup_pipe(runner, pipe, left, right)
|
58
|
+
left.set_stdout(runner, pipe.writer)
|
59
|
+
right.set_stdin(runner, pipe.reader)
|
60
|
+
end
|
61
|
+
|
62
|
+
def wait(runner)
|
63
|
+
# It's likely we should actually wait for these in parallel; I'm
|
64
|
+
# not really sure right now. Might be tricky to avoid waiting
|
65
|
+
# for other processes run by this program (could probably use
|
66
|
+
# process groups for that?)
|
67
|
+
@pipeline.each {|cmd| cmd.wait(runner)}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
module Rubysh
|
2
|
+
# Note that in bash, the semantics of redirection appear to be
|
3
|
+
# following (tested empirically, rather than reading a spec):
|
4
|
+
#
|
5
|
+
# - [a<&b] and [a>&b] mean the same thing: copy FD b to a
|
6
|
+
# (try 'echo test 3>/tmp/testing.txt 1<&3')
|
7
|
+
# - [a<&a] appears to be a no-op: ls /dev/fd 9<&9
|
8
|
+
# - If b != a is an invalid file descriptor, then [a>&b] throws an
|
9
|
+
# error.
|
10
|
+
# - Pathnames can only be on the right-hand side of a redirect.
|
11
|
+
class Redirect < BaseDirective
|
12
|
+
VALID_DIRECTIONS = ['<', '>', '>>']
|
13
|
+
|
14
|
+
attr_accessor :source, :direction, :target
|
15
|
+
|
16
|
+
def initialize(source, direction, target)
|
17
|
+
unless VALID_DIRECTIONS.include?(direction)
|
18
|
+
raise Rubysh::Error::BaseError.new("Direction must be one of #{VALID_DIRECTIONS.join(', ')}, not #{direction.inspect}")
|
19
|
+
end
|
20
|
+
|
21
|
+
unless source.kind_of?(IO) || source.kind_of?(FD) || source.kind_of?(Integer)
|
22
|
+
raise Rubysh::Error::BaseError.new("Invalid source: #{source.inspect}. Source must be an IO, a Rubysh::FD, or an Integer.")
|
23
|
+
end
|
24
|
+
|
25
|
+
unless target.respond_to?(:fileno) || target.kind_of?(Integer) || target.kind_of?(String) || target.kind_of?(Symbol)
|
26
|
+
raise Rubysh::Error::BaseError.new("Invalid target: #{target.inspect}. Target must respond to :fileno or be an Integer, a String, or a Symbol.")
|
27
|
+
end
|
28
|
+
|
29
|
+
@source = source
|
30
|
+
@target = target
|
31
|
+
@direction = direction
|
32
|
+
end
|
33
|
+
|
34
|
+
def printable_source
|
35
|
+
Util.to_fileno(source)
|
36
|
+
end
|
37
|
+
|
38
|
+
def printable_target
|
39
|
+
case target
|
40
|
+
when Symbol
|
41
|
+
target.inspect
|
42
|
+
else
|
43
|
+
Util.to_fileno(target)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# TODO: support files
|
48
|
+
def stringify
|
49
|
+
source_file = printable_source
|
50
|
+
target_file = printable_target
|
51
|
+
|
52
|
+
case direction
|
53
|
+
when '<', '>>'
|
54
|
+
source_file = nil if source_file == 0
|
55
|
+
when '>'
|
56
|
+
source_file = nil if source_file == 1
|
57
|
+
else
|
58
|
+
raise Rubysh::Error::BaseError.new("Unrecognized direction: #{direction.inspect}")
|
59
|
+
end
|
60
|
+
|
61
|
+
ampersand = target_file.kind_of?(Integer) ? '&' : nil
|
62
|
+
|
63
|
+
"#{source_file}#{direction}#{ampersand}#{target_file}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
"Redirect: #{stringify}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def ==(other)
|
71
|
+
self.class == other.class &&
|
72
|
+
self.printable_source == other.printable_source &&
|
73
|
+
self.printable_target == other.printable_target
|
74
|
+
end
|
75
|
+
|
76
|
+
def reading?
|
77
|
+
direction == '<'
|
78
|
+
end
|
79
|
+
|
80
|
+
def target_reading?
|
81
|
+
!reading?
|
82
|
+
end
|
83
|
+
|
84
|
+
def writing?
|
85
|
+
!reading?
|
86
|
+
end
|
87
|
+
|
88
|
+
def target_writing?
|
89
|
+
!writing?
|
90
|
+
end
|
91
|
+
|
92
|
+
def truncate?
|
93
|
+
direction == '>'
|
94
|
+
end
|
95
|
+
|
96
|
+
def named_target?
|
97
|
+
target.kind_of?(Symbol)
|
98
|
+
end
|
99
|
+
|
100
|
+
def target_name
|
101
|
+
raise Rubysh::Error::BaseError.new("Not a named target") unless named_target?
|
102
|
+
target
|
103
|
+
end
|
104
|
+
|
105
|
+
def prepare!(runner)
|
106
|
+
prepare_target(runner)
|
107
|
+
end
|
108
|
+
|
109
|
+
def prepare_target(runner)
|
110
|
+
return unless named_target?
|
111
|
+
targets = runner.targets
|
112
|
+
if targets.include?(target_name)
|
113
|
+
raise Rubysh::Error::BaseError.new("#{runner} already has a named target: #{target_name.inspect}")
|
114
|
+
end
|
115
|
+
|
116
|
+
pipe = Subprocess::PipeWrapper.new
|
117
|
+
|
118
|
+
targets[target_name] = {
|
119
|
+
:target_reading? => target_reading?,
|
120
|
+
:target => target_reading? ? pipe.reader : pipe.writer,
|
121
|
+
:complement => target_reading? ? pipe.writer : pipe.reader,
|
122
|
+
:buffer => []
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
# E.g. Rubysh.stdin < :stdin
|
127
|
+
def apply_parent!(runner)
|
128
|
+
return unless named_target?
|
129
|
+
target_state = runner.target_state(target_name)
|
130
|
+
target_state[:complement].close
|
131
|
+
end
|
132
|
+
|
133
|
+
def apply!(runner)
|
134
|
+
Rubysh.log.info("About to apply #{self} for #{$$}")
|
135
|
+
|
136
|
+
# Open the target
|
137
|
+
target_io = file_as_io(runner, target)
|
138
|
+
|
139
|
+
target_fd = Util.to_fileno(target_io)
|
140
|
+
source_fd = Util.to_fileno(source)
|
141
|
+
|
142
|
+
# Copy target -> source
|
143
|
+
Util.dup2(target_fd, source_fd)
|
144
|
+
Util.set_cloexec(source_fd, false)
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
# If providing your own open FD, you have to set cloexec yourself.
|
150
|
+
def file_as_io(runner, file, default_to_cloexec=true)
|
151
|
+
return file if file.kind_of?(IO)
|
152
|
+
# If it's an FD, canonicalize to the FD number
|
153
|
+
file = Util.to_fileno(file)
|
154
|
+
|
155
|
+
if file.kind_of?(Integer)
|
156
|
+
io = Util.io_without_autoclose(file)
|
157
|
+
# Someone else opened
|
158
|
+
default_to_cloexec = false
|
159
|
+
elsif file.kind_of?(String) && reading?
|
160
|
+
io = File.open(file)
|
161
|
+
elsif file.kind_of?(String) && writing? && truncate?
|
162
|
+
# Make the following cases explicit for future compatability
|
163
|
+
# (also to make it clear on an exception which case is at
|
164
|
+
# fault).
|
165
|
+
io = File.open(file, 'w')
|
166
|
+
elsif file.kind_of?(String) && writing? && !truncate?
|
167
|
+
io = File.open(file, 'a')
|
168
|
+
elsif file.kind_of?(Symbol)
|
169
|
+
target_state = runner.target_state(file)
|
170
|
+
io = target_state[:complement]
|
171
|
+
# Someone else opened
|
172
|
+
default_to_cloexec = false
|
173
|
+
else
|
174
|
+
raise Rubysh::Error::BaseError.new("Unrecognized file spec: #{file.inspect}")
|
175
|
+
end
|
176
|
+
|
177
|
+
io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if default_to_cloexec
|
178
|
+
io
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|