Re: How to pass data as STDIN to Capture::Tiny
by kcott (Archbishop) on Jan 02, 2017 at 03:37 UTC
|
"The command can also accept arguments via STDIN which would avoid this security problem, but I haven't been able to figure out how to do this with Capture::Tiny. If I have the password as a variable in my script, how can I pass this into STDIN for external command using Capture::Tiny."
In the following code, I've used cat as an example external command.
This can read from STDIN.
It outputs whatever it reads in to STDOUT:
handy for this test; hopefully, your external command isn't echoing passwords.
#!/usr/bin/env perl
use strict;
use warnings;
use autodie qw{:all};
use Capture::Tiny qw{capture};
my ($stdout, $stderr) = capture \&run_external_command;
printf "STDOUT: %sSTDERR: %s\n", $stdout, $stderr;
sub run_external_command {
my $external_command = 'cat';
open my $cmd_pipe, '|-', $external_command;
print $cmd_pipe "password\n";
}
Output:
STDOUT: password
STDERR:
| [reply] [d/l] [select] |
|
|
Hi Ken,
Just to pick up on what the AM mentioned: properly closeing a piped open is important so one can catch all possible errors (as opposed to closeing regular files, where in my experience things go wrong much less often). I see you used autodie, but as far as I can tell it doesn't catch errors when the filehandle is implicitly closed when it goes out of scope.
In your code, if I replace the command with something that returns a nonzero exit code, and add the close at the end of the sub, I get confusing results: inside of capture, the error doesn't seem to get caught at all, and outside of capture, I get the confusing error message "Can't close(GLOB(0x8e2b62)) filehandle: '' at ...". (In fact, if I remember correctly, strange interactions with autodie and piped opens is one of the reasons I started avoiding autodie.)
So here's the same run_external_command code, without autodie but with the minimum error handling:
my $extcmd = 'cat';
open my $cmd_pipe, '|-', $extcmd or die "open $extcmd: $!";
print $cmd_pipe "password\n";
close $cmd_pipe or die "close $extcmd: ".($! ? $! : "\$?=$?");
However, nowadays I very much prefer to use more better-suited modules, one of my favorites is IPC::Run3. For the OP:
use IPC::Run3 'run3';
my $stdin = "password\n";
my @extcmd = ('cat');
run3 \@extcmd, \$stdin, \my $stdout, \my $stderr
or die "run3 failed";
$? and die "run3: \$?=$?";
print "stdout: <<$stdout>>\n";
print "stderr: <<$stderr>>\n";
Regards, -- Hauke D | [reply] [d/l] [select] |
|
|
close $cmd_pipe;
and I started to add some error checking.
However, realising that I had no idea what the OP's external command was
nor what error codes he might be looking for,
I ripped that all out and just posted the part answering the original question.
I probably should have left the close statement.
I don't know what autodie is doing under the hood;
however, given that
open my $cmd_pipe, '|-', $external_command;
performs a fork,
I suspect it's checking that (in its :threads category)
and not doing anything in the :system category.
The only mention of pipe that I can see in
autodie is that it's in the :ipc category.
There's also one instance in IPC::System::Simple
that I found somewhat humourous:
"Implementing the capture command involves dark and terrible magicks involving pipes, and one of them has sprung a leak."
[That's &IPC::System::Simple::capture,
not &Capture::Tiny::capture.]
"perlipc: Using open() for IPC"
has more information on error checking:
"Be careful to check the return values from both open() and close(). If you're writing to a pipe, you should also trap SIGPIPE. ..."
See also: "perlvar: Error Variables".
"However, nowadays I very much prefer to use more better-suited modules, ..."
Yes, Capture::Tiny wouldn't have been my first choice
for this task; however, the OP mentions it four times in his single-paragraph post, so that's what I used.
| [reply] [d/l] [select] |
|
|
|
|
This works, but you can't trust the exit value returned as the 3rd output value of capture and must instead inspect $?. Also, you must explicitly call close on the pipe.
| [reply] |
|
|
"This works, but you can't trust the exit value returned as the 3rd output value of capture and must instead inspect $?."
Well, the documentation for
Capture::Tiny v0.44
(the latest version, at the time of writing, and the version I used) states:
"The capture function takes a code reference and returns what is sent to STDOUT and STDERR as well as any return values from the code reference." [my emphasis]
I took this to be correct but maybe you have other information.
Please share.
I did a quick test.
The third value, when captured, did contain the return value of the code reference.
Could you provide some code showing this third value with something other than the return value of the code reference.
| [reply] [d/l] |
|
|
Hi,
but you can't trust the exit value returned as the 3rd output value of capture and must instead inspect $?
I'm guessing what you're comparing kcott's code to is this example from the Capture::Tiny doc:
($stdout, $stderr, $exit) = capture {
system( $cmd, @args );
};
In that code, the reason that you get $? stored into $exit is that system returns the same thing as $?, and capture returns the return value(s) from the block of code. Since in kcott's code the last statement in the block is print, its return value is what capture returns as the 3rd return value. If you wanted to use the above pattern of getting $? as the 3rd return value, just change kcott's code like this: sub run_external_command { ...; $? }.
Hope this helps, -- Hauke D
| [reply] [d/l] [select] |
|
|
use Capture::Tiny::Extended 'capture';
my ( $out, $err, $res ) = capture { system( 'ls' ) };
...it is unhealthy to remain near things that are in the process of blowing up. man page for WARP, by Larry Wall
| [reply] [d/l] |
|
|
Re: How to pass data as STDIN to Capture::Tiny
by Athanasius (Archbishop) on Jan 02, 2017 at 03:13 UTC
|
| [reply] |
Re: How to pass data as STDIN to Capture::Tiny
by Anonymous Monk on Jan 02, 2017 at 01:55 UTC
|
Try IPC::Run3 instead, it runs external commands, captures STDOUT and STDERR, and allows feeding the commands STDIN. | [reply] |
Re: How to pass data as STDIN to Capture::Tiny [more complete example]
by kcott (Archbishop) on Jan 04, 2017 at 03:59 UTC
|
Following discussions in various branches of this thread, here's a more complete example.
It reports on the following:
-
Whatever is output to STDOUT.
With the test external command (cat), you'll just see password.
-
Whatever is output to STDERR.
With 'cat 1>&2', you'll get password here instead;
with 'cat -X', an invalid option message,
with 'xcat', an invalid command message,
and so on.
-
An exit value from &run_external_command: either 1 (success),
0 (error, e.g. an invalid command), or
<undef> (fatal, e.g. SIGPIPE).
-
The command that was run.
-
The PID returned by open or an error message.
-
The return value of print on success, otherwise an error message.
-
The return value of close on success, otherwise an error message.
-
Messages from the $SIG{PIPE} handler:
either "SIGPIPE not received." or various depending on actions if a SIGPIPE signal was trapped.
-
Various other messages under exceptional conditions, e.g. "Died at ...".
All of the above is still reported even under such conditions;
although values may be empty, or just "<undef>", depending on where the exception occurred.
As well as testing with different commands, I also (temporarily) added lines like the following in different places:
kill PIPE => $$;
kill PIPE => $pid;
kill TERM => $pid;
die 'Test Death';
Here's the code:
#!/usr/bin/env perl -l
use strict;
use warnings;
use Capture::Tiny qw{capture};
use Try::Tiny;
my $retvals_for;
{
my ($stdout, $stderr, $exit);
try {
($stdout, $stderr, $exit) = capture \&run_external_command;
}
catch {
die "FATAL! $_" if defined $_;
}
finally {
print_results($stdout, $stderr, $exit);
};
die "ERROR! External command failed.\n" unless $exit;
}
sub run_external_command {
my $external_command = 'cat';
push @{$retvals_for->{command}}, $external_command;
{
my $cmd_pipe;
push @{$retvals_for->{signal}}, 'SIGPIPE not received.';
local $SIG{PIPE} = sub {
pop @{$retvals_for->{signal}};
push @{$retvals_for->{signal}}, 'SIGPIPE received.', @_;
if (exists $retvals_for->{open} and $retvals_for->{open}[0
+]) {
kill TERM => $retvals_for->{open}[0]
if kill 0 => $retvals_for->{open}[0];
my $close_ok = close $cmd_pipe;
if ($close_ok) {
push @{$retvals_for->{signal}}, "Closed pipe: $clo
+se_ok";
}
else {
push @{$retvals_for->{signal}}, "Can't close pipe:
+ $!, $?";
}
}
die 'SIGPIPE received.';
};
my $pid = open $cmd_pipe, '|-', $external_command;
if (defined $pid) {
push @{$retvals_for->{open}}, $pid;
}
else {
push @{$retvals_for->{open}}, "Can't open pipe: $!";
return 0;
}
my $print_ok = print $cmd_pipe "password\n";
if ($print_ok) {
push @{$retvals_for->{print}}, $print_ok;
}
else {
push @{$retvals_for->{print}}, "Can't print pipe: $!";
return 0;
}
my $close_ok = close $cmd_pipe;
if ($close_ok) {
push @{$retvals_for->{close}}, $close_ok;
}
else {
push @{$retvals_for->{close}}, "Can't close pipe: $!, $?";
return 0;
}
}
return 1;
}
sub print_results {
my ($stdout, $stderr, $exit) = @_;
print 'STDOUT:';
print defined $stdout ? $stdout : '<undef>';
print 'STDERR:';
print defined $stderr ? $stderr : '<undef>';
print 'EXIT:';
print defined $exit ? $exit : '<undef>';
print 'Return Values';
for my $source (qw{command open print close signal}) {
print ucfirst($source), ':';
print for @{$retvals_for->{$source}};
}
}
Sample output:
STDOUT:
password
STDERR:
EXIT:
1
Return Values
Command:
cat
Open:
5906
Print:
1
Close:
1
Signal:
SIGPIPE not received.
| [reply] [d/l] [select] |
|
|
Impressively thorough. But now you're scaring me into using IPC::Run3 ;-)
BTW, more googling found this ticket on github: stdin custom filehandle option.
| [reply] |