Let File::Path report the extended system error message
[p5sagit/p5-mst-13.2.git] / lib / File / Fetch.pm
CommitLineData
79fd8837 1package File::Fetch;
2
3use strict;
4use FileHandle;
5use File::Copy;
6use File::Spec;
7use File::Spec::Unix;
8use File::Fetch::Item;
9use File::Basename qw[dirname];
10
11use Cwd qw[cwd];
12use Carp qw[carp];
13use IPC::Cmd qw[can_run run];
14use File::Path qw[mkpath];
15use Params::Check qw[check];
16use Module::Load::Conditional qw[can_load];
17use Locale::Maketext::Simple Style => 'gettext';
18
19use vars qw[ $VERBOSE $PREFER_BIN $FROM_EMAIL $USER_AGENT
20 $BLACKLIST $METHOD_FAIL $VERSION $METHODS
21 $FTP_PASSIVE $TIMEOUT $DEBUG $WARN
22 ];
23
24$VERSION = 0.08;
25$PREFER_BIN = 0; # XXX TODO implement
26$FROM_EMAIL = 'File-Fetch@example.com';
27$USER_AGENT = 'File::Fetch/$VERSION';
28$BLACKLIST = [qw|ftp|];
29$METHOD_FAIL = { };
30$FTP_PASSIVE = 1;
31$TIMEOUT = 0;
32$DEBUG = 0;
33$WARN = 1;
34
35### methods available to fetch the file depending on the scheme
36$METHODS = {
37 http => [ qw|lwp wget curl lynx| ],
38 ftp => [ qw|lwp netftp wget curl ncftp ftp| ],
39 file => [ qw|lwp file| ],
40 rsync => [ qw|rsync| ]
41};
42
43### silly warnings ###
44local $Params::Check::VERBOSE = 1;
45local $Params::Check::VERBOSE = 1;
46local $Module::Load::Conditional::VERBOSE = 0;
47local $Module::Load::Conditional::VERBOSE = 0;
48
49### see what OS we are on, important for file:// uris ###
50use constant ON_UNIX => ($^O ne 'MSWin32' and
51 $^O ne 'MacOS' and
52 $^O ne 'VMS');
53
54=pod
55
56=head1 NAME
57
58File::Fetch - A generic file fetching mechanism
59
60=head1 SYNOPSIS
61
62 use File::Fetch;
63
64 ### build a File::Fetch object ###
65 my $ff = File::Fetch->new(uri => 'http://some.where.com/dir/a.txt');
66
67 ### fetch the uri to cwd() ###
68 my $where = $ff->fetch() or die $ff->error;
69
70 ### fetch the uri to /tmp ###
71 my $where = $ff->fetch( to => '/tmp' );
72
73 ### parsed bits from the uri ###
74 $ff->uri;
75 $ff->scheme;
76 $ff->host;
77 $ff->path;
78 $ff->file;
79
80=head1 DESCRIPTION
81
82File::Fetch is a generic file fetching mechanism.
83
84It allows you to fetch any file pointed to by a C<ftp>, C<http>,
85C<file>, or C<rsync> uri by a number of different means.
86
87See the C<HOW IT WORKS> section further down for details.
88
89=head1 METHODS
90
91=head2 $ff = File::Fetch->new( uri => 'http://some.where.com/dir/file.txt' );
92
93Parses the uri and creates a corresponding File::Fetch::Item object,
94that is ready to be C<fetch>ed and returns it.
95
96Returns false on failure.
97
98=cut
99
100sub new {
101 my $class = shift;
102 my %hash = @_;
103
104 my ($uri);
105 my $tmpl = {
106 uri => { required => 1, store => \$uri },
107 };
108
109 check( $tmpl, \%hash ) or return;
110
111 ### parse the uri to usable parts ###
112 my $href = __PACKAGE__->_parse_uri( $uri ) or return;
113
114 ### make it into a FFI object ###
115 my $ffi = File::Fetch::Item->new( %$href ) or return;
116
117
118 ### return the object ###
119 return $ffi;
120}
121
122### parses an uri to a hash structure:
123###
124### $class->_parse_uri( 'ftp://ftp.cpan.org/pub/mirror/index.txt' )
125###
126### becomes:
127###
128### $href = {
129### scheme => 'ftp',
130### host => 'ftp.cpan.org',
131### path => '/pub/mirror',
132### file => 'index.html'
133### };
134###
135sub _parse_uri {
136 my $self = shift;
137 my $uri = shift or return;
138
139 my $href = { uri => $uri };
140
141 ### find the scheme ###
142 $uri =~ s|^(\w+)://||;
143 $href->{scheme} = $1;
144
145 ### file:// paths have no host ###
146 if( $href->{scheme} eq 'file' ) {
147 $href->{path} = $uri;
148 $href->{host} = '';
149
150 } else {
151 @{$href}{qw|host path|} = $uri =~ m|([^/]*)(/.*)$|s;
152 }
153
154 ### split the path into file + dir ###
155 { my @parts = File::Spec::Unix->splitpath( delete $href->{path} );
156 $href->{path} = $parts[1];
157 $href->{file} = $parts[2];
158 }
159
160
161 return $href;
162}
163
164=head2 $ff->fetch( [to => /my/output/dir/] )
165
166Fetches the file you requested. By default it writes to C<cwd()>,
167but you can override that by specifying the C<to> argument.
168
169Returns the full path to the downloaded file on success, and false
170on failure.
171
172=cut
173
174sub fetch {
175 my $self = shift or return;
176 my %hash = @_;
177
178 my $to;
179 my $tmpl = {
180 to => { default => cwd(), store => \$to },
181 };
182
183 check( $tmpl, \%hash ) or return;
184
185 ### create the path if it doesn't exist yet ###
186 unless( -d $to ) {
187 eval { mkpath( $to ) };
188
189 return $self->_error(loc("Could not create path '%1'",$to)) if $@;
190 }
191
192 ### set passive ftp if required ###
193 local $ENV{FTP_PASSIVE} = $FTP_PASSIVE;
194
195 ###
196 for my $method ( @{ $METHODS->{$self->scheme} } ) {
197 my $sub = '_'.$method.'_fetch';
198
199 unless( __PACKAGE__->can($sub) ) {
200 $self->_error(loc("Cannot call method for '%1' -- WEIRD!",
201 $method));
202 next;
203 }
204
205 ### method is blacklisted ###
206 next if grep { lc $_ eq $method } @$BLACKLIST;
207
208 ### method is known to fail ###
209 next if $METHOD_FAIL->{$method};
210
211 if(my $file = $self->$sub(to=>File::Spec->catfile($to,$self->file))){
212
213 unless( -e $file && -s _ ) {
214 $self->_error(loc("'%1' said it fetched '%2', ".
215 "but it was not created",$method,$file));
216
217 ### mark the failure ###
218 $METHOD_FAIL->{$method} = 1;
219
220 next;
221
222 } else {
223
224 my $abs = File::Spec->rel2abs( $file );
225 return $abs;
226 }
227 }
228 }
229
230
231 ### if we got here, we looped over all methods, but we weren't able
232 ### to fetch it.
233 return;
234}
235
236=head1 ACCESSORS
237
238A C<File::Fetch> object has the following accessors
239
240=over 4
241
242=item $ff->uri
243
244The uri you passed to the constructor
245
246=item $ff->scheme
247
248The scheme from the uri (like 'file', 'http', etc)
249
250=item $ff->host
251
252The hostname in the uri, will be empty for a 'file' scheme.
253
254=item $ff->path
255
256The path from the uri, will be at least a single '/'.
257
258=item $ff->file
259
260The name of the remote file. Will be used as the name for the local
261file as well.
262
263=back
264
265=cut
266
267########################
268### _*_fetch methods ###
269########################
270
271### LWP fetching ###
272sub _lwp_fetch {
273 my $self = shift;
274 my %hash = @_;
275
276 my ($to);
277 my $tmpl = {
278 to => { required => 1, store => \$to }
279 };
280 check( $tmpl, \%hash ) or return;
281
282 ### modules required to download with lwp ###
283 my $use_list = {
284 LWP => '0.0',
285 'LWP::UserAgent' => '0.0',
286 'HTTP::Request' => '0.0',
287 'HTTP::Status' => '0.0',
288 URI => '0.0',
289
290 };
291
292 if( can_load(modules => $use_list) ) {
293
294 ### setup the uri object
295 my $uri = URI->new( File::Spec::Unix->catfile(
296 $self->path, $self->file
297 ) );
298
299 ### special rules apply for file:// uris ###
300 $uri->scheme( $self->scheme );
301 $uri->host( $self->scheme eq 'file' ? '' : $self->host );
302 $uri->userinfo("anonymous:$FROM_EMAIL") if $self->scheme ne 'file';
303
304 ### set up the useragent object
305 my $ua = LWP::UserAgent->new();
306 $ua->timeout( $TIMEOUT ) if $TIMEOUT;
307 $ua->agent( $USER_AGENT );
308 $ua->from( $FROM_EMAIL );
309 $ua->env_proxy;
310
311 my $res = $ua->mirror($uri, $to) or return;
312
313 ### uptodate or fetched ok ###
314 if ( $res->code == 304 or $res->code == 200 ) {
315 return $to;
316
317 } else {
318 return $self->_error(loc("Fetch failed! HTTP response: %1 %2 [%3]",
319 $res->code, HTTP::Status::status_message($res->code),
320 $res->status_line));
321 }
322
323 } else {
324 $METHOD_FAIL->{'lwp'} = 1;
325 return;
326 }
327}
328
329### Net::FTP fetching
330sub _netftp_fetch {
331 my $self = shift;
332 my %hash = @_;
333
334 my ($to);
335 my $tmpl = {
336 to => { required => 1, store => \$to }
337 };
338 check( $tmpl, \%hash ) or return;
339
340 ### required modules ###
341 my $use_list = { 'Net::FTP' => 0 };
342
343 if( can_load( modules => $use_list ) ) {
344
345 ### make connection ###
346 my $ftp;
347 my @options = ($self->host);
348 push(@options, Timeout => $TIMEOUT) if $TIMEOUT;
349 unless( $ftp = Net::FTP->new( @options ) ) {
350 return $self->_error(loc("Ftp creation failed: %1",$@));
351 }
352
353 ### login ###
354 unless( $ftp->login( anonymous => $FROM_EMAIL ) ) {
355 return $self->_error(loc("Could not login to '%1'",$self->host));
356 }
357
358 ### set binary mode, just in case ###
359 $ftp->binary;
360
361 ### create the remote path
362 ### remember remote paths are unix paths! [#11483]
363 my $remote = File::Spec::Unix->catfile( $self->path, $self->file );
364
365 ### fetch the file ###
366 my $target;
367 unless( $target = $ftp->get( $remote, $to ) ) {
368 return $self->_error(loc("Could not fetch '%1' from '%2'",
369 $remote, $self->host));
370 }
371
372 ### log out ###
373 $ftp->quit;
374
375 return $target;
376
377 } else {
378 $METHOD_FAIL->{'netftp'} = 1;
379 return;
380 }
381}
382
383### /bin/wget fetch ###
384sub _wget_fetch {
385 my $self = shift;
386 my %hash = @_;
387
388 my ($to);
389 my $tmpl = {
390 to => { required => 1, store => \$to }
391 };
392 check( $tmpl, \%hash ) or return;
393
394 ### see if we have a wget binary ###
395 if( my $wget = can_run('wget') ) {
396
397 ### no verboseness, thanks ###
398 my $cmd = [ $wget, '--quiet' ];
399
400 ### if a timeout is set, add it ###
401 push(@$cmd, '--timeout=' . $TIMEOUT) if $TIMEOUT;
402
403 ### run passive if specified ###
404 push @$cmd, '--passive-ftp' if $FTP_PASSIVE;
405
406 ### set the output document, add the uri ###
407 push @$cmd, '--output-document', $to, $self->uri;
408
409 ### shell out ###
410 my $captured;
411 unless( run( command => $cmd, buffer => \$captured, verbose => 0 ) ) {
412 ### wget creates the output document always, even if the fetch
413 ### fails.. so unlink it in that case
414 1 while unlink $to;
415
416 return $self->_error(loc( "Command failed: %1", $captured || '' ));
417 }
418
419 return $to;
420
421 } else {
422 $METHOD_FAIL->{'wget'} = 1;
423 return;
424 }
425}
426
427
428### /bin/ftp fetch ###
429sub _ftp_fetch {
430 my $self = shift;
431 my %hash = @_;
432
433 my ($to);
434 my $tmpl = {
435 to => { required => 1, store => \$to }
436 };
437 check( $tmpl, \%hash ) or return;
438
439 ### see if we have a wget binary ###
440 if( my $ftp = can_run('ftp') ) {
441
442 my $fh = FileHandle->new;
443
444 local $SIG{CHLD} = 'IGNORE';
445
446 unless ($fh->open("|$ftp -n")) {
447 return $self->_error(loc("%1 creation failed: %2", $ftp, $!));
448 }
449
450 my @dialog = (
451 "lcd " . dirname($to),
452 "open " . $self->host,
453 "user anonymous $FROM_EMAIL",
454 "cd /",
455 "cd " . $self->path,
456 "binary",
457 "get " . $self->file . " " . $self->file,
458 "quit",
459 );
460
461 foreach (@dialog) { $fh->print($_, "\n") }
462 $fh->close or return;
463
464 return $to;
465 }
466}
467
468### lynx is stupid - it decompresses any .gz file it finds to be text
469### use /bin/lynx to fetch files
470sub _lynx_fetch {
471 my $self = shift;
472 my %hash = @_;
473
474 my ($to);
475 my $tmpl = {
476 to => { required => 1, store => \$to }
477 };
478 check( $tmpl, \%hash ) or return;
479
480 ### see if we have a wget binary ###
481 if( my $lynx = can_run('lynx') ) {
482
483
484 ### write to the output file ourselves, since lynx ass_u_mes to much
485 my $local = FileHandle->new(">$to")
486 or return $self->_error(loc(
487 "Could not open '%1' for writing: %2",$to,$!));
488
489 ### dump to stdout ###
490 my $cmd = [
491 $lynx,
492 '-source',
493 "-auth=anonymous:$FROM_EMAIL",
494 ];
495
496 push @$cmd, "-connect_timeout=$TIMEOUT" if $TIMEOUT;
497
498 push @$cmd, $self->uri;
499
500 ### shell out ###
501 my $captured;
502 unless(run( command => $cmd,
503 buffer => \$captured,
504 verbose => $DEBUG )
505 ) {
506 return $self->_error(loc("Command failed: %1", $captured || ''));
507 }
508
509 ### print to local file ###
510 ### XXX on a 404 with a special error page, $captured will actually
511 ### hold the contents of that page, and make it *appear* like the
512 ### request was a success, when really it wasn't :(
513 ### there doesn't seem to be an option for lynx to change the exit
514 ### code based on a 4XX status or so.
515 ### the closest we can come is using --error_file and parsing that,
516 ### which is very unreliable ;(
517 $local->print( $captured );
518 $local->close or return;
519
520 return $to;
521
522 } else {
523 $METHOD_FAIL->{'lynx'} = 1;
524 return;
525 }
526}
527
528### use /bin/ncftp to fetch files
529sub _ncftp_fetch {
530 my $self = shift;
531 my %hash = @_;
532
533 my ($to);
534 my $tmpl = {
535 to => { required => 1, store => \$to }
536 };
537 check( $tmpl, \%hash ) or return;
538
539 ### we can only set passive mode in interactive sesssions, so bail out
540 ### if $FTP_PASSIVE is set
541 return if $FTP_PASSIVE;
542
543 ### see if we have a wget binary ###
544 if( my $ncftp = can_run('ncftp') ) {
545
546 my $cmd = [
547 $ncftp,
548 '-V', # do not be verbose
549 '-p', $FROM_EMAIL, # email as password
550 $self->host, # hostname
551 dirname($to), # local dir for the file
552 # remote path to the file
553 File::Spec::Unix->catdir( $self->path, $self->file ),
554 ];
555
556 ### shell out ###
557 my $captured;
558 unless(run( command => $cmd,
559 buffer => \$captured,
560 verbose => $DEBUG )
561 ) {
562 return $self->_error(loc("Command failed: %1", $captured || ''));
563 }
564
565 return $to;
566
567 } else {
568 $METHOD_FAIL->{'ncftp'} = 1;
569 return;
570 }
571}
572
573### use /bin/curl to fetch files
574sub _curl_fetch {
575 my $self = shift;
576 my %hash = @_;
577
578 my ($to);
579 my $tmpl = {
580 to => { required => 1, store => \$to }
581 };
582 check( $tmpl, \%hash ) or return;
583
584 if (my $curl = can_run('curl')) {
585
586 ### these long opts are self explanatory - I like that -jmb
587 my $cmd = [ $curl ];
588
589 push(@$cmd, '--connect-timeout', $TIMEOUT) if $TIMEOUT;
590
591 push(@$cmd, '--silent') unless $DEBUG;
592
593 ### curl does the right thing with passive, regardless ###
594 if ($self->scheme eq 'ftp') {
595 push(@$cmd, '--user', "anonymous:$FROM_EMAIL");
596 }
597
598 ### curl doesn't follow 302 (temporarily moved) etc automatically
599 ### so we add --location to enable that.
600 push @$cmd, '--fail', '--location', '--output', $to, $self->uri;
601
602 my $captured;
603 unless(run( command => $cmd,
604 buffer => \$captured,
605 verbose => $DEBUG )
606 ) {
607
608 return $self->_error(loc("Command failed: %1", $captured || ''));
609 }
610
611 return $to;
612
613 } else {
614 $METHOD_FAIL->{'curl'} = 1;
615 return;
616 }
617}
618
619
620### use File::Copy for fetching file:// urls ###
621### XXX file:// uri to local path conversion is just too weird...
622### depend on LWP to do it for us
623sub _file_fetch {
624 my $self = shift;
625 my %hash = @_;
626
627 my ($to);
628 my $tmpl = {
629 to => { required => 1, store => \$to }
630 };
631 check( $tmpl, \%hash ) or return;
632
633 ### prefix a / on unix systems with a file uri, since it would
634 ### look somewhat like this:
635 ### file://home/kane/file
636 ### wheras windows file uris might look like:
637 ### file://C:/home/kane/file
638 my $path = ON_UNIX ? '/'. $self->path : $self->path;
639
640 my $remote = File::Spec->catfile( $path, $self->file );
641
642 ### File::Copy is littered with 'die' statements :( ###
643 my $rv = eval { File::Copy::copy( $remote, $to ) };
644
645 ### something went wrong ###
646 if( !$rv or $@ ) {
647 return $self->_error(loc("Could not copy '%1' to '%2': %3 %4",
648 $remote, $to, $!, $@));
649 }
650
651 return $to;
652}
653
654### use /usr/bin/rsync to fetch files
655sub _rsync_fetch {
656 my $self = shift;
657 my %hash = @_;
658
659 my ($to);
660 my $tmpl = {
661 to => { required => 1, store => \$to }
662 };
663 check( $tmpl, \%hash ) or return;
664
665 if (my $rsync = can_run('rsync')) {
666
667 my $cmd = [ $rsync ];
668
669 ### XXX: rsync has no I/O timeouts at all, by default
670 push(@$cmd, '--timeout=' . $TIMEOUT) if $TIMEOUT;
671
672 push(@$cmd, '--quiet') unless $DEBUG;
673
674 push @$cmd, $self->uri, $to;
675
676 my $captured;
677 unless(run( command => $cmd,
678 buffer => \$captured,
679 verbose => $DEBUG )
680 ) {
681
682 return $self->_error(loc("Command failed: %1", $captured || ''));
683 }
684
685 return $to;
686
687 } else {
688 $METHOD_FAIL->{'rsync'} = 1;
689 return;
690 }
691}
692
693#################################
694#
695# Error code
696#
697#################################
698
699=pod
700
701=head2 $ff->error([BOOL])
702
703Returns the last encountered error as string.
704Pass it a true value to get the C<Carp::longmess()> output instead.
705
706=cut
707
708### Error handling, the way Archive::Tar does it ###
709{
710 my $error = '';
711 my $longmess = '';
712
713 sub _error {
714 my $self = shift;
715 $error = shift;
716 $longmess = Carp::longmess($error);
717
718 ### set Archive::Tar::WARN to 0 to disable printing
719 ### of errors
720 if( $WARN ) {
721 carp $DEBUG ? $longmess : $error;
722 }
723
724 return;
725 }
726
727 sub error {
728 my $self = shift;
729 return shift() ? $longmess : $error;
730 }
731}
732
733
734
7351;
736
737=pod
738
739=head1 HOW IT WORKS
740
741File::Fetch is able to fetch a variety of uris, by using several
742external programs and modules.
743
744Below is a mapping of what utilities will be used in what order
745for what schemes, if available:
746
747 file => LWP, file
748 http => LWP, wget, curl, lynx
749 ftp => LWP, Net::FTP, wget, curl, ncftp, ftp
750 rsync => rsync
751
752If you'd like to disable the use of one or more of these utilities
753and/or modules, see the C<$BLACKLIST> variable further down.
754
755If a utility or module isn't available, it will be marked in a cache
756(see the C<$METHOD_FAIL> variable further down), so it will not be
757tried again. The C<fetch> method will only fail when all options are
758exhausted, and it was not able to retrieve the file.
759
760A special note about fetching files from an ftp uri:
761
762By default, all ftp connections are done in passive mode. To change
763that, see the C<$FTP_PASSIVE> variable further down.
764
765Furthermore, ftp uris only support anonymous connections, so no
766named user/password pair can be passed along.
767
768C</bin/ftp> is blacklisted by default; see the C<$BLACKLIST> variable
769further down.
770
771=head1 GLOBAL VARIABLES
772
773The behaviour of File::Fetch can be altered by changing the following
774global variables:
775
776=head2 $File::Fetch::FROM_EMAIL
777
778This is the email address that will be sent as your anonymous ftp
779password.
780
781Default is C<File-Fetch@example.com>.
782
783=head2 $File::Fetch::USER_AGENT
784
785This is the useragent as C<LWP> will report it.
786
787Default is C<File::Fetch/$VERSION>.
788
789=head2 $File::Fetch::FTP_PASSIVE
790
791This variable controls whether the environment variable C<FTP_PASSIVE>
792and any passive switches to commandline tools will be set to true.
793
794Default value is 1.
795
796Note: When $FTP_PASSIVE is true, C<ncftp> will not be used to fetch
797files, since passive mode can only be set interactively for this binary
798
799=head2 $File::Fetch::TIMEOUT
800
801When set, controls the network timeout (counted in seconds).
802
803Default value is 0.
804
805=head2 $File::Fetch::WARN
806
807This variable controls whether errors encountered internally by
808C<File::Fetch> should be C<carp>'d or not.
809
810Set to false to silence warnings. Inspect the output of the C<error()>
811method manually to see what went wrong.
812
813Defaults to C<true>.
814
815=head2 $File::Fetch::DEBUG
816
817This enables debugging output when calling commandline utilities to
818fetch files.
819This also enables C<Carp::longmess> errors, instead of the regular
820C<carp> errors.
821
822Good for tracking down why things don't work with your particular
823setup.
824
825Default is 0.
826
827=head2 $File::Fetch::BLACKLIST
828
829This is an array ref holding blacklisted modules/utilities for fetching
830files with.
831
832To disallow the use of, for example, C<LWP> and C<Net::FTP>, you could
833set $File::Fetch::BLACKLIST to:
834
835 $File::Fetch::BLACKLIST = [qw|lwp netftp|]
836
837The default blacklist is [qw|ftp|], as C</bin/ftp> is rather unreliable.
838
839See the note on C<MAPPING> below.
840
841=head2 $File::Fetch::METHOD_FAIL
842
843This is a hashref registering what modules/utilities were known to fail
844for fetching files (mostly because they weren't installed).
845
846You can reset this cache by assigning an empty hashref to it, or
847individually remove keys.
848
849See the note on C<MAPPING> below.
850
851=head1 MAPPING
852
853
854Here's a quick mapping for the utilities/modules, and their names for
855the $BLACKLIST, $METHOD_FAIL and other internal functions.
856
857 LWP => lwp
858 Net::FTP => netftp
859 wget => wget
860 lynx => lynx
861 ncftp => ncftp
862 ftp => ftp
863 curl => curl
864 rsync => rsync
865
866=head1 FREQUENTLY ASKED QUESTIONS
867
868=head2 So how do I use a proxy with File::Fetch?
869
870C<File::Fetch> currently only supports proxies with LWP::UserAgent.
871You will need to set your environment variables accordingly. For
872example, to use an ftp proxy:
873
874 $ENV{ftp_proxy} = 'foo.com';
875
876Refer to the LWP::UserAgent manpage for more details.
877
878=head2 I used 'lynx' to fetch a file, but its contents is all wrong!
879
880C<lynx> can only fetch remote files by dumping its contents to C<STDOUT>,
881which we in turn capture. If that content is a 'custom' error file
882(like, say, a C<404 handler>), you will get that contents instead.
883
884Sadly, C<lynx> doesn't support any options to return a different exit
885code on non-C<200 OK> status, giving us no way to tell the difference
886between a 'successfull' fetch and a custom error page.
887
888Therefor, we recommend to only use C<lynx> as a last resort. This is
889why it is at the back of our list of methods to try as well.
890
891=head1 TODO
892
893=over 4
894
895=item Implement $PREFER_BIN
896
897To indicate to rather use commandline tools than modules
898
899=head1 AUTHORS
900
901This module by
902Jos Boumans E<lt>kane@cpan.orgE<gt>.
903
904=head1 COPYRIGHT
905
906This module is
907copyright (c) 2003 Jos Boumans E<lt>kane@cpan.orgE<gt>.
908All rights reserved.
909
910This library is free software;
911you may redistribute and/or modify it under the same
912terms as Perl itself.
913
914=cut
915
916# Local variables:
917# c-indentation-style: bsd
918# c-basic-offset: 4
919# indent-tabs-mode: nil
920# End:
921# vim: expandtab shiftwidth=4:
922
923
924
925