File Coverage

blib/script/licensecheck
Criterion Covered Total %
statement 122 182 67.0
branch 32 72 44.4
condition 9 40 22.5
subroutine 19 22 86.3
pod n/a
total 182 316 57.5


line stmt bran cond sub pod time code
1             #!/usr/local/bin/perl
2              
3 7     7   28596 use v5.12;
  7         22  
4 7     7   2852 use utf8;
  7         1828  
  7         42  
5 7     7   3241 use open qw(:locale);
  7         8563  
  7         31  
6 7     7   146797 use warnings;
  7         20  
  7         328  
7 7     7   3068 use autodie;
  7         104809  
  7         29  
8              
9             =head1 NAME
10              
11             licensecheck - simple license checker for source files
12              
13             =head1 VERSION
14              
15             Version v3.3.10
16              
17             =head1 SYNOPSIS
18              
19             licensecheck [ --help | --version ]
20              
21             licensecheck [ --list-licenses | --list-naming-schemes ]
22              
23             licensecheck [OPTION...] PATH [PATH...]
24              
25             =head1 DESCRIPTION
26              
27             B attempts to determine the license
28             that applies to each file passed to it,
29             by searching the start of the file
30             for text belonging to various licenses.
31              
32             If any of the arguments passed are directories,
33             B will add the files contained within
34             to the list of files to process.
35              
36             When multiple Fs are provided,
37             only files matching B<--check> and not B<--ignore> are checked.
38              
39             =cut
40              
41 7     7   44565 use Getopt::Long 2.24 qw(:config gnu_getopt);
  7         93141  
  7         171  
42 7     7   4304 use IO::Interactive qw(is_interactive);
  7         47872  
  7         60  
43              
44 7         955214 my $USE_COLOR;
45              
46             BEGIN {
47             $USE_COLOR = !(
48             exists $ENV{NO_COLOR}
49             or ( $ENV{COLOR} and !$ENV{COLOR} )
50 7   33 7   848 or !is_interactive
51             );
52 7 50       315 $Pod::Usage::Formatter = 'Pod::Text::Color' if $USE_COLOR;
53             }
54 7     7   3274 use Pod::Usage 1.60;
  7         275180  
  7         1405  
55             my $COPYRIGHT;
56             use Pod::Constants
57             -trim => 1,
58             'COPYRIGHT AND LICENSE' =>
59 7     7   3040 sub { ($COPYRIGHT) = s/C<< (.*) >>/$1/gr; $COPYRIGHT =~ s/©/©/g };
  7         12275  
  7         63  
  7         29890  
  7         88  
60 7     7   3285 use String::License::Naming::Custom;
  7         5292285  
  7         270  
61 7     7   5359 use Path::Tiny;
  7         76195  
  7         441  
62 7     7   5359 use Path::Iterator::Rule;
  7         76978  
  7         296  
63 7     7   3587 use String::Escape qw(unbackslash);
  7         35615  
  7         606  
64 7     7   83 use List::Util 1.45 qw(uniqstr);
  7         114  
  7         867  
65 7     7   36 use Log::Any qw($log);
  7         29  
  7         55  
66 7     7   17605 use Log::Any::Adapter;
  7         2216  
  7         24  
67              
68 7     7   3381 use App::Licensecheck;
  7         39  
  7         12644  
69              
70 7         25 our $VERSION = 'v3.3.10';
71              
72 7         154 my $progname = path($0)->basename;
73              
74 7         1011 our %OPT = ();
75 7         86 my @OPT = ();
76              
77             =head1 OPTIONS
78              
79             =head2 Resolving patterns
80              
81             =over 16
82              
83             =item B<--shortname-scheme>
84              
85             I
86              
87             comma-separated priority list of license naming schemes
88             to use for license identifiers
89             S<(default value: unset (use verbose description))>
90              
91             =item B<--list-licenses>
92              
93             I
94              
95             list identifiers for all detectable licenses and exit
96              
97             =item B<--list-naming-schemes>
98              
99             I
100              
101             list all available license naming schemes and exit
102              
103             =back
104              
105             =cut
106              
107 7         21 push @OPT, qw(
108             shortname-scheme=s
109             list-licenses
110             list-naming-schemes
111             );
112              
113             =head2 Selecting files
114              
115             =over 16
116              
117             =item B<-c> I, B<--check>=I
118              
119             I
120              
121             regular expression of files to include
122             when more than one F is provided
123             S<(default value: common source files)>
124              
125             =item B<-i> I, B<--ignore>=I
126              
127             I
128              
129             regular expression of files to skip
130             when more than one F is provided
131             S<(default value: some backup and VCS files)>
132              
133             =item B<-r>, B<--recursive>
134              
135             I
136              
137             traverse directories recursively
138              
139             =back
140              
141             =cut
142              
143 7         22 push @OPT, qw(
144             check|c=s
145             ignore|i=s
146             recursive|r
147             );
148 7         21 $OPT{check} = 'common source files';
149 7         27 $OPT{ignore} = 'some backup and VCS files';
150              
151             =head2 Parsing contents
152              
153             =over 16
154              
155             =item B<-l> I, B<--lines>=I
156              
157             I
158              
159             number of lines to parse from top of each file;
160             implies optimistic search
161             including only first cluster of detected copyrights or licenses;
162             set to I<0> to parse the whole file
163             (and ignore B<--tail>)
164             S<(default value: I<60>)>
165              
166             =item B<--tail>=I
167              
168             I
169              
170             number of bytes to parse from bottom of each file
171             when parsing only from top of each file and finding nothing there;
172             set to 0 to avoid parsing from end of file
173             (or set B<--lines> to I<0> and ignore this setting)
174             S<(default value: 5000 (roughly 60 lines))>
175              
176             =item B<-e> I, B<--encoding>=I
177              
178             I
179              
180             try decode source files from the specified codec,
181             with C as fallback
182             S<(default value: unset (no decoding))>
183              
184             =back
185              
186             =cut
187              
188 7         23 push @OPT, qw(
189             lines|l=i
190             tail=i
191             encoding|e=s
192             );
193 7         15 $OPT{lines} = 60;
194 7         19 $OPT{tail} = 5000;
195              
196             =head2 Reporting results
197              
198             =over 16
199              
200             =item B<--copyright>
201              
202             I
203              
204             add copyright statements to license information
205              
206             =item B<-s>, B<--skipped>
207              
208             I
209              
210             Log files in Fs
211             matching neither B<--check> nor B<--ignore>
212             as warnings
213             S<(default: log as debug)>
214              
215             =item B<-m>, B<--machine>
216              
217             I
218              
219             print license information as C-separated fields,
220             for processing with line-oriented tools like C and C
221              
222             =item B<--[no-]deb-machine>
223              
224             I
225              
226             print license information like a Debian copyright file;
227             implies B<--copyright> and B<--shortname-scheme>=I
228              
229             =item B<--list-delimiter>=I
230              
231             I
232              
233             printf-string used between multiple plain list items
234             in Debian copyright file
235             S<(default value: I<'\n '> (NEWLINE SPACE))>
236              
237             =item B<--rfc822-delimiter>=I
238              
239             I
240              
241             printf-string used between multiple RFC822-style items
242             in Debian copyright file
243             S<(default value: I<'\n '> (NEWLINE SPACE SPACE))>
244              
245             =item B<--copyright-delimiter>=I
246              
247             I
248              
249             printf-string used between years and owners
250             in Debian copyright file
251             S<(default value: I<', '> (COMMA SPACE))>
252              
253             =item B<--[no-]merge-licenses>
254              
255             I
256              
257             merge same-licensed files in Debian copyright file
258              
259             =back
260              
261             =cut
262              
263 7         47 push @OPT, qw(
264             copyright
265             skipped|s
266             machine|m
267             deb-machine!
268             list-delimiter=s
269             rfc822-delimiter=s
270             copyright-delimiter=s
271             merge-licenses!
272             );
273 7         15 $OPT{'list-delimiter'} = '\n ';
274 7         21 $OPT{'rfc822-delimiter'} = '\n ';
275 7         20 $OPT{'copyright-delimiter'} = ', ';
276              
277             =head2 General
278              
279             =over 16
280              
281             =item B<-h>, B<--help>
282              
283             print help message and exit
284              
285             =item B<-v>, B<--version>
286              
287             print version and copyright information and exit
288              
289             =item B<--quiet>, B<--verbose>, B<--debug>, B<--trace>
290              
291             I
292              
293             Emit only error messages to STDERR (with option B<--quiet>),
294             or (in addition errors and warnings) also notices/debug/traces.
295             The more "noisy" option wins if several are set.
296             S<(default: emit errors and warnings>
297              
298             =back
299              
300             =cut
301              
302 7         32 push @OPT, qw(
303             help|h
304             version|v
305             quiet
306             verbose
307             debug
308             trace
309             );
310              
311             # deprecated
312 7         11 push @OPT, qw(
313             deb-fmt!
314             );
315              
316             # obsolete
317 7         25 push @OPT, qw(
318             text|t
319             noconf|no-conf
320             no-verbose
321             );
322              
323 7 50       61 GetOptions( \%OPT, @OPT ) or pod2usage(2);
324              
325 7 50       12973 if ( $OPT{trace} ) {
    50          
    50          
    50          
326 0         0 Log::Any::Adapter->set(
327             'Screen', use_color => $USE_COLOR,
328             default_level => 'trace'
329             );
330             }
331             elsif ( $OPT{debug} ) {
332 0         0 Log::Any::Adapter->set(
333             'Screen', use_color => $USE_COLOR,
334             default_level => 'debug'
335             );
336             }
337             elsif ( $OPT{verbose} ) {
338 0         0 Log::Any::Adapter->set(
339             'Screen', use_color => $USE_COLOR,
340             default_level => 'info'
341             );
342             }
343             elsif ( $OPT{quiet} ) {
344 0         0 Log::Any::Adapter->set(
345             'Screen', use_color => $USE_COLOR,
346             default_level => 'error'
347             );
348             }
349             else {
350 7         110 Log::Any::Adapter->set( 'Screen', use_color => $USE_COLOR );
351             }
352              
353 7 100       17024 pod2usage(1) if ( $OPT{help} );
354 5 50       18 if ( $OPT{version} ) { version(); exit 0; }
  0         0  
  0         0  
355              
356 5         11 my @schemes;
357 5 50 33     78 if ( $OPT{'shortname-scheme'} ) {
    100 66        
      66        
358 0         0 @schemes = split /[\s,]+/, $OPT{'shortname-scheme'};
359             }
360             elsif ($OPT{'deb-machine'}
361             || $OPT{'deb-fmt'}
362             || $OPT{'list-licenses'}
363             || $OPT{'list-naming-schemes'} )
364             {
365 2         6 @schemes = qw(debian spdx);
366             }
367              
368             # TODO: skip when future option '--strict-schemes' is enabled
369 5 100       20 push @schemes, 'internal'
370             if @schemes;
371              
372 5         6 my $naming;
373 5 100       14 if (@schemes) {
374 2         38 $naming = String::License::Naming::Custom->new( schemes => \@schemes );
375             }
376             else {
377 3         75 $naming = String::License::Naming::Custom->new( schemes => \@schemes );
378             }
379              
380 5 100       107087 if ( $OPT{'list-licenses'} ) {
381 2         16 say for $naming->list_licenses;
382 2         0 exit 0;
383             }
384              
385 3 50       13 if ( $OPT{'list-naming-schemes'} ) {
386 0         0 say for $naming->list_schemes;
387 0         0 exit 0;
388             }
389              
390 3 50       12 if ( $OPT{text} ) {
391 0         0 $log->warn('option -text ignored: obsolete'); # since 2015
392             }
393 3 50       7 if ( $OPT{noconf} ) {
394 0         0 $log->warn('option --no-conf ingored: obsolete'); # since 2016
395             }
396 3 50       12 if ( $OPT{noverbose} ) {
397 0         0 $log->warn('option --no-verbose ignored: obsolete'); # since 2021
398             }
399              
400 3 50       10 pod2usage("$progname: No paths provided.")
401             unless @ARGV;
402              
403             my $app = App::Licensecheck->new(
404              
405             # parse
406             top_lines => $OPT{lines},
407             end_bytes => $OPT{tail},
408             encoding => $OPT{encoding},
409              
410             # report
411 3         53 naming => $naming,
412             );
413              
414 3         10 my $default_check_regex = q!
415             /[\w-]+$ # executable scripts or README like file
416             |\.( # search for file suffix
417             c(c|pp|xx)? # c and c++
418             |h(h|pp|xx)? # header files for c and c++
419             |S
420             |css|less # HTML css and similar
421             |f(77|90)?
422             |go
423             |groovy
424             |lisp
425             |scala
426             |clj
427             |p(l|m)?6?|t|xs|pod6? # perl5 or perl6
428             |sh
429             |php
430             |py(|x)
431             |rb
432             |java
433             |js
434             |vala
435             |el
436             |sc(i|e)
437             |cs
438             |pas
439             |inc
440             |dtd|xsl
441             |mod
442             |m
443             |md|markdown
444             |tex
445             |mli?
446             |(c|l)?hs
447             )$
448             !;
449              
450             # From dpkg-source
451 3         6 my $default_ignore_regex = q!
452             # Ignore general backup files
453             ~$|
454             # Ignore emacs recovery files
455             (?:^|/)\.#|
456             # Ignore vi swap files
457             (?:^|/)\..*\.swp$|
458             # Ignore baz-style junk files or directories
459             (?:^|/),,.*(?:$|/.*$)|
460             # File-names that should be ignored (never directories)
461             (?:^|/)(?:DEADJOE|\.cvsignore|\.arch-inventory|\.bzrignore|\.gitignore)$|
462             # File or directory names that should be ignored
463             (?:^|/)(?:CVS|RCS|\.pc|\.deps|\{arch\}|\.arch-ids|\.svn|\.hg|_darcs|\.git|
464             \.shelf|_MTN|\.bzr(?:\.backup|tags)?)(?:$|/.*$)
465             !;
466              
467 3         19 my $check_regex = $OPT{check};
468 3 50 33     46 if ( !$check_regex or $check_regex eq 'common source files' ) {
469 3         730 $check_regex = qr/$default_check_regex/x;
470             }
471             else {
472 0         0 $check_regex = qr/$check_regex/;
473             }
474              
475 3         15 my $ignore_regex = $OPT{ignore};
476 3 50 33     19 if ( !$ignore_regex or $ignore_regex eq 'some backup and VCS files' ) {
477 3         330 $ignore_regex = qr/$default_ignore_regex/x;
478             }
479             else {
480 0         0 $ignore_regex = qr/$ignore_regex/;
481             }
482              
483 3         19 my %patternfiles;
484             my %patternownerlines;
485 3         0 my %patternlicense;
486              
487 3         11 my @paths = @ARGV;
488              
489 3         32 my $do = Path::Iterator::Rule->new;
490 3         34 my %options = (
491             follow_symlinks => 0,
492             );
493              
494             $do->max_depth(1)
495 3 50       23 unless $OPT{recursive};
496 3     3   189 $do->not( sub {/$ignore_regex/} );
  3         961  
497 3         131 $do->file->nonempty;
498              
499 3 50       122 if ( @paths >> 1 ) {
500 0 0 0     0 if ( $log->is_debug or $OPT{skipped} && $log->is_warn ) {
      0        
501 0     0   0 my $dont = $do->clone->not( sub {/$check_regex/} );
  0         0  
502 0         0 foreach ( $dont->all( @paths, \%options ) ) {
503 0 0       0 if ( $OPT{skipped} ) {
504 0         0 $log->warnf( 'skipped file %s', $_ );
505             }
506             else {
507 0         0 $log->debugf( 'skipped file %s', $_ );
508             }
509             }
510             }
511 0     0   0 $do->and( sub {/$check_regex/} );
  0         0  
512             }
513              
514 3         15 foreach my $file ( $do->all( @paths, \%options ) ) {
515 3         495 my ( $license, $copyright ) = $app->parse($file);
516              
517             # drop duplicates
518 3         145 my @copyrights = uniqstr sort { $b cmp $a } split /^/, $copyright;
  0         0  
519 3         17 chomp @copyrights;
520              
521 3 50       26 if ( $OPT{'deb-machine'} ) {
    50          
522 0         0 my @ownerlines_clean = ();
523 0         0 my %owneryears = ();
524 0         0 my $owneryears_seem_correct = 1;
525 0         0 for my $ownerline (@copyrights) {
526 0         0 my ( $owneryear, $owner )
527             = $ownerline =~ /^(\d{4}(?:(?:-|, )\d{4})*)? ?(\S.*)?/;
528 0 0       0 $owneryears_seem_correct = 0 unless ($owneryear);
529 0 0       0 $owner =~ s/,?\s+All Rights Reserved\.?//gi if ($owner);
530             push @ownerlines_clean,
531 0   0     0 join unbackslash( $OPT{'copyright-delimiter'}, ),
      0        
532             $owneryear || (), $owner || ();
533 0   0     0 push @{ $owneryears{ $owner || '' } }, $owneryear;
  0         0  
534             }
535 0         0 my @owners = sort keys %owneryears;
536             @owners = ()
537 0 0 0     0 if ( $OPT{'merge-licenses'} and $owneryears_seem_correct );
538 0         0 my $pattern = join( "\n", $license, @owners );
539 0         0 push @{ $patternfiles{"$pattern"} }, $file;
  0         0  
540 0         0 push @{ $patternownerlines{"$pattern"} }, @ownerlines_clean;
  0         0  
541 0         0 $patternlicense{"$pattern"} = $license;
542             }
543             elsif ( $OPT{machine} ) {
544 3         18 print "$file\t$license";
545             print "\t" . ( join( " / ", @copyrights ) or '*No copyright*' )
546 3 100 50     21 if $OPT{copyright};
547 3         12 print "\n";
548             }
549             else {
550 0         0 print "$file: ";
551 0 0       0 print '*No copyright* ' unless @copyrights;
552 0         0 print $license . "\n";
553             print ' [Copyright: ' . join( ' / ', @copyrights ) . "]\n"
554 0 0 0     0 if @copyrights and $OPT{copyright};
555 0 0       0 print "\n" if $OPT{copyright};
556             }
557             }
558              
559 3 50       0 if ( $OPT{'deb-machine'} ) {
560 0         0 print <<'HEADER';
561             Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
562             Upstream-Name: FIXME
563             Upstream-Contact: FIXME
564             Source: FIXME
565             Disclaimer: Autogenerated by licensecheck
566              
567             HEADER
568 0         0 foreach my $pattern (
569             sort {
570 0 0       0 @{ $patternfiles{$b} } <=> @{ $patternfiles{$a} }
  0         0  
  0         0  
571             || $a cmp $b
572             }
573             keys %patternfiles
574             )
575             {
576             my @ownerlines_unique
577 0         0 = uniqstr sort @{ $patternownerlines{$pattern} };
  0         0  
578 0 0       0 @ownerlines_unique = ('NONE') unless (@ownerlines_unique);
579             print 'Files: ',
580             join(
581             unbackslash( $OPT{'list-delimiter'}, ),
582 0         0 sort @{ $patternfiles{$pattern} }
  0         0  
583             ),
584             "\n";
585             print 'Copyright: ',
586             join(
587 0         0 unbackslash( $OPT{'rfc822-delimiter'}, ),
588             @ownerlines_unique
589             ),
590             "\n";
591 0         0 print "License: $patternlicense{$pattern}\n FIXME\n\n";
592             }
593             }
594              
595             =head1 ENVIRONMENT
596              
597             =over 6
598              
599             =item NO_COLOR
600              
601             If defined, will disable color.
602             Consulted before COLOR.
603              
604             =item COLOR
605              
606             Can be set to 0 to explicitly disable colors.
607             The default is to use color when connected to a terminal.
608              
609             =item LOG_LEVEL
610              
611             =item QUIET
612              
613             =item VERBOSE
614              
615             =item DEBUG
616              
617             =item TRACE
618              
619             Used to emit varying details about discoveries to STDERR
620             when verbosity is not set
621             using either of options B<--quiet>, B<--verbose>, B<--debug> or B<--trace>.
622             See L for more details.
623              
624             =item LOG_PREFIX
625              
626             The default formatter groks these variables.
627             See B in L for more details.
628              
629             =back
630              
631             =head1 CAVEATS
632              
633             The exact output may change between releases,
634             due to the inherently fragile scanning of unstructured data,
635             and the ongoing improvements to detection patterns.
636             For some level of stability,
637             use one of the machine-readable output formats
638             and define a B<--shortname-scheme>.
639              
640             Option B<--deb-fmt> was deprecated since v3.2.
641             Please use option B<--shortname-scheme>=I instead.
642              
643             =cut
644              
645             sub version
646             {
647 0     0     print <<"EOF";
648             This is $progname version $VERSION
649              
650             $COPYRIGHT
651             EOF
652             }
653              
654             =head1 SEE ALSO
655              
656             Other similar tools exist.
657              
658             Here is a list of known tools also command-line based and general-purpose:
659              
660             =over 16
661              
662             =item L
663              
664             Written in Perl.
665              
666             =item L
667              
668             Written in Python.
669              
670             Specific to Debian packages.
671              
672             =item L
673              
674             Written in Python.
675              
676             =item L
677              
678             Written in Ruby.
679              
680             =item L
681              
682             Written in Ruby.
683              
684             =item L
685              
686             Written in C++.
687              
688             Used in L
689             (along with Monk and Nomos apparently unavailable as standalone command-line tools).
690              
691             =item L
692              
693             Written in Go.
694              
695             =item L
696              
697             Written in Python.
698              
699             =back
700              
701             =encoding UTF-8
702              
703             =head1 AUTHOR
704              
705             Jonas Smedegaard C<< >>
706              
707             =head1 COPYRIGHT AND LICENSE
708              
709             This program is based on the script "licensecheck" from the KDE SDK,
710             originally introduced by Stefan Westerfeld C<< >>.
711              
712             Copyright © 2007, 2008 Adam D. Barratt
713              
714             Copyright © 2012 Francesco Poli
715              
716             Copyright © 2016-2022 Jonas Smedegaard
717              
718             Copyright © 2017-2022 Purism SPC
719              
720             This program is free software:
721             you can redistribute it and/or modify it
722             under the terms of the GNU Affero General Public License
723             as published by the Free Software Foundation,
724             either version 3, or (at your option) any later version.
725              
726             This program is distributed in the hope that it will be useful,
727             but WITHOUT ANY WARRANTY;
728             without even the implied warranty
729             of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
730             See the GNU Affero General Public License for more details.
731              
732             You should have received a copy
733             of the GNU Affero General Public License along with this program.
734             If not, see .
735              
736             =cut