File Coverage

blib/lib/Complete/File.pm
Criterion Covered Total %
statement 110 125 88.0
branch 76 106 71.7
condition 28 41 68.2
subroutine 16 16 100.0
pod 2 2 100.0
total 232 290 80.0


line stmt bran cond sub pod time code
1             package Complete::File;
2              
3             our $DATE = '2021-02-08'; # DATE
4             our $VERSION = '0.443'; # VERSION
5              
6 2     2   182557 use 5.010001;
  2         27  
7 2     2   11 use strict;
  2         4  
  2         43  
8 2     2   9 use warnings;
  2         4  
  2         62  
9              
10 2     2   990 use Complete::Common qw(:all);
  2         695  
  2         267  
11 2     2   1128 use Complete::Util qw(hashify_answer);
  2         12665  
  2         3285  
12              
13             require Exporter;
14             our @ISA = qw(Exporter);
15             our @EXPORT_OK = qw(
16             complete_file
17             complete_dir
18             );
19              
20             our %SPEC;
21              
22             $SPEC{':package'} = {
23             v => 1.1,
24             summary => 'Completion routines related to files',
25             };
26              
27             $SPEC{complete_file} = {
28             v => 1.1,
29             summary => 'Complete file and directory from local filesystem',
30             args => {
31             %arg_word,
32             filter => {
33             summary => 'Only return items matching this filter',
34             description => <<'_',
35              
36             Filter can either be a string or a code.
37              
38             For string filter, you can specify a pipe-separated groups of sequences of these
39             characters: f, d, r, w, x. Dash can appear anywhere in the sequence to mean
40             not/negate. An example: `f` means to only show regular files, `-f` means only
41             show non-regular files, `drwx` means to show only directories which are
42             readable, writable, and executable (cd-able). `wf|wd` means writable regular
43             files or writable directories.
44              
45             For code filter, you supply a coderef. The coderef will be called for each item
46             with these arguments: `$name`. It should return true if it wants the item to be
47             included.
48              
49             _
50             schema => ['any*' => {of => ['str*', 'code*']}],
51             tags => ['category:filtering'],
52             },
53             file_regex_filter => {
54             summary => 'Filter shortcut for file regex',
55             description => <<'_',
56              
57             This is a shortcut for constructing a filter. So instead of using `filter`, you
58             use this option. This will construct a filter of including only directories or
59             regular files, and the file must match a regex pattern. This use-case is common.
60              
61             _
62             schema => 're*',
63             tags => ['category:filtering'],
64             },
65             exclude_dir => {
66             schema => 'bool*',
67             description => <<'_',
68              
69             This is also an alternative to specifying full `filter`. Set this to true if you
70             do not want directories.
71              
72             If you only want directories, take a look at `complete_dir()`.
73              
74             _
75             tags => ['category:filtering'],
76             },
77             file_ext_filter => {
78             schema => ['any*', of=>['re*', ['array*',of=>'str*']]],
79             description => <<'_',
80              
81             This is also an alternative to specifying full `filter` or `file_regex_filter`.
82             You can set this to a regex or a set of extensions to accept. Note that like in
83             `file_regex_filter`, directories of any name is also still allowed.
84              
85             _
86             tags => ['category:filtering'],
87             },
88             starting_path => {
89             schema => 'str*',
90             default => '.',
91             },
92             handle_tilde => {
93             schema => 'bool',
94             default => 1,
95             },
96             allow_dot => {
97             summary => 'If turned off, will not allow "." or ".." in path',
98             description => <<'_',
99              
100             This is most useful when combined with `starting_path` option to prevent user
101             going up/outside the starting path.
102              
103             _
104             schema => 'bool',
105             default => 1,
106             },
107             recurse => {
108             schema => 'bool*',
109             cmdline_aliases => {r=>{}},
110             },
111             recurse_matching => {
112             schema => ['str*', in=>['level-by-level', 'all-at-once']],
113             default => 'level-by-level',
114             },
115             exclude_leaf => {
116             schema => 'bool*',
117             },
118             exclude_dir => {
119             schema => 'bool*',
120             },
121             },
122             args_rels => {
123             dep_all => [recurse_matching => ['recurse']],
124             },
125             result_naked => 1,
126             result => {
127             schema => 'array',
128             },
129             };
130             sub complete_file {
131 13     13 1 10647 require Complete::Path;
132 13         5199 require Encode;
133 13         20577 require File::Glob;
134              
135 13         53 my %args = @_;
136 13   50     35 my $word = $args{word} // "";
137 13   50     50 my $handle_tilde = $args{handle_tilde} // 1;
138 13   50     39 my $allow_dot = $args{allow_dot} // 1;
139              
140             # if word is starts with "~/" or "~foo/" replace it temporarily with user's
141             # name (so we can restore it back at the end). this is to mimic bash
142             # support. note that bash does not support case-insensitivity for "foo".
143 13         16 my $result_prefix;
144 13   50     34 my $starting_path = $args{starting_path} // '.';
145 13 50 33     104 if ($handle_tilde && $word =~ s!\A(~[^/]*)/!!) {
    50 33        
146 0         0 $result_prefix = "$1/";
147 0         0 my @dir = File::Glob::bsd_glob($1); # glob will expand ~foo to /home/foo
148 0 0       0 return [] unless @dir;
149 0         0 $starting_path = Encode::decode('UTF-8', $dir[0]);
150             } elsif ($allow_dot && $word =~ s!\A((?:\.\.?/+)+|/+)!!) {
151             # just an optimization to skip sequences of '../'
152 0         0 $starting_path = $1;
153 0         0 $result_prefix = $1;
154 0 0       0 $starting_path =~ s#/+\z## unless $starting_path =~ m!\A/!;
155             }
156              
157             # bail if we don't allow dot and the path contains dot
158 13 50 33     28 return [] if !$allow_dot &&
159             $word =~ m!(?:\A|/)\.\.?(?:\z|/)!;
160              
161             # prepare list_func
162             my $list = sub {
163 24     24   2330 my ($path, $intdir, $isint) = @_;
164 24 50       689 opendir my($dh), $path or return undef;
165 24         63 my @res;
166 24         556 for (sort readdir $dh) {
167             # skip . and .. if leaf is empty, like in bash
168 206 100 100     5703 next if ($_ eq '.' || $_ eq '..') && $intdir eq '';
      100        
169 186 100 100     905 next if $isint && !(-d "$path/$_");
170 158         363 push @res, Encode::decode('UTF-8', $_);
171             }
172 24         1096 \@res;
173 13         63 };
174              
175             # prepare filter_func
176              
177             # from the filter option
178 13         20 my $filter;
179 13 100 100     58 if ($args{filter} && !ref($args{filter})) {
    100 66        
180 2         10 my @seqs = split /\s*\|\s*/, $args{filter};
181             $filter = sub {
182 6     6   14 my $name = shift;
183 6 50       79 my @st = stat($name) or return 0;
184 6         15 my $mode = $st[2];
185 6         6 my $pass;
186             SEQ:
187 6         12 for my $seq (@seqs) {
188 7         24 my $neg = sub { $_[0] };
  3         10  
189 7         19 for my $c (split //, $seq) {
190 11 100       35 if ($c eq '-') { $neg = sub { $_[0] ? 0 : 1 } }
  4 100       14  
  4 50       14  
    50          
    50          
    100          
    50          
191 0 0       0 elsif ($c eq 'r') { next SEQ unless $neg->($mode & 0400) }
192 0 0       0 elsif ($c eq 'w') { next SEQ unless $neg->($mode & 0200) }
193 0 0       0 elsif ($c eq 'x') { next SEQ unless $neg->($mode & 0100) }
194 1 50       3 elsif ($c eq 'f') { next SEQ unless $neg->($mode & 0100000)}
195 6 100       13 elsif ($c eq 'd') { next SEQ unless $neg->($mode & 0040000)}
196             else {
197 0         0 die "Unknown character in filter: $c (in $seq)";
198             }
199             }
200 3         5 $pass = 1; last SEQ;
  3         8  
201             }
202 6         17 $pass;
203 2         10 };
204             } elsif ($args{filter} && ref($args{filter}) eq 'CODE') {
205 1         2 $filter = $args{filter};
206             }
207              
208             # from the file_regex_filter option
209 13         16 my $filter_fregex;
210 13 100       23 if ($args{file_regex_filter}) {
211             $filter_fregex = sub {
212 11     11   15 my $name = shift;
213 11 100       115 return 1 if -d $name;
214 7 50       19 return 0 unless -f _;
215 7 100       44 return 1 if $name =~ $args{file_regex_filter};
216 5         19 0;
217 1         4 };
218             }
219              
220             # from the file_ext_filter option
221 13         17 my $filter_fext;
222 13 100 100     52 if ($args{file_ext_filter} && ref $args{file_ext_filter} eq 'Regexp') {
    100 66        
223             $filter_fext = sub {
224 3     3   5 my $name = shift;
225 3 50       40 return 1 if -d $name;
226 3 50       9 return 0 unless -f _;
227 3 50       37 my $ext = $name =~ /\.(\w+)\z/ ? $1 : '';
228 3 100       20 return 1 if $ext =~ $args{file_ext_filter};
229 1         5 0;
230 1         4 };
231             } elsif ($args{file_ext_filter} && ref $args{file_ext_filter} eq 'ARRAY') {
232             $filter_fext = sub {
233 3     3   4 my $name = shift;
234 3 50       40 return 1 if -d $name;
235 3 50       10 return 0 unless -f _;
236 3 50       26 my $ext = $name =~ /\.(\w+)\z/ ? $1 : '';
237 3 50       16 if ($Complete::Common::OPT_CI) {
238 0         0 $ext = lc($ext);
239 0         0 for my $e (@{ $args{file_ext_filter} }) {
  0         0  
240 0 0       0 return 1 if $ext eq lc($e);
241             }
242             } else {
243 3         4 for my $e (@{ $args{file_ext_filter} }) {
  3         9  
244 5 100       14 return 1 if $ext eq $e;
245             }
246             }
247 1         5 0;
248 1         4 };
249             }
250              
251             # from _dir (used by complete_dir)
252 13         16 my $filter_dir;
253 13 100       26 if ($args{_dir}) {
254 1 100   10   3 $filter_dir = sub { return 0 unless (-d $_[0]); 1 };
  10         151  
  3         11  
255             }
256              
257             # from exclude_dir option
258 13         16 my $filter_xdir;
259 13 100       20 if ($args{exclude_dir}) {
260 1 100   3   4 $filter_xdir = sub { return 0 if (-d $_[0]); 1 };
  3         40  
  1         4  
261             }
262              
263             # final filter sub
264             my $final_filter = sub {
265 70     70   3686 my $name = shift;
266 70 100       117 if ($filter_dir) { return 0 unless $filter_dir->($name) }
  10 100       17  
267 63 100       91 if ($filter_xdir) { return 0 unless $filter_xdir->($name) }
  3 100       5  
268 61 100       88 if ($filter) { return 0 unless $filter->($name) }
  17 100       52  
269 48 100       86 if ($filter_fregex) { return 0 unless $filter_fregex->($name) }
  11 100       14  
270 43 100       71 if ($filter_fext) { return 0 unless $filter_fext->($name) }
  6 100       11  
271 41         74 1;
272 13         40 };
273              
274             my $compres = Complete::Path::complete_path(
275             word => $word,
276             list_func => $list,
277 41     41   775 is_dir_func => sub { -d $_[0] },
278             filter_func => $final_filter,
279             starting_path => $starting_path,
280             result_prefix => $result_prefix,
281             recurse => $args{recurse},
282             recurse_matching => $args{recurse_matching},
283             exclude_leaf => $args{exclude_leaf},
284             exclude_nonleaf => $args{exclude_nonleaf} // $args{exclude_dir},
285 13   66     97 );
286              
287             # XXX why doesn't Complete::Path return hash answer with path_sep? we add
288             # workaround here to enable path mode.
289 13         349 hashify_answer($compres, {path_sep=>'/'});
290             }
291              
292             $SPEC{complete_dir} = do {
293             my $spec = {%{ $SPEC{complete_file} }}; # shallow copy
294              
295             $spec->{summary} = 'Complete directory from local filesystem '.
296             '(wrapper for complete_dir() that only picks directories)';
297             $spec->{args} = { %{$spec->{args}} }; # shallow copy of args
298             delete $spec->{args}{file_regex_filter};
299             delete $spec->{args}{file_ext_filter};
300             delete $spec->{args}{exclude_dir};
301              
302             $spec;
303             };
304             sub complete_dir {
305 1     1 1 1474 my %args = @_;
306              
307 1         8 complete_file(%args, _dir=>1);
308             }
309              
310             1;
311             # ABSTRACT: Completion routines related to files
312              
313             __END__