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