Skip Menu |

This queue is for tickets about the GnuPG-Interface CPAN distribution.

Report information
The Basics
Id: 93812
Status: new
Priority: 0/
Queue: GnuPG-Interface

People
Owner: Nobody in particular
Requestors: guilhem [...] cpan.org
Cc:
AdminCc:

Bug Information
Severity: Wishlist
Broken in: (no value)
Fixed in: (no value)



Subject: Please add a method to GnuPG::Handles objects to process multiple handles at once.
Hi there, A common use of GnuPG in scripts is to parse the output and process it as it comes. Therefore the following pattern is very common (at least in my code) when working with GnuPG::Interface. my $pid = $gpg->list_public_keys(handles => $handles, command_args => ...); while ($handles->{stdout}->getlines()) { # process line } waitpid $pid, 0; $handles->{stdout}->close(); This is fine as it doesn't involve anything complicated; however it's far more tricky when two IO::Handle objects have to be processed simultaneously, since IO::Select has to enter into the picture. Given that it's not so uncommon to have to process two handles at once (force instance the standard output and the status descriptor when editing a key), I propose to add the following method 'communicate' (heavily inspired from caff's 'readwrite_gpg' function [1]) to GnuPG::Handles objects. $handles->communicate( writefd => "Some data to write in\n" , readfd => sub { push @data, $_ } , OPTIONS ); * writefd (resp. readfd) are input (resp. output) data members of GnuPG::Handles objects. Possible values are qw/stdin passphrase command/ (resp. qw/stdout stderr status logger/). * When input data members are given, their value must be a scalar which is written to the corresponding handle. * When output data members are given, their value must be a reference to either a subroutine (used as a callback processing each line, bound as $_, which is read on the handle) or a regular expression (which is matched against each line read on the handle). If the callback returns a non-zero value, or if the regular expression matches the current line, then the method exits. * In list context, this method returns a hash where keys are the given output data members, and values are string representing the data read. * The only option is keep_handles_open (default: undefined) which, if set, don't autoclose the handles after writing/reading or when the callback aborts. Examples: # Dump output and status FDs: my %output = $handles->communicate(); print "Stdout is:\n$output{stdout}\nStatus is:\n$output{status}\n"; # Get the first public key found in a listing $handles->communicate( stdout => sub { if (/^pub:.:(?:[^:]*:){2,2}([^:]+):/) { $key = $1; return 1; } return; }); # Store all public key found in a listing: $handles->communicate( stdout => sub { push @key, $1 if /^pub:.:(?:[^:]*:){2,2}([^:]+):/; return; }); # Select the first UID when editing a key, and keep the handles open: $handles->communicate( command => "uid 1\n", status => qr/^\[GNUPG:\] GET_LINE keyedit\.prompt$/, keep_handles_open => 1); # Store all UIDs as well as attributes $handles->communicate( stdout => sub { push @uids, $1 if /^(?:uid|uat):.:.*:[0-9A-F]{40}::([^:]+):$/ return; }, status => sub { push @photos, {key => $1, size => $2, revoked => $3 & 0x02} if /^\[GNUPG:\] ATTRIBUTE [0-9A-F]{24}([0-9A-F]{16}) (\d+) 1 1 1 \d+ \d+ (\d+)$/ return; }); # Delete a sig in the keyedit prompt, but remember which: $handles->communicate( command => "yes\n", keep_handles_open => 1, status => qr/^\[GNUPG:\] GET_BOOL keyedit\.delsig\.(?:unknown|invalid|valid)$/, stdout => sub { push @sigs, $1 if /^sig:.::\d+:([0-9A-F]{16}):\d+:.*:(1[0-3]|30)[lx]$/ return; }); I attach a proof of concept (again, heavily inspired from caff's 'readwrite_gpg' function [1]). Feedback welcome. Thanks, cheers, -- Guilhem. [1] http://pgp-tools.alioth.debian.org/
Subject: communicate.pl
sub communicate($%) { my $self = shift; my %args = @_; my @wfds = qw/stdin passphrase command/; # known input handles my @rfds = qw/stdout stderr status logger/; # known output handles # ignore naked globs and unkown handles; ignore inputs we are not writing to @wfds = grep {defined $self->{$_} and ref $self->{$_} eq 'IO::Handle' and defined $args{$_}} @wfds; @rfds = grep {defined $self->{$_} and ref $self->{$_} eq 'IO::Handle'} @rfds; $self->{$_}->blocking(0) foreach (@wfds, @rfds); my $ws = IO::Select->new( @$self{@wfds} ); my $rs = IO::Select->new( @$self{@rfds} ); my %names = map { $self->{$_} => $_ } (@rfds,@wfds); my %left = map {$_ => length $args{$_}} @wfds; # #bytes left to write my %output = map {$_ => ''} @rfds; my $done = 0; while ($rs->count() or $ws->count()) { my @ready = IO::Select::select($rs, $ws, undef, $done ? 0 : 1); $_ //= [] foreach @ready[0,1]; # if we exited due to an aborted callback, we keep processing # the other handles (but select without timeout) to give a # chance for the related data (e.g. interleaving handles) to # show up last if $done and !@{$ready[0]} and !@{$ready[1]}; foreach my $fd (@{$ready[0]}) { # readers my $name = $names{$fd}; if ($fd->eof) { # done reading from $fd $rs->remove($fd); $fd->close() unless $args{keep_handles_open}; next; } foreach my $line ($fd->getlines()) { $output{$name} .= $line if wantarray; if (defined $args{$name}) { # callback local $_ = $line; my $r = ref $args{$name} eq 'CODE' ? $args{$name}->() : ref $args{$name} eq 'Regexp' ? $line =~ /$args{$name}/ : die "Unrecognized reference for $name: ".(ref $args{$name}); if ($r) { $done = 1; $rs->remove($fd); # stop processing that fd $fd->close() unless $args{keep_handles_open}; last; # exit the getlines } } } } foreach my $fd (@{$ready[1]}) { # writers my $name = $names{$fd}; if ($left{$name}) { my $written = $fd->syswrite($args{$name}, $left{$name}, -$left{$name}); $left{$name} -= $written; }; unless ($left{$name}) { # done writing in $fd $ws->remove($fd); $fd->close() unless $args{keep_handles_open}; } } } return %output if wantarray; }