File Coverage

blib/lib/App/grepurl.pm
Criterion Covered Total %
statement 35 198 17.6
branch 0 88 0.0
condition 0 15 0.0
subroutine 12 25 48.0
pod 0 12 0.0
total 47 338 13.9


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2 2     2   606177 use v5.16;
  2         8  
3 2     2   10 use strict;
  2         3  
  2         50  
4 2     2   9 use warnings;
  2         4  
  2         220  
5              
6             package App::grepurl;
7              
8             =encoding utf8
9              
10             =head1 NAME
11              
12             App::grepurl - print links in HTML
13              
14             =head1 SYNOPSIS
15              
16             grepurl [-bdv] [-e extension[,extension] [-E extension[,extension]
17             [-h host[,host]] [-H host[,host]] [-p regex] [-P regex]
18             [-s scheme[,scheme]] [-s scheme[,scheme]] [-u URL]
19              
20             =head1 DESCRIPTION
21              
22             The grepurl program searches through the URL specified in the -u
23             switch and prints the URLs that satisfies the given set of options.
24             It applies the options roughly in order of which part of the URL
25             the option affects (scheme, host, path, extension).
26              
27             So far, grepurl expects to search through HTML, although I want to add
28             other content types, especially plain text, RSS feeds, and so on.
29              
30             =head1 OPTIONS
31              
32             =over 4
33              
34             =item -a
35              
36             arrange (sort) links in ascending order
37              
38             =item -A
39              
40             arrange (sort) links in descending order
41              
42             =item -b
43              
44             turn relative URLs into absolute ones
45              
46             =item -d
47              
48             turn on debugging output
49              
50             =item -e EXTENSION
51              
52             select links with these extensions (comma separated)
53              
54             =item -E EXTENSION
55              
56             exclude links with these extensions (comma separated)
57              
58             =item -h HOST
59              
60             select links with these hosts (comma separated)
61              
62             =item -H HOST
63              
64             exclude links with these hosts (comma separated)
65              
66             =item -p REGEX
67              
68             select only paths that match this Perl regex
69              
70             =item -P REGEX
71              
72             exclude paths that match this Perl regex
73              
74             =item -r REGEX
75              
76             select only URLs that match this Perl regex (applies to entire URL)
77              
78             =item -R REGEX
79              
80             exclude URLs that match this Perl regex (applies to entire URL)
81              
82             =item -s SCHEME
83              
84             select only these schemes (comma separated)
85              
86             =item -S SCHEME
87              
88             exclude these schemes (comma separated)
89              
90             =item -t FILE
91              
92             extract URLs from plain text file (not implemented)
93              
94             =item -u URL
95              
96             extract URLs from URL (may be file://), expects HTML
97              
98             =item -v
99              
100             turn on verbose output
101              
102             =item -1
103              
104             print found URLs only once (print a unique list)
105              
106             =back
107              
108             =head2 Examples
109              
110             =over 4
111              
112             =item Print all the links
113              
114             grepurl -u http://www.example.com/
115              
116             =item Print all the links, and resolve relative URLs
117              
118             grepurl -b -u http://www.example.com/
119              
120             =item Print links with the edxtension .jpg
121              
122             grepurl -e jpg -u http://www.example.com/
123              
124             =item Print links with the edxtension .jpg and .jpeg
125              
126             grepurl -e jpg,jpeg -u http://www.example.com/
127              
128             =item Do not print links with the extension .cfm or .asp
129              
130             grepurl -E cfm,asp -u http://www.example.com/
131              
132             =item Print only links to www.panix.com
133              
134             grepurl -h www.panix.com -u http://www.example.com/
135              
136             =item Print only links to www.panix.com or www.perl.com
137              
138             grepurl -h www.panix.com,www.perl.com -u http://www.example.com/
139              
140             =item Do not print links to www.microsoft.com
141              
142             grepurl -H www.microsoft.com -u http://www.example.com/
143              
144             =item Print links with "perl" in the path
145              
146             grepurl -p perl -u http://www.example.com
147              
148             =item Print links with "perl" or "pearl" in the path
149              
150             grepurl -p "pea?rl" -u http://www.example.com
151              
152             =item Print links with "fred" or "barney" in the path
153              
154             grepurl -p "fred|barney" -u http://www.example.com
155              
156             =item Do not print links with "SCO" in the path
157              
158             grepurl -P SCO -u http://www.example.com
159              
160             =item Do not print links whose path matches "Micro.*"
161              
162             grepurl -P "Micro.*" -u http://www.example.com
163              
164             =item Do not print links whose URL matches "Micro.*" anywhere
165              
166             grepurl -R "Micro.*" -u http://www.example.com
167              
168             =item Print only web links
169              
170             grepurl -s http -u http://www.example.com/
171              
172             =item Print ftp and gopher links
173              
174             grepurl -s ftp,gopher -u http://www.example.com/
175              
176             =item Exclude ftp and gopher links
177              
178             grepurl -S ftp,gopher -u http://www.example.com/
179              
180             =item Arrange the links in an ascending sort
181              
182             grepurl -a -u http://www.example.com/
183              
184             =item Arrange the links in an descending sort
185              
186             grepurl -A -u http://www.example.com/
187              
188             =item Arrange the links in an descending sort, and print unique URLs
189              
190             grepurl -A -1 -u http://www.example.com/
191              
192             =back
193              
194             =head1 TO DO
195              
196             =over 4
197              
198             =item Operate over an entire directory or website
199              
200             =back
201              
202             =head1 SEE ALSO
203              
204             urifind by darren chamberlain Edarren@cpan.orgE
205              
206             =head1 SOURCE AVAILABILITY
207              
208             This source is in Github:
209              
210             https://github.com/briandfoy/app-grepurl
211              
212             =head1 AUTHOR
213              
214             brian d foy, C<< >>
215              
216             =head1 COPYRIGHT
217              
218             Copyright © 2004-2025, brian d foy . All rights reserved.
219              
220             You may use this program under the terms of the Artistic License 2.0.
221              
222             =cut
223              
224 2     2   18 use File::Basename;
  2         3  
  2         229  
225 2     2   1304 use FindBin;
  2         2525  
  2         116  
226 2     2   1120 use Getopt::Std;
  2         5179  
  2         154  
227 2     2   1290 use Mojo::DOM;
  2         437238  
  2         131  
228 2     2   1118 use Mojo::URL;
  2         20009  
  2         12  
229 2     2   1223 use Mojo::UserAgent;
  2         522015  
  2         12  
230 2     2   113 use Mojo::Util qw(dumper);
  2         2  
  2         544  
231              
232             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
233             our $VERSION = '1.013';
234              
235             run(@ARGV) unless caller;
236              
237             sub new {
238 0     0 0   my $self = bless {}, $_[0];
239 0           $self->init;
240 0           $self;
241             }
242              
243       0 0   sub init {}
244              
245 0     0 0   sub debug { warn join "\n", @_, '' }
246              
247             sub run {
248 0     0 0   my( $class, @args ) = @_;
249 0 0         unless( @args ) {
250 0           print "$FindBin::Script $VERSION\n";
251 0           exit;
252             }
253              
254 0           my %opts;
255             {
256 0           local @ARGV = @args;
  0            
257 0           getopts( 'bdv1' . 'aAiIjJ' . 'e:E:f:h:H:p:P:s:S:t:u:', \%opts );
258             }
259             # print STDERR Dumper( \%opts ); use Data::Dumper;
260             # print STDERR "Processed opts\n";
261              
262 0           my $obj = $class->new();
263 0           $obj->{opts} = \%opts;
264              
265             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
266 0   0       $obj->{Debug} = $opts{d} || $ENV{GREPURL_DEBUG} || 0;
267 2 0   2   37 { no warnings 'redefine'; *debug = sub { 0 } unless $obj->{Debug} }
  2     0   6  
  2         3127  
  0            
  0            
  0            
268              
269 0   0       $obj->{Verbose} = $opts{v} || $ENV{GREPURL_VERBOSE} || 0;
270 0   0       $obj->{Either} = $obj->{Debug} || $obj->{Verbose} || 0;
271              
272 0           $obj->{Hosts} = uncommify( $opts{h} );
273 0           $obj->{No_hosts} = uncommify( $opts{H} );
274              
275 0           $obj->{Schemes} = uncommify( $opts{'s'} );
276 0           $obj->{No_schemes} = uncommify( $opts{S} );
277              
278 0           $obj->{Extensions} = uncommify( $opts{e} );
279 0           $obj->{No_extensions} = uncommify( $opts{E} );
280              
281 0           $obj->{Path} = regex( $opts{p} );
282 0           $obj->{No_path} = regex( $opts{P} );
283              
284 0           $obj->{Regex} = regex( $opts{r} );
285 0           $obj->{No_regex} = regex( $opts{R} );
286              
287 0 0         $obj->debug_summary if $obj->{Debug};
288              
289 0           debug( "Moving on\n" );
290              
291             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
292 0           my $text = $obj->get_text;
293              
294 0 0 0       die "There is no text!\n" unless( defined $text && length $text > 0 );
295 0           my $urls = $obj->extract_from_html( $text );
296 0           debug( "Got URLs:\n" . dumper($urls) );
297              
298 0           @$urls = do {
299 0 0         if( defined $opts{b} ) {
300 0           my $base = Mojo::URL->new( $opts{b} );
301 0           debug( "Base url is $base\n" );
302 0           map { Mojo::URL->new( $_ )->base( $base )->to_abs } @$urls;
  0            
303             }
304             else {
305 0           map { Mojo::URL->new( $_ ) } @$urls;
  0            
306             }
307             };
308              
309             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
310             # Filters
311             #
312             # To select things, only pass through those elements
313             #
314             # To not select things, pass through anything that does not match
315             @$urls = map {
316 0           my $s = eval { $_->scheme };
  0            
317             defined $s ?
318 0 0         exists $obj->{Schemes}{$s} ? $_ : ()
    0          
319             :
320             ()
321 0 0         } @$urls if defined $opts{'s'};
322              
323             @$urls = map {
324 0           my $s = eval { $_->scheme };
  0            
325             defined $s ?
326 0 0         exists $obj->{No_schemes}{$s} ? () : $_
    0          
327             :
328             $_
329 0 0         } @$urls if defined $opts{S};
330              
331             @$urls = map {
332 0           my $h = eval { $_->host };
  0            
333             defined $h ?
334 0 0         exists $obj->{Hosts}{ $h } ? $_ : ()
    0          
335             :
336             ()
337 0 0         } @$urls if defined $opts{h};
338              
339             @$urls = map {
340 0           my $h = eval { $_->host };
  0            
341             defined $h ?
342 0 0         exists $obj->{No_hosts}{ $h } ? () : $_
    0          
343             :
344             $_
345 0 0         } @$urls if defined $opts{H};
346              
347             @$urls = map {
348 0           my $p = eval { $_->path };
  0            
349 0           my( $file ) = basename( $p );
350 0           my( $e ) = $file =~ /\.([^.]+)$/;
351 0   0       $e ||= '';
352 0 0         exists $obj->{Extensions}->{$e} ? $_ : ()
353 0 0         } @$urls if defined $opts{e};
354              
355             @$urls = map {
356 0           my $p = eval { $_->path };
  0            
357 0           my( $file ) = basename( $p );
358 0           my( $e ) = $file =~ /\.([^.]+)$/;
359 0   0       $e ||= '';
360 0 0         exists $obj->{No_extensions}->{$e} ? () : $_
361 0 0         } @$urls if defined $opts{E};
362              
363             @$urls = map {
364 0 0 0       my $p = eval { $_->path } || ''; $p =~ m/$obj->{Path}/ ? $_ : ()
  0            
365 0 0         } @$urls if defined $opts{p};
366              
367             @$urls = map {
368 0 0         my $p = $_->path; $p =~ m/$obj->{No_path}/ ? () : $_
  0            
369 0 0         } @$urls if defined $opts{P};
370              
371             @$urls = map {
372 0 0         my $u = $_->abs; $u =~ m/$obj->{Regex}/ ? $_ : ()
  0            
373 0 0         } @$urls if defined $opts{r};
374              
375             @$urls = map {
376 0 0         my $u = $_->abs; $u =~ m/$obj->{No_regex}/ ? () : $_
  0            
377 0 0         } @$urls if defined $opts{R};
378              
379             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
380             # Unique
381 0 0         @$urls = do { my %u = map { $_, 1 } @$urls; keys %u } if defined $opts{1};
  0            
  0            
  0            
382              
383             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
384             # Sort
385 0 0         @$urls = sort { $a cmp $b } @$urls if defined $opts{a};
  0            
386 0 0         @$urls = sort { $b cmp $a } @$urls if defined $opts{A};
  0            
387              
388             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
389             # Sort
390 0           $" = "\n";
391 0           print "@$urls\n";
392             }
393              
394             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
395             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
396             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
397              
398             sub extract_from_html {
399 0     0 0   my( $self, $text ) = @_;
400 0           debug( "In extract_from_html" );
401              
402 0           require Mojo::DOM;
403              
404 0           my $dom = Mojo::DOM->new( $text );
405              
406 0           debug( "Made DOM" );
407             my $links = [
408 0           @{ $dom->find('a[href]')->map( attr => 'href' )->to_array },
409 0           @{ $dom->find('img[src]')->map( attr => 'src' )->to_array },
  0            
410             ]
411             ;
412              
413 0           debug( "Found " . @$links . " links" );
414              
415 0           $links;
416             }
417              
418             sub get_text {
419 0     0 0   my( $self ) = @_;
420 0           my $opts = $self->{opts};
421              
422 0 0         if( defined $opts->{u} ) {
    0          
    0          
    0          
423 0           my $url = Mojo::URL->new( $opts->{u} );
424 0 0         die "Bad url [$opts->{u}]!\n" unless ref $url;
425 0 0         if( $url->scheme ne 'file' ) {
426 0           $self->read_from_url( $url );
427             }
428             else {
429 0           ( my $path = $url ) =~ s|\Afile://||;
430 0           $self->read_from_text_file( $path );
431             }
432             }
433             elsif( defined $opts->{t} ) {
434 0           my $file = $opts->{t};
435 0 0         die "Could not read file [$file]!\n" unless -r $file;
436 0           $self->read_from_text_file( $file );
437             }
438             elsif( @ARGV > 0 ) {
439 0           my $file = $opts->{t};
440 0 0         die "Could not read file [$file]!\n" unless -r $file;
441 0           $self->read_from_text_file( $file );
442             }
443             elsif( -t STDIN ) {
444 0           read_from_stdin();
445             }
446             else {
447 0           return;
448             }
449             }
450              
451             sub read_from_url {
452 0     0 0   my( $self, $url ) = @_;
453 0           debug( "Reading from url" );
454              
455 0           my $data = Mojo::UserAgent->new->get( $url )->result->body;
456              
457 0           $data;
458             }
459              
460             sub read_from_text_file {
461 0     0 0   my( $self, $file ) = @_;
462 0           debug( "Reading from file <$file>" );
463              
464 0           my $data = do { local $/; open my($fh), $file; <$fh> };
  0            
  0            
  0            
465              
466 0           $data;
467             }
468              
469             sub read_from_stdin {
470 0     0 0   my( $self ) = @_;
471 0 0         print "Reading from standard input" if $self->{Either};
472              
473 0           my $data = do { local $/; };
  0            
  0            
474              
475 0           $data;
476             }
477              
478             sub regex {
479 0     0 0   my( $self, $option ) = @_;
480              
481 0 0         return unless defined $option;
482              
483 0           my $regex = eval { qr/$option/ };
  0            
484              
485 0           $@ =~ s/at $FindBin::Script line \d+.*//;
486              
487 0 0         die "$FindBin::Script: $@" if $@;
488              
489 0           $regex;
490             }
491              
492             sub uncommify {
493 0     0 0   my( $self, $option ) = @_;
494              
495 0 0         return {} unless defined $option;
496              
497 0           return { map { $_, 1 } split m/,/, $option };
  0            
498             }
499              
500             sub debug_summary {
501 0     0 0   my( $self ) = @_;
502 2     2   31 no warnings;
  2         4  
  2         556  
503              
504 0           local $" = "\n\t";
505              
506 0           my $opts = $self->{opts};
507              
508 0           debug( <<"DEBUG" );
509             Version: $VERSION
510             Verbose: $self->{Verbose}
511             Debug: $self->{Debug}
512             Ascending: $opts->{a}
513             Descending: $opts->{A}
514             Unique: $opts->{1}
515             Image: $opts->{i}
516             Image(-): $opts->{I}
517             Javascript: $opts->{j}
518             Javascript(-): $opts->{j}
519             Hosts: $opts->{h}
520 0           @{ [ keys %{ $self->{Hosts} } ] }
  0            
521             Hosts(-): $opts->{H}
522 0           @{ [ keys %{ $self->{No_hosts} } ] }
  0            
523             Path: $opts->{p}
524             $self->{Path}
525             Path(-): $opts->{P}
526             $self->{No_path}
527             Regex: $opts->{r}
528             $self->{Regex}
529             Regex(-): $opts->{R}
530             $self->{No_regex}
531             Scheme: $opts->{s}
532 0           @{ [ keys %{ $self->{Schemes} } ] }
  0            
533             Scheme(-): $opts->{S}
534 0           @{ [ keys %{ $self->{No_schemes} } ] }
  0            
535             DEBUG
536             }
537              
538             1;