File Coverage

blib/lib/Complete/File.pm
Criterion Covered Total %
statement 111 126 88.1
branch 76 106 71.7
condition 26 38 68.4
subroutine 16 16 100.0
pod 2 2 100.0
total 231 288 80.2


line stmt bran cond sub pod time code
1             package Complete::File;
2              
3             our $DATE = '2021-02-02'; # DATE
4             our $VERSION = '0.442'; # VERSION
5              
6 2     2   194704 use 5.010001;
  2         24  
7 2     2   11 use strict;
  2         4  
  2         40  
8 2     2   9 use warnings;
  2         4  
  2         57  
9              
10 2     2   1008 use Complete::Common qw(:all);
  2         786  
  2         268  
11 2     2   1262 use Complete::Util qw(hashify_answer);
  2         13332  
  2         3535  
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             },
112             result_naked => 1,
113             result => {
114             schema => 'array',
115             },
116             };
117             sub complete_file {
118 13     13 1 12292 require Complete::Path;
119 13         5454 require Encode;
120 13         20869 require File::Glob;
121              
122 13         53 my %args = @_;
123 13   50     38 my $word = $args{word} // "";
124 13         23 my $recurse = $args{recurse};
125 13   50     51 my $handle_tilde = $args{handle_tilde} // 1;
126 13   50     38 my $allow_dot = $args{allow_dot} // 1;
127              
128             # if word is starts with "~/" or "~foo/" replace it temporarily with user's
129             # name (so we can restore it back at the end). this is to mimic bash
130             # support. note that bash does not support case-insensitivity for "foo".
131 13         18 my $result_prefix;
132 13   50     37 my $starting_path = $args{starting_path} // '.';
133 13 50 33     114 if ($handle_tilde && $word =~ s!\A(~[^/]*)/!!) {
    50 33        
134 0         0 $result_prefix = "$1/";
135 0         0 my @dir = File::Glob::bsd_glob($1); # glob will expand ~foo to /home/foo
136 0 0       0 return [] unless @dir;
137 0         0 $starting_path = Encode::decode('UTF-8', $dir[0]);
138             } elsif ($allow_dot && $word =~ s!\A((?:\.\.?/+)+|/+)!!) {
139             # just an optimization to skip sequences of '../'
140 0         0 $starting_path = $1;
141 0         0 $result_prefix = $1;
142 0 0       0 $starting_path =~ s#/+\z## unless $starting_path =~ m!\A/!;
143             }
144              
145             # bail if we don't allow dot and the path contains dot
146 13 50 33     34 return [] if !$allow_dot &&
147             $word =~ m!(?:\A|/)\.\.?(?:\z|/)!;
148              
149             # prepare list_func
150             my $list = sub {
151 24     24   2239 my ($path, $intdir, $isint) = @_;
152 24 50       750 opendir my($dh), $path or return undef;
153 24         64 my @res;
154 24         592 for (sort readdir $dh) {
155             # skip . and .. if leaf is empty, like in bash
156 206 100 100     6669 next if ($_ eq '.' || $_ eq '..') && $intdir eq '';
      100        
157 186 100 100     1120 next if $isint && !(-d "$path/$_");
158 158         392 push @res, Encode::decode('UTF-8', $_);
159             }
160 24         1361 \@res;
161 13         67 };
162              
163             # prepare filter_func
164              
165             # from the filter option
166 13         19 my $filter;
167 13 100 100     59 if ($args{filter} && !ref($args{filter})) {
    100 66        
168 2         12 my @seqs = split /\s*\|\s*/, $args{filter};
169             $filter = sub {
170 6     6   10 my $name = shift;
171 6 50       93 my @st = stat($name) or return 0;
172 6         14 my $mode = $st[2];
173 6         9 my $pass;
174             SEQ:
175 6         12 for my $seq (@seqs) {
176 7         29 my $neg = sub { $_[0] };
  3         12  
177 7         21 for my $c (split //, $seq) {
178 11 100       50 if ($c eq '-') { $neg = sub { $_[0] ? 0 : 1 } }
  4 100       16  
  4 50       19  
    50          
    50          
    100          
    50          
179 0 0       0 elsif ($c eq 'r') { next SEQ unless $neg->($mode & 0400) }
180 0 0       0 elsif ($c eq 'w') { next SEQ unless $neg->($mode & 0200) }
181 0 0       0 elsif ($c eq 'x') { next SEQ unless $neg->($mode & 0100) }
182 1 50       2 elsif ($c eq 'f') { next SEQ unless $neg->($mode & 0100000)}
183 6 100       17 elsif ($c eq 'd') { next SEQ unless $neg->($mode & 0040000)}
184             else {
185 0         0 die "Unknown character in filter: $c (in $seq)";
186             }
187             }
188 3         7 $pass = 1; last SEQ;
  3         8  
189             }
190 6         21 $pass;
191 2         10 };
192             } elsif ($args{filter} && ref($args{filter}) eq 'CODE') {
193 1         2 $filter = $args{filter};
194             }
195              
196             # from the file_regex_filter option
197 13         22 my $filter_fregex;
198 13 100       29 if ($args{file_regex_filter}) {
199             $filter_fregex = sub {
200 11     11   17 my $name = shift;
201 11 100       138 return 1 if -d $name;
202 7 50       19 return 0 unless -f _;
203 7 100       52 return 1 if $name =~ $args{file_regex_filter};
204 5         21 0;
205 1         4 };
206             }
207              
208             # from the file_ext_filter option
209 13         19 my $filter_fext;
210 13 100 100     55 if ($args{file_ext_filter} && ref $args{file_ext_filter} eq 'Regexp') {
    100 66        
211             $filter_fext = sub {
212 3     3   5 my $name = shift;
213 3 50       47 return 1 if -d $name;
214 3 50       9 return 0 unless -f _;
215 3 50       27 my $ext = $name =~ /\.(\w+)\z/ ? $1 : '';
216 3 100       32 return 1 if $ext =~ $args{file_ext_filter};
217 1         6 0;
218 1         3 };
219             } elsif ($args{file_ext_filter} && ref $args{file_ext_filter} eq 'ARRAY') {
220             $filter_fext = sub {
221 3     3   4 my $name = shift;
222 3 50       44 return 1 if -d $name;
223 3 50       11 return 0 unless -f _;
224 3 50       26 my $ext = $name =~ /\.(\w+)\z/ ? $1 : '';
225 3 50       8 if ($Complete::Common::OPT_CI) {
226 0         0 $ext = lc($ext);
227 0         0 for my $e (@{ $args{file_ext_filter} }) {
  0         0  
228 0 0       0 return 1 if $ext eq lc($e);
229             }
230             } else {
231 3         4 for my $e (@{ $args{file_ext_filter} }) {
  3         10  
232 5 100       15 return 1 if $ext eq $e;
233             }
234             }
235 1         6 0;
236 1         5 };
237             }
238              
239             # from _dir (used by complete_dir)
240 13         18 my $filter_dir;
241 13 100       28 if ($args{_dir}) {
242 1 100   10   3 $filter_dir = sub { return 0 unless (-d $_[0]); 1 };
  10         149  
  3         11  
243             }
244              
245             # from exclude_dir option
246 13         14 my $filter_xdir;
247 13 100       26 if ($args{exclude_dir}) {
248 1 100   3   4 $filter_xdir = sub { return 0 if (-d $_[0]); 1 };
  3         48  
  1         4  
249             }
250              
251             # final filter sub
252             my $final_filter = sub {
253 70     70   4175 my $name = shift;
254 70 100       137 if ($filter_dir) { return 0 unless $filter_dir->($name) }
  10 100       20  
255 63 100       105 if ($filter_xdir) { return 0 unless $filter_xdir->($name) }
  3 100       5  
256 61 100       108 if ($filter) { return 0 unless $filter->($name) }
  17 100       32  
257 48 100       105 if ($filter_fregex) { return 0 unless $filter_fregex->($name) }
  11 100       17  
258 43 100       81 if ($filter_fext) { return 0 unless $filter_fext->($name) }
  6 100       12  
259 41         84 1;
260 13         42 };
261              
262             my $compres = Complete::Path::complete_path(
263             word => $word,
264             list_func => $list,
265 41     41   938 is_dir_func => sub { -d $_[0] },
266 13         60 filter_func => $final_filter,
267             starting_path => $starting_path,
268             result_prefix => $result_prefix,
269             recurse => $recurse,
270             );
271              
272             # XXX why doesn't Complete::Path return hash answer with path_sep? we add
273             # workaround here to enable path mode.
274 13         387 hashify_answer($compres, {path_sep=>'/'});
275             }
276              
277             $SPEC{complete_dir} = do {
278             my $spec = {%{ $SPEC{complete_file} }}; # shallow copy
279              
280             $spec->{summary} = 'Complete directory from local filesystem '.
281             '(wrapper for complete_dir() that only picks directories)';
282             $spec->{args} = { %{$spec->{args}} }; # shallow copy of args
283             delete $spec->{args}{file_regex_filter};
284             delete $spec->{args}{file_ext_filter};
285             delete $spec->{args}{exclude_dir};
286              
287             $spec;
288             };
289             sub complete_dir {
290 1     1 1 1403 my %args = @_;
291              
292 1         7 complete_file(%args, _dir=>1);
293             }
294              
295             1;
296             # ABSTRACT: Completion routines related to files
297              
298             __END__