rubysh 0.0.1
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/.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
|