File Coverage

blib/lib/App/PTP/Args.pm
Criterion Covered Total %
statement 146 186 78.4
branch 11 30 36.6
condition 5 15 33.3
subroutine 68 88 77.2
pod 0 13 0.0
total 230 332 69.2


line stmt bran cond sub pod time code
1             package App::PTP::Args;
2              
3 20     20   68136 use 5.022;
  20         84  
4 20     20   97 use strict;
  20         32  
  20         391  
5 20     20   99 use warnings;
  20         41  
  20         687  
6              
7 20     20   9576 use App::PTP::Commands ':CMD';
  20         71  
  20         4393  
8 20     20   149 use App::PTP::Util;
  20         54  
  20         534  
9 20         124 use Getopt::Long qw(GetOptionsFromArray :config auto_abbrev no_ignore_case
10 20     20   14079 permute auto_version);
  20         239325  
11 20     20   4605 use List::Util;
  20         46  
  20         1040  
12 20     20   10478 use Pod::Usage;
  20         964024  
  20         3145  
13 20     20   203 use Scalar::Util 'looks_like_number';
  20         42  
  20         62949  
14              
15             # Name of files or directory to be processed. This can also contain a reference
16             # to the $stdin_marker variable, to indicate that the standard input needs to be
17             # processed.
18             my @inputs;
19              
20             # The list of actions applied to the input. This is a list of array reference.
21             # Each of these array will contain the name of the command to run, the coderef
22             # for it, and then its arguments if any.
23             my @pipeline;
24              
25             # This hash contains options that are used during the pipeline and that can be
26             # set or un-set for each command.
27             my %modes;
28             # This hash contains options that are global for the whole program.
29             my %options;
30              
31             my $default_input_field = '\s*,\s*|\t';
32             my $default_output_field = "\t";
33              
34             # env(foo => default)
35             # Returns the given environment variable or the default value.
36             # Always return the default value if the HARNESS_ACTIVE variable is set (so that
37             # tests are not affected by environment variables).
38             sub env {
39 966     966 0 1669 my ($var, $default) = @_;
40 966 50       2889 return $default if $ENV{HARNESS_ACTIVE};
41 0   0     0 return $ENV{$var} // $default;
42             }
43              
44             sub get_default_modes {
45 138     138 0 242 my %m;
46 138         340 $m{case_sensitive} = not(env(PTP_DEFAULT_CASE_INSENSITIVE => 0));
47 138         321 $m{quote_regex} = env(PTP_DEFAULT_QUOTE_REGEX => 0);
48 138         290 $m{global_match} = not(env(PTP_DEFAULT_LOCAL_MATCH => 0));
49 138         310 $m{comparator} = \"default";
50 138         309 $m{regex_engine} = env(PTP_DEFAULT_REGEX_ENGINE => 'perl');
51 138         288 $m{fatal_error} = env(PTP_DEFAULT_FATAL_ERROR => 0);
52 138         300 $m{inverse_match} = env(PTP_DEFAULT_INVERSE_MATCH => 0);
53 138         320 $m{input_field} = $default_input_field;
54 138         611 $m{output_field} = $default_output_field;
55 138         1183 return %m;
56             }
57              
58             sub get_default_options {
59 138     138 0 216 my %o;
60 138         308 $o{input_encoding} = 'UTF-8';
61 138         298 $o{output_encoding} = 'UTF-8';
62 138         285 $o{input_separator} = '\n'; # This will be interpreted in a regex
63 138         281 $o{output_separator} = "\n";
64 138         298 $o{preserve_eol} = 0;
65 138         239 $o{fix_final_separator} = 0;
66 138         271 $o{recursive} = 0;
67 138         237 $o{input_filter} = undef;
68 138         258 $o{debug_mode} = 0;
69 138         258 $o{merge} = 0;
70 138         266 $o{in_place} = 0;
71 138         247 $o{output} = undef;
72 138         254 $o{append} = 0;
73 138         229 $o{abort} = 0;
74 138         252 $o{preserve_perl_env} = 0;
75 138         246 $o{use_safe} = env(PTP_DEFAULT_SAFE => 0);
76 138         1215 return %o;
77             }
78              
79             # Resets all the global variables used for the command line parsing. This is
80             # really useful only in tests.
81             sub reset_global {
82 138     138 0 335 @inputs = ();
83 138         803 @pipeline = ();
84 138         483 %modes = get_default_modes();
85 138         434 %options = get_default_options();
86             }
87              
88             sub set_output {
89 0     0 0 0 my (undef, $f) = @_;
90 0 0       0 if (defined $options{output}) {
91 0         0 die "Only a single occurence of --output or --append is allowed.\n";
92             }
93 0         0 $options{output} = $f;
94             }
95              
96             sub options_flags {(
97 0     0   0 'help|h' => sub { pod2usage(-exitval => 0, -verbose => 2) },
98             'debug|d+' => \$options{debug_mode},
99             'merge|m!' => \$options{merge},
100             'in-place|i!' => \$options{in_place},
101             'output|o=s' => \&set_output,
102 0     0   0 'append|a=s' => sub { set_output(@_); $options{append} = 1; },
  0         0  
103             'abort!' => \$options{abort},
104              
105             'recursive|R|r!' => \$options{recursive},
106             'input-filter=s' => \$options{input_filter},
107             'input-encoding|in-encoding=s' => \$options{input_encoding},
108             'output-encoding|out-encoding=s' => \$options{output_encoding},
109             'input-separator|in-separator=s' => \$options{input_separator},
110             'output-separator|out-separator=s' => \$options{output_separator},
111             'fix-final-separator!' => \$options{fix_final_separator},
112 1     1   5449 '0' => sub { $options{input_separator} = '\000';
113 1         3 $options{output_separator} = '' },
114 1     1   5063 '00' => sub { $options{output_separator} = "\000" },
115             'preserve-input-separator|eol' =>
116 1     1   5078 sub { $options{preserve_eol} = 1; $options{output_separator} = '' },
  1         6  
117             'preserve-perl-env!' => \$options{preserve_perl_env},
118 36     36   182659 'safe:2' => sub { $options{use_safe} = $_[1] },
119 139     139 0 2387 )}
120              
121             sub modes_flags {(
122 1     1   240 'case-sensitive|S' => sub { $modes{case_sensitive} = 1 },
123 3     3   10424 'case-insensitive|I' => sub { $modes{case_sensitive} = 0 },
124 11     11   52601 'quote-regexp|Q' => sub { $modes{quote_regex} = 1 },
125 0     0   0 'end-quote-regexp|E' => sub { $modes{quote_regex} = 0 },
126 1     1   254 'global-match|G' => sub { $modes{global_match} = 1 },
127 2     2   10531 'local-match|L' => sub { $modes{global_match} = 0 },
128 1     1   5027 'comparator|C=s' => sub { $modes{comparator} = $_[1] },
129             'regex-engine|re=s' =>
130 0 0   0   0 sub { die "Invalid value for --regex-engine: $_[1]\n" if $_[1] !~ /^\w+$/;
131 0         0 $modes{regex_engine} = $_[1] },
132 0     0   0 'fatal-error|X' => sub { $modes{fatal_error} = 1 },
133 0     0   0 'ignore-error' => sub { $modes{fatal_error} = 0 }, # Find a short option?
134 1     1   5056 'inverse-match|V' => sub { $modes{inverse_match} = 1 },
135 0     0   0 'normal-match|N' => sub { $modes{inverse_match} = 0 },
136 1     1   5012 'input-field-separator|F=s' => sub { $modes{input_field} = $_[1] },
137             'output-field-separator|P=s' => \$modes{output_field},
138 0     0   0 'default' => sub { $modes{input_field} = $default_input_field;
139 0         0 $modes{output_field} = $default_output_field; },
140 1     1   5172 'bytes' => sub { $modes{input_field} = ''; $modes{output_field} = ''; },
  1         3  
141 1     1   5164 'csv' => sub { $modes{input_field} = '\s*,\s*'; $modes{output_field} = ','; },
  1         3  
142 1     1   5065 'tsv' => sub { $modes{input_field} = '\t'; $modes{output_field} = "\t"; },
  1         3  
143 1     1   5019 'none' => sub { $modes{input_field} = '(?!)' },
144 139     139 0 3395 )}
145              
146             sub input_flags {(
147 25     25   40673 '<>' => sub { push @inputs, $_[0] }, # Any options not matched otherwise.
148 9     9   779 '' => sub { push @inputs, \$App::PTP::Files::stdin_marker }, # a single '-'
149 139     139 0 791 )}
150              
151             sub is_int {
152 10     10 0 20 my ($str) = @_;
153 10   33     72 return looks_like_number($str) && int($str) == $str;
154             }
155              
156             sub validate_cut_spec {
157 5     5 0 25 my ($spec) = @_;
158 5         33 my @fields = split /\s*,\s*/, $spec;
159 5         17 for my $f (@fields) {
160 10 50       20 die "Fields passed to --cut must all be integers: $f\n" unless is_int($f);
161 10 50       40 $f-- if $f > 0;
162             }
163 5         23 return \@fields;
164             }
165              
166             # The array associated with each action contains the name of the action, the
167             # method to call for that action, a copy of the current %modes, and all the
168             # other arguments that should be passed to the method.
169             sub action_flags {(
170             'grep|g=s' =>
171 27     27   42244 sub { push @pipeline, ['grep', \&do_grep, {%modes}, $_[1]] },
172             'substitute|s=s{2}' =>
173 38     38   46261 sub { push @pipeline, ['substitute', \&do_substitute, {%modes},
174             $_[1]] },
175             # All the do_perl below could have the same sub using "$_[0]" instead of the
176             # manually specified name.
177             'perl|p=s' =>
178 19     19   34347 sub { push @pipeline, ['perl', \&do_perl, {%modes}, 'perl', $_[1]] },
179             'n=s' =>
180 21     21   25845 sub { push @pipeline, ['n', \&do_perl, {%modes}, 'n', $_[1]] },
181             'filter|f=s' =>
182 6     6   11174 sub { push @pipeline, ['filter', \&do_perl, {%modes}, 'filter', $_[1]] },
183             'mark-line|ml=s' =>
184 7     7   6650 sub { push @pipeline, ['mark-line', \&do_perl, {%modes}, 'mark-line',
185             $_[1]] },
186             'execute|e=s' =>
187 20     20   31734 sub { push @pipeline, ['execute', \&do_execute, {%modes}, 'execute',
188             $_[1]] },
189             'M=s' =>
190 1     1   5034 sub { push @pipeline, ['M', \&do_execute, {%modes}, 'M', $_[1]] },
191             'load|l=s' =>
192 3     3   795 sub { push @pipeline, ['load', \&do_load, {%modes}, $_[1]] },
193 7     7   21430 'sort' => sub { push @pipeline, ['sort', \&do_sort, {%modes}] },
194             'numeric-sort|ns' =>
195 1     1   5054 sub { my $opt = {%modes, comparator => \"numeric" };
196 1         5 push @pipeline, [ 'numeric-sort', \&do_sort, $opt] },
197             'locale-sort|ls' =>
198 1     1   5073 sub { my $opt = {%modes, comparator => \"locale" };
199 1         5 push @pipeline, [ 'numeric-sort', \&do_sort, $opt] },
200             'custom-sort|cs=s' =>
201 1     1   5033 sub { my $opt = {%modes, comparator => $_[1] };
202 1         6 push @pipeline, [ 'custom-sort', \&do_sort, $opt] },
203             'unique|uniq|u' =>
204 4     4   6309 sub { push @pipeline, ['unique', \&do_list_op, {%modes},
205             \&App::PTP::Util::uniqstr, 'together'] },
206             'global-unique|guniq|gu' =>
207 2     2   5197 sub { push @pipeline, ['global-unique', \&do_list_op, {%modes},
208             \&App::PTP::Util::globaluniqstr, 'together'] },
209 4     4   20207 'head:i' => sub { push @pipeline, ['head', \&do_head, {%modes}, $_[1]] },
210 4     4   19900 'tail:i' => sub { push @pipeline, ['tail', \&do_tail, {%modes}, $_[1]] },
211             'reverse|tac' =>
212             sub { push @pipeline,
213 0     0   0 ['reverse', \&do_list_op, {%modes}, sub {reverse @_ }, 'same'] },
  0         0  
214             'shuffle' =>
215 0     0   0 sub { push @pipeline, ['shuffle', \&do_list_op, {%modes},
216             \&List::Util::shuffle, 'none'] },
217 1     1   279 'eat' => sub { push @pipeline, ['eat', \&do_eat, {%modes}] },
218             'delete-marked' =>
219 3     3   760 sub { push @pipeline, ['delete-marked', \&do_delete_marked, {%modes},
220             0] },
221             'delete-before' =>
222 0     0   0 sub { push @pipeline, ['delete-before', \&do_delete_marked, {%modes},
223             -1] },
224             'delete-after' =>
225 0     0   0 sub { push @pipeline, ['delete-after', \&do_delete_marked, {%modes},
226             1] },
227             'delete-at-offset=i' =>
228 0     0   0 sub { push @pipeline, ['delete-at-offset', \&do_delete_marked, {%modes},
229             $_[1]] },
230             'insert-before=s' =>
231 0     0   0 sub { push @pipeline, ['insert-before', \&do_insert_marked, {%modes},
232             -1, $_[1]] },
233             'insert-after=s' =>
234 3     3   765 sub { push @pipeline, ['insert-after', \&do_insert_marked, {%modes},
235             0, $_[1]] },
236             'insert-at-offset=s{2}' =>
237 0     0   0 sub { push @pipeline, ['insert-at-offset', \&do_insert_marked, {%modes},
238             $_[1]] },
239             'clear-markers' =>
240 0     0   0 sub { push @pipeline, ['clear-markers', \&do_set_markers, {%modes}, 0] },
241             'set-all-markers' =>
242 0     0   0 sub { push @pipeline, ['set-all-markers', \&do_set_markers, {%modes},
243             1] },
244 5     5   6442 'cut=s' => sub { push @pipeline, ['cut', \&do_cut, {%modes},
245             validate_cut_spec($_[1])] },
246 0     0   0 'paste=s' => sub { push @pipeline, ['paste', \&do_paste, {%modes}, $_[1]] },
247 3     3   5696 'pivot' => sub { push @pipeline, ['pivot', \&do_pivot, {%modes}, 'pivot'] },
248 1     1   4933 'anti-pivot' => sub { push @pipeline, ['anti-pivot', \&do_pivot, {%modes},
249             'anti-pivot'] },
250 2     2   5274 'transpose' => sub { push @pipeline, ['transpose', \&do_pivot, {%modes},
251             'transpose'] },
252             'number-lines|nl' =>
253 0     0   0 sub { push @pipeline, ['number-lines', \&do_number_lines, {%modes}] },
254             'file-name|fn' =>
255 2     2   9967 sub { push @pipeline, ['file-name', \&do_file_name, {%modes}, 1] },
256             'prefix-file-name|pfn' =>
257 2     2   5626 sub { push @pipeline, ['prefix-file-name', \&do_file_name, {%modes}, 0] },
258             'line-count|lc' =>
259 1     1   291 sub { push @pipeline, ['line-count', \&do_line_count, {%modes}] },
260 6     6   1561 'tee=s' => sub { push @pipeline, ['tee', \&do_tee, {%modes}, $_[1]] },
261 6     6   23519 'shell=s' => sub { push @pipeline, ['shell', \&do_shell, {%modes}, 'shell',
262             $_[1]] }
263 139     139 0 7745 )}
264              
265             sub all_args {
266 139     139 0 101830 return (options_flags(), modes_flags(), input_flags(), action_flags());
267             }
268              
269             # parse_command_line(\@args)
270             sub parse_command_line {
271 138     138 0 315 my ($args) = @_;
272 138         457 reset_global();
273 138 50       447 GetOptionsFromArray($args, all_args())
274             or pod2usage(-exitval => 2, -verbose => 0);
275            
276 138 50       59137 if ($options{debug_mode} > 1) {
277             # When -d is specified multiple times, we add the marker on the final
278             # output.
279 0         0 push @pipeline, ['show-marker', \&do_perl, {%modes}, 'perl',
280             'pf "%s %s", ($m ? "*" : " "), $_']
281             }
282              
283             # Because of the way the options are processed, each --replace options
284             # (expecting two arguments) is pushed twice in the pipeline sub (once for each
285             # argument). We're fixing this here.
286 138         588 for my $i (0 .. $#pipeline) {
287 201 100       753 if ($pipeline[$i][0] eq 'substitute') {
    50          
288 19         41 push @{$pipeline[$i]}, $pipeline[$i+1]->[3];
  19         70  
289 19         53 $pipeline[$i+1][0] = 'garbage';
290             } elsif ($pipeline[$i][0] eq 'insert-at-offset') {
291 0         0 my $o = $pipeline[$i]->[3];
292 0 0       0 if (!ist_int($o)) {
293 0         0 die "The first argument to --insert-at-offset must be an integer: $o\n";
294             }
295 0         0 push @{$pipeline[$i]}, $pipeline[$i+1]->[3];
  0         0  
296 0         0 $pipeline[$i+1][0] = 'garbage';
297             }
298             }
299 138         326 @pipeline = grep { $_->[0] ne 'garbage' } @pipeline;
  201         719  
300              
301             # Add any options that were passed after a '--' to the list of inputs.
302 138         296 push @inputs, @$args;
303              
304             # Add the standard input marker to the inputs if no other input were
305             # specified.
306 138 100       419 push @inputs, \$App::PTP::Files::stdin_marker if not @inputs;
307              
308 138 0 33     401 if ($options{in_place} && $options{merge}) {
309 0         0 die "The --in-place and --merge options are incompatible.\n";
310             }
311              
312 138 0 33     343 if ($options{in_place} && $options{output}) {
313 0 0       0 if ($options{append}) {
314 0         0 die "The --in-place and --append options are incompatible.\n";
315             } else {
316 0         0 die "The --in-place and --output options are incompatible.\n";
317             }
318             }
319            
320 138 50 66     397 if (defined $options{input_filter} && !$options{recursive}) {
321 0         0 print "WARNING: The --input-filter option is useless unless --recursive is specified too.\n";
322             }
323            
324 138         549 return (\@inputs, \@pipeline, \%options);
325             }
326              
327             1;