I want to create a simple IO object that represents a pipe opened to another program to that I can periodically write to another program’s STDIN as my app runs. I want it to be bullet-proof (in that it catches all errors) and cross-platform. The best options I can find are:
open
sub io_read {
local $SIG{__WARN__} = sub { }; # Silence warning.
open my $pipe, '|-', @_ or die "Cannot exec $_[0]: $!\n";
return $pipe;
}
Advantages:
- Cross-platform
- Simple
Disadvantages
- No
$SIG{PIPE}to catch errors from the piped program - Are other errors caught?
IO::Pipe
sub io_read {
IO::Pipe->reader(@_);
}
Advantages:
- Simple
- Returns an IO::Handle object for OO interface
- Supported by the Perl core.
Disadvantages
- Still No
$SIG{PIPE}to catch errors from the piped program - Not supported on Win32 (or, at least, its tests are skipped)
IPC::Run
There is no interface for writing to a file handle in IPC::Run, only appending to a scalar. This seems…weird.
IPC::Run3
No file handle interface here, either. I could use a code reference, which would be called repeatedly to spool to the child, but looking at the source code, it appears that it actually writes to a temporary file, and then opens it and spools its contents to the pipe’d command’s STDIN. Wha?
IPC::Cmd
Still no file handle interface.
What am I missing here? It seems as if this should be a solved problem, and I’m kind of stunned that it’s not. IO::Pipe comes closest to what I want, but the lack of $SIG{PIPE} error handling and the lack of support for Windows is distressing. Where is the piping module that will JDWIM?
Thanks to guidance from @ikegami, I have found that the best choice for interactively reading from and writing to another process in Perl is IPC::Run. However, it requires that the program you are reading from and writing to have a known output when it is done writing to its STDOUT, such as a prompt. Here’s an example that executes
bash, has it runls -l, and then prints that output:Because it is running as an IPC (I assume),
bashdoes not emit a prompt when it is done writing to its STDOUT. So I use thenew_appender()function to have it emit something I can match to find the end of the output (by callingecho __END__). I’ve also used an anonymous subroutine after a call tonew_chunkerto collect the output into an array, rather than a scalar (just pass a reference to a scalar to'>'if you want that).So this works, but it sucks for a whole host of reasons, in my opinion:
__END__, though). If I was controlling a database client, I might have to send something likeSELECT 'IM OUTTA HERE';. Different applications would require differentnew_appenderhacks.$inand$outscalars feels weird and action-at-a-distance-y. I dislike it.new_chunkerto get line-oriented output is nice, if still a bit weird. That regains a bit of the efficiency on reading output from a program, though, assuming it is buffered efficiently by IPC::Run.I now realize that, although the interface for IPC::Run could potentially be a bit nicer, overall the weaknesses of the IPC model in particular makes it tricky to deal with at all. There is no generally-useful IPC interface, because one has to know too much about the specifics of the particular program being run to get it to work. This is okay, maybe, if you know exactly how it will react to inputs, and can reliably recognize when it is done emitting output, and don’t need to worry much about cross-platform compatibility. But that was far from sufficient for my need for a generally useful way to interact with various database command-line clients in a CPAN module that could be distributed to a whole host of operating systems.
In the end, thanks to packaging suggestions in comments on a blog post, I decided to abandon the use of IPC for controlling those clients, and to use the DBI, instead. It provides an excellent API, robust, stable, and mature, and suffers none of the drawbacks of IPC.
My recommendation for those who come after me is this: