File Coverage

blib/lib/App/perlimports/CLI.pm
Criterion Covered Total %
statement 110 165 66.6
branch 33 70 47.1
condition 10 33 30.3
subroutine 20 23 86.9
pod 1 1 100.0
total 174 292 59.5


line stmt bran cond sub pod time code
1             package App::perlimports::CLI;
2              
3 3     3   5725 use Moo;
  3         25963  
  3         15  
4 3     3   6210 use utf8;
  3         43  
  3         16  
5              
6             our $VERSION = '0.000052';
7              
8 3     3   1372 use App::perlimports ();
  3         7  
  3         59  
9 3     3   1274 use App::perlimports::Config ();
  3         19  
  3         117  
10 3     3   1911 use App::perlimports::Document ();
  3         15  
  3         130  
11 3     3   1908 use Capture::Tiny qw( capture_stdout );
  3         59564  
  3         239  
12 3     3   1914 use Getopt::Long::Descriptive qw( describe_options );
  3         101705  
  3         24  
13 3     3   770 use List::Util qw( uniq );
  3         14  
  3         153  
14 3     3   1575 use Log::Dispatch ();
  3         666152  
  3         141  
15 3     3   2023 use Path::Iterator::Rule ();
  3         37328  
  3         125  
16 3     3   29 use Path::Tiny qw( path );
  3         8  
  3         183  
17 3     3   32 use Try::Tiny qw( catch try );
  3         62  
  3         172  
18 3     3   27 use Types::Standard qw( ArrayRef Bool HashRef InstanceOf Object Str );
  3         8  
  3         55  
19              
20             has _args => (
21             is => 'ro',
22             isa => HashRef,
23             lazy => 1,
24             builder => '_build_args',
25             );
26              
27             has _config => (
28             is => 'ro',
29             isa => InstanceOf [App::perlimports::Config::],
30             lazy => 1,
31             builder => '_build_config',
32             );
33              
34             has _config_file => (
35             is => 'ro',
36             isa => Str,
37             lazy => 1,
38             init_arg => 'config',
39             builder => '_build_config_file',
40             );
41              
42             # off by default
43             has _inplace_edit => (
44             is => 'ro',
45             isa => Bool,
46             lazy => 1,
47             default => sub {
48             my $self = shift;
49             return
50             defined $self->_opts->inplace_edit
51             ? $self->_opts->inplace_edit
52             : 0;
53             },
54             );
55              
56             has _json => (
57             is => 'ro',
58             isa => Bool,
59             lazy => 1,
60             default => sub {
61             my $self = shift;
62             return defined $self->_opts->json
63             ? $self->_opts->json
64             : 0;
65             },
66             );
67              
68             has _lint => (
69             is => 'ro',
70             isa => Bool,
71             lazy => 1,
72             default => sub {
73             my $self = shift;
74             return defined $self->_opts->lint
75             ? $self->_opts->lint
76             : 0;
77             },
78             );
79              
80             has _opts => (
81             is => 'ro',
82             isa => InstanceOf ['Getopt::Long::Descriptive::Opts'],
83             lazy => 1,
84             default => sub { $_[0]->_args->{opts} },
85             );
86              
87             # off by default
88             has _read_stdin => (
89             is => 'ro',
90             isa => Bool,
91             lazy => 1,
92             default => sub {
93             my $self = shift;
94             return
95             defined $self->_opts->read_stdin ? $self->_opts->read_stdin
96             : defined $self->_config->{read_stdin}
97             ? $self->_config->{read_stdin}
98             : 0;
99             },
100             );
101              
102             has _usage => (
103             is => 'ro',
104             isa => Object,
105             lazy => 1,
106             default => sub { $_[0]->_args->{usage} },
107             );
108              
109             with 'App::perlimports::Role::Logger';
110              
111             sub _build_args {
112 1     1   62 my ( $opt, $usage ) = describe_options(
113             'perlimports %o',
114             [
115             'filename|f=s',
116             'A file you would like to run perlimports on. Alternatively, just provide a list of one or more file names without a named parameter as the last arguments to this script: perlimports file1 file2 file3'
117             ],
118             [],
119             [
120             'config-file=s',
121             'Path to a perlimports config file. If this parameter is not supplied, we will look for a file called perlimports.toml or .perlimports.toml in the current directory and then look for a perlimports.toml in XDG_CONFIG_HOME (usually something like $HOME/perlimports/perlimports.toml). This behaviour can be disabled via --no-config-file'
122             ],
123             [],
124             [
125             'create-config-file=s',
126             'Create a sample config file using the supplied name and then exit.',
127             { shortcircuit => 1 }
128             ],
129             [],
130             [
131             'ignore-modules=s',
132             'Comma-separated list of modules to ignore.'
133             ],
134             [],
135             [
136             'ignore-modules-pattern=s',
137             'Regular expression that matches modules to ignore.'
138             ],
139             [],
140             [
141             'cache!',
142             '(Experimental and currently discouraged.) Cache some objects in order to speed up subsequent runs. Defaults to no cache.',
143             ],
144             [],
145             [
146             'ignore-modules-filename=s',
147             'Path to file listing modules to ignore. One per line.'
148             ],
149             [],
150             [
151             'ignore-modules-pattern-filename=s',
152             'Path to file listing regular expressions that matches modules to ignore. One per line.'
153             ],
154             [],
155             [
156             'json',
157             '(Experimental) Emit linting results as JSON rather than plain text'
158             ],
159             [],
160             [
161             'never-export-modules=s',
162             'Comma-separated list of modules which do not export symbols.'
163             ],
164             [],
165             [
166             'never-export-modules-filename=s',
167             q{Path to file listing modules which don't export symbols. One per line.}
168             ],
169             [],
170             [ 'inplace-edit|i', 'Edit the file in place.' ],
171             [],
172             [
173             'libs=s',
174             'Comma-separated list of library paths to include (eg --libs lib,t/lib,dev/lib)',
175              
176             ],
177             [],
178             [
179             'lint',
180             'Act as a linter only. Do not edit any files.',
181             ],
182             [],
183             [
184             'no-config-file',
185             'Do not look for a perlimports config file.'
186             ],
187             [],
188             [
189             'padding!',
190             'Pad imports: qw( foo bar ) vs qw(foo bar). Defaults to true',
191             ],
192             [],
193             [
194             'read-stdin',
195             'Read statements to process from STDIN rather than the supplied file.',
196             ],
197             [],
198             [
199             'preserve-duplicates!',
200             'Preserve duplicate use statements for the same module. This is the default behaviour. You are encouraged to disable it.',
201             ],
202             [],
203             [
204             'preserve-unused!',
205             'Preserve use statements for modules which appear to be unused. This is the default behaviour. You are encouraged to disable it.',
206             ],
207             [],
208             [
209             'range-begin=i',
210             'Experimental. First line of range to tidy or lint. Mostly useful for editors.',
211             ],
212             [],
213             [
214             'range-end=i',
215             'Experimental. Last line of range to tidy or lint. Mostly useful for editors.',
216             ],
217             [],
218             [
219             'tidy-whitespace!',
220             'Reformat use statements even when changes are only whitespace. This is the default behaviour.',
221             ],
222             [],
223             [],
224             [ 'version', 'Print installed version', { shortcircuit => 1 } ],
225             [
226             'log-level|l=s', 'Print messages to STDERR',
227             ],
228             [
229             'log-filename=s', 'Log messages to file rather than STDERR',
230             ],
231             [ 'help', 'Print usage message and exit', { shortcircuit => 1 } ],
232             [
233             'verbose-help', 'Print usage message and documentation ',
234             { shortcircuit => 1 }
235             ],
236             );
237              
238 1         7546 return { opts => $opt, usage => $usage, };
239             }
240              
241             sub _build_config {
242 1     1   72 my $self = shift;
243 1         2 my %config;
244 1 50 33     23 if ( !$self->_opts->no_config_file && $self->_config_file ) {
245 1         43 %config = %{ $self->_read_config_file };
  1         5  
246              
247             # The Bool type provided by Types::Standard doesn't seem to like
248             # JSON::PP::Boolean
249 1         10 for my $key ( keys %config ) {
250 13         23 my $maybe_bool = $config{$key};
251 13         24 my $ref = ref $maybe_bool;
252 13 100       22 next unless $ref;
253              
254 8 100 66     33 if ( $ref eq 'JSON::PP::Boolean'
255             || $ref eq 'Types::Serializer::Boolean' ) {
256 5 100       57 $config{$key} = $$maybe_bool ? 1 : 0;
257             }
258             }
259             }
260              
261 1         8 my @config_options = qw(
262             cache
263             ignore_modules_filename
264             ignore_modules_pattern
265             log_filename
266             log_level
267             never_export_modules_filename
268             padding
269             preserve_duplicates
270             preserve_unused
271             tidy_whitespace
272             );
273             my @config_option_lists
274 1         3 = ( 'ignore_modules', 'libs', 'never_export_modules' );
275              
276 0         0 my %args = map { $_ => $self->_opts->$_ }
277 1         3 grep { defined $self->_opts->$_ } @config_options;
  10         288  
278              
279 1         18 for my $list (@config_option_lists) {
280 3         46 my $val = $self->_opts->$list;
281 3 50       36 if ( defined $val ) {
282 0         0 $args{$list} = [ split m{,}, $val ];
283             }
284             }
285 1         15 return App::perlimports::Config->new( %config, %args );
286             }
287              
288             sub _build_config_file {
289 1     1   53 my $self = shift;
290              
291 1 50       16 if ( $self->_opts->config_file ) {
292 0 0       0 if ( !-e $self->_opts->config_file ) {
293 0         0 die $self->_opts->config_file . ' not found';
294             }
295 0         0 return $self->_opts->config_file;
296             }
297              
298 1         36 my @filenames = ( 'perlimports.toml', '.perlimports.toml', );
299              
300 1         4 for my $name (@filenames) {
301 1 50       45 return $name if -e $name;
302             }
303              
304 0         0 require File::XDG;
305              
306 0         0 my $xdg_config = File::XDG->new( name => 'perlimports', api => 1 );
307 0         0 my $file = $xdg_config->config_home->child( $filenames[0] );
308 0 0       0 return -e $file ? "$file" : q{};
309             }
310              
311             sub _read_config_file {
312 1     1   2 my $self = shift;
313              
314 1         516 require TOML::Tiny;
315 1         99703 my $config = TOML::Tiny::from_toml( path( $self->_config_file )->slurp );
316 1   50     9465 return $config || {};
317             }
318              
319             ## no critic (Subroutines::ProhibitExcessComplexity)
320             sub run {
321 1     1 1 7759 my $self = shift;
322 1         35 my $opts = $self->_opts;
323              
324 1 50 0     87 ( print $VERSION, "\n" ) && return 0 if $opts->version;
325 1 50 0     22 ( print $self->_usage->text ) && return 0 if $opts->help;
326              
327 1 50       7 if ( $opts->verbose_help ) {
328 0         0 require Pod::Usage; ## no perlimports
329 0         0 my $fh = \*STDOUT;
330 0         0 Pod::Usage::pod2usage(
331             (
332             {
333             -exitval => 'NOEXIT',
334             -message => $self->_usage->text,
335             -output => $fh,
336             }
337             )
338             );
339 0         0 return 0;
340             }
341              
342 1 50       8 if ( $opts->create_config_file ) {
343 0         0 my $exit_code = 0;
344             try {
345 0     0   0 App::perlimports::Config->create_config(
346             $opts->create_config_file );
347             }
348             catch {
349 0     0   0 print STDERR $_, "\n";
350 0         0 $exit_code = 1;
351 0         0 };
352 0         0 return $exit_code;
353             }
354              
355 1         8 my $input;
356             my $selection;
357 1         0 my $tmp_file;
358              
359 1 50       25 if ( $self->_read_stdin ) {
360             ## no critic (Variables::RequireInitializationForLocalVars)
361 0         0 local $/;
362 0         0 $input = <>;
363 0 0 0     0 if ( $opts->range_begin && $opts->range_end ) {
364 0         0 $tmp_file = Path::Tiny->tempfile('perlimportsXXXXXXXX');
365 0         0 $tmp_file->spew($input);
366 0         0 my @lines = split( qr{\n}, $input );
367 0         0 my $end = $opts->range_end;
368 0 0       0 if ( $end > scalar @lines + 1 ) {
369 0         0 $end = scalar @lines + 1;
370             }
371 0         0 $selection = join "\n",
372             @lines[ $opts->range_begin - 1 .. $end - 1 ];
373             }
374             else {
375 0         0 $selection = $input;
376             }
377             }
378              
379 1         306 unshift @INC, @{ $self->_config->libs };
  1         19  
380              
381 1 50       78 my $logger
    50          
382             = $self->_has_logger
383             ? $self->logger
384             : Log::Dispatch->new(
385             outputs => [
386             $self->_config->log_filename
387             ? [
388             'File',
389             binmode => ':encoding(UTF-8)',
390             filename => $self->_config->log_filename,
391             min_level => $self->_config->log_level,
392             mode => '>>',
393             newline => 1,
394             ]
395             : [
396             'Screen',
397             min_level => $self->_config->log_level,
398             newline => 1,
399             stderr => 1,
400             utf8 => 1,
401             ]
402             ]
403             );
404              
405 1 50 33     68206 if ( $self->_json && !$self->_lint ) {
406 0         0 $logger->error('--json can only be used with --lint');
407 0         0 return 1;
408             }
409              
410 1 50 33     88 if ( $self->_lint && $self->_inplace_edit ) {
411 0         0 $logger->error('Cannot lint if inplace edit has been enabled');
412 0         0 return 1;
413             }
414              
415 1 50 33     78 if ( ( $opts->range_begin && !$opts->range_end )
      33        
      33        
416             || ( $opts->range_end && !$opts->range_begin ) ) {
417 0         0 $logger->error('You must supply both range_begin and range_end');
418 0         0 return 1;
419             }
420              
421 1 50 33     23 if ( $opts->range_begin && !$self->_read_stdin ) {
422 0         0 $logger->error(
423             'You must specify --read-stdin if you provide a range');
424 0         0 return 1;
425             }
426              
427 1 50       14 my @files = $tmp_file ? ("$tmp_file") : _filter_paths(
    50          
428             $opts->filename ? $opts->filename : (),
429             @ARGV
430             );
431              
432 1 50       9 unless (@files) {
433 0         0 $logger->error(q{Mandatory parameter 'filename' missing});
434 0         0 $logger->error( $self->_usage->text );
435 0         0 return 1;
436             }
437              
438             my %doc_args = (
439             cache => $self->_config->cache,
440 1         68 @{ $self->_config->ignore }
441             ? ( ignore_modules => $self->_config->ignore )
442             : (),
443 1         79 @{ $self->_config->ignore_pattern }
444             ? ( ignore_modules_pattern => $self->_config->ignore_pattern )
445             : (),
446 1 50       28 @{ $self->_config->never_export }
  1 50       87  
    50          
    50          
447             ? ( never_export_modules => $self->_config->never_export )
448             : (),
449             json => $self->_json,
450             lint => $self->_lint,
451             logger => $logger,
452             padding => $self->_config->padding,
453             preserve_duplicates => $self->_config->preserve_duplicates,
454             preserve_unused => $self->_config->preserve_unused,
455             tidy_whitespace => $self->_config->tidy_whitespace,
456             $selection ? ( selection => $selection ) : (),
457             );
458              
459 1         371 my $exit_code = 0;
460             FILENAME:
461 1         72 foreach my $filename (@files) {
462 1 50       9 if ( !path($filename)->is_file ) {
463 0         0 $logger->error("$filename does not appear to be a file");
464 0         0 $logger->error( $self->_usage->text );
465 0         0 return 1;
466             }
467              
468 1         103 $logger->notice( '🚀 Starting file: ' . $filename );
469              
470 1         64 my $pi_doc = App::perlimports::Document->new(
471             %doc_args,
472             filename => $filename,
473             );
474              
475             # Capture STDOUT here so that 3rd party code printing to STDOUT doesn't get
476             # piped back into vim.
477 1         84 my ( $stdout, $tidied, $linter_success );
478              
479 1 50       28 if ( $self->_lint ) {
480             ( $stdout, $linter_success ) = capture_stdout(
481             sub {
482 0     0   0 return $pi_doc->linter_success;
483             }
484 0         0 );
485 0 0       0 if ($linter_success) {
486 0         0 $logger->error( $filename . ' OK' );
487             }
488             else {
489 0         0 $exit_code = 1;
490             }
491 0         0 next FILENAME;
492             }
493              
494             ( $stdout, $tidied ) = capture_stdout(
495             sub {
496 1     1   1084 return $pi_doc->tidied_document;
497             }
498 1         51 );
499              
500 1 50       976 if ( $self->_read_stdin ) {
    50          
501 0         0 print STDOUT $tidied;
502             }
503             elsif ( $self->_inplace_edit ) {
504              
505             # append() with truncate, because spew() can change file permissions
506 0         0 path($filename)->append( { truncate => 1 }, $tidied );
507             }
508              
509             else {
510 1         149 print STDOUT $tidied;
511             }
512             }
513 1         222 return $exit_code;
514             }
515              
516             ## use critic
517              
518             sub _filter_paths {
519 1     1   16 my @paths = @_;
520 1         3 my @files;
521 1         14 my $rule = Path::Iterator::Rule->new->or(
522             Path::Iterator::Rule->new->perl_module,
523             Path::Iterator::Rule->new->perl_script,
524             Path::Iterator::Rule->new->perl_test,
525             );
526              
527 1         896 foreach my $path (@paths) {
528 1 50       24 if ( -d $path ) {
529 0         0 my $iter = $rule->iter($path);
530 0         0 while ( defined( my $file = $iter->() ) ) {
531 0         0 push @files, $file;
532             }
533             }
534             else {
535 1         4 push @files, $path;
536             }
537             }
538 1         41 return uniq @files;
539             }
540              
541             1;
542              
543             =pod
544              
545             =encoding UTF-8
546              
547             =head1 NAME
548              
549             App::perlimports::CLI - CLI arg parsing for C<perlimports>
550              
551             =head1 VERSION
552              
553             version 0.000052
554              
555             =head1 DESCRIPTION
556              
557             This module isn't really meant to provide a public interface.
558              
559             =head2 run()
560              
561             The method which will do the argument parsing and print out the results.
562              
563             =head1 AUTHOR
564              
565             Olaf Alders <olaf@wundercounter.com>
566              
567             =head1 COPYRIGHT AND LICENSE
568              
569             This software is copyright (c) 2020 by Olaf Alders.
570              
571             This is free software; you can redistribute it and/or modify it under
572             the same terms as the Perl 5 programming language system itself.
573              
574             =cut
575              
576             __END__
577              
578             # ABSTRACT: CLI arg parsing for C<perlimports>
579