File Coverage

blib/lib/Dir/ls.pm
Criterion Covered Total %
statement 109 137 79.5
branch 67 96 69.7
condition 39 67 58.2
subroutine 16 16 100.0
pod 1 1 100.0
total 232 317 73.1


line stmt bran cond sub pod time code
1             package Dir::ls;
2              
3 1     1   490 use strict;
  1         7  
  1         29  
4 1     1   4 use warnings;
  1         2  
  1         26  
5 1     1   4 use Carp 'croak';
  1         2  
  1         81  
6 1     1   22 use Exporter 'import';
  1         2  
  1         35  
7 1     1   6 use Fcntl ':mode';
  1         1  
  1         220  
8 1     1   7 use File::Spec;
  1         2  
  1         20  
9 1     1   690 use File::stat;
  1         9577  
  1         4  
10 1     1   538 use Path::ExpandTilde;
  1         1012  
  1         52  
11 1     1   453 use Sort::filevercmp 'fileversort';
  1         1149  
  1         55  
12 1     1   429 use Text::Glob 'glob_to_regex';
  1         856  
  1         66  
13              
14 1     1   559 use sort 'stable';
  1         559  
  1         5  
15              
16             our $VERSION = '1.000';
17              
18             our @EXPORT = 'ls';
19              
20             sub ls {
21 19     19 1 18154 my ($dir, $opt);
22 19 100       71 if (ref $_[0] eq 'HASH') {
23 1         2 ($opt) = @_;
24             } else {
25 18         40 ($dir, $opt) = @_;
26             }
27 19 100 66     84 $dir = '.' unless defined $dir and length $dir;
28 19 100       167 my %options = %{$opt || {}};
  19         104  
29 19         54 foreach my $option (sort grep { m/^-/ } keys %options) {
  28         103  
30 4         17 (my $name = $option) =~ s/^-+//;
31 4         12 $options{$name} = delete $options{$option};
32             }
33              
34 19         59 $dir = expand_tilde($dir); # do homedir expansion
35              
36 19 50       1030 opendir my $dh, $dir or croak "Failed to open directory '$dir': $!";
37 19         554 my @entries = readdir $dh;
38 19 50       230 closedir $dh or croak "Failed to close directory '$dir': $!";
39              
40 19   66     137 my $show_all = !!($options{a} or $options{all} or $options{f});
41 19   100     61 my $show_almost_all = !!($options{A} or $options{'almost-all'});
42 19   66     59 my $skip_backup = !!($options{B} or $options{'ignore-backups'});
43 19 100       51 my $hide_pattern = defined $options{hide} ? glob_to_regex($options{hide}) : undef;
44 19 50       425 my $ignore_glob = defined $options{I} ? $options{I} : $options{ignore};
45 19 100       43 my $ignore_pattern = defined $ignore_glob ? glob_to_regex($ignore_glob) : undef;
46             @entries = grep {
47 19 100 100     472 ($show_all ? 1 : $show_almost_all ? ($_ ne '.' and $_ ne '..') :
  266 100 100     1510  
    100 100        
    100          
    100          
48             (!m/^\./ and defined $hide_pattern ? !m/$hide_pattern/ : 1))
49             and ($skip_backup ? !m/~\z/ : 1)
50             and (defined $ignore_pattern ? !m/$ignore_pattern/ : 1)
51             } @entries;
52              
53 19   100     58 my $sort = $options{sort} || '';
54 19 50 33     235 if ($options{U} or $options{f} or $sort eq 'none') {
    100 33        
    100 66        
    100 66        
    50 66        
    50 33        
    50          
    50          
55 0         0 $sort = 'U';
56             } elsif ($options{v} or $sort eq 'version') {
57 1         2 $sort = 'v';
58             } elsif ($options{S} or $sort eq 'size') {
59 1         3 $sort = 'S';
60             } elsif ($options{X} or $sort eq 'extension') {
61 1         3 $sort = 'X';
62             } elsif ($options{t} or $sort eq 'time') {
63 0         0 $sort = 't';
64             } elsif ($options{c}) {
65 0         0 $sort = 'c';
66             } elsif ($options{u}) {
67 0         0 $sort = 'u';
68             } elsif (defined $options{sort}) {
69 0         0 croak "Unknown sort option '$sort'; must be 'none', 'size', 'time', 'version', or 'extension'";
70             }
71              
72 19         28 my %stat;
73              
74 19 50       38 unless ($sort eq 'U') {
75 19 100       34 if ($sort eq 'v') {
76 1         5 @entries = fileversort @entries;
77             } else {
78             {
79             # pre-sort by collation
80 1     1   1133 use locale;
  1         595  
  1         5  
  18         23  
81 18         95 @entries = sort @entries;
82             }
83              
84 18 100       71 if ($sort eq 'S') {
    100          
    50          
    50          
    50          
85 1         2 my @sizes = map { _stat($_, $dir, \%stat)->size } @entries;
  12         112  
86 1         13 @entries = @entries[sort { $sizes[$b] <=> $sizes[$a] } 0..$#entries];
  29         40  
87             } elsif ($sort eq 'X') {
88 1         3 my @extensions = map { _ext_sorter($_) } @entries;
  12         20  
89 1     1   147 use locale;
  1         2  
  1         4  
90 1         6 @entries = @entries[sort { $extensions[$a] cmp $extensions[$b] } 0..$#entries];
  31         44  
91             } elsif ($sort eq 't') {
92 0         0 my @mtimes = map { _stat($_, $dir, \%stat)->mtime } @entries;
  0         0  
93 0         0 @entries = @entries[sort { $mtimes[$a] <=> $mtimes[$b] } 0..$#entries];
  0         0  
94             } elsif ($sort eq 'c') {
95 0         0 my @ctimes = map { _stat($_, $dir, \%stat)->ctime } @entries;
  0         0  
96 0         0 @entries = @entries[sort { $ctimes[$a] <=> $ctimes[$b] } 0..$#entries];
  0         0  
97             } elsif ($sort eq 'u') {
98 0         0 my @atimes = map { _stat($_, $dir, \%stat)->atime } @entries;
  0         0  
99 0         0 @entries = @entries[sort { $atimes[$a] <=> $atimes[$b] } 0..$#entries];
  0         0  
100             }
101             }
102              
103 19 100 66     2994 @entries = reverse @entries if $options{r} or $options{reverse};
104              
105 19 100       36 if ($options{'group-directories-first'}) {
106 2         6 my ($dirs, $files) = ([], []);
107 2 100       6 push @{S_ISDIR(_stat($_, $dir, \%stat)->mode) ? $dirs : $files}, $_ for @entries;
  24         233  
108 2         28 @entries = (@$dirs, @$files);
109             }
110             }
111              
112 19   50     58 my $indicators = $options{'indicator-style'} || '';
113 19 50 66     163 if ($indicators eq 'none') {
    100 33        
    50 33        
    50 33        
    50          
114 0         0 $indicators = '';
115             } elsif ($options{p} or $indicators eq 'slash') {
116 1         3 $indicators = 'p';
117             } elsif ($options{'file-type'} or $indicators eq 'file-type') {
118 0         0 $indicators = 'f';
119             } elsif ($options{F} or $options{classify} or $indicators eq 'classify') {
120 0         0 $indicators = 'F';
121             } elsif (defined $options{'indicator-style'}) {
122 0         0 croak "Unknown indicator-style option '$indicators'; must be 'none', 'slash', 'file-type', or 'classify'";
123             }
124              
125 19 100       37 if (length $indicators) {
126 1         12 foreach my $entry (@entries) {
127 11         28 my $mode = _stat($entry, $dir, \%stat)->mode;
128 11 100 33     118 if (S_ISDIR($mode)) {
    50          
129 1         3 $entry .= '/';
130             } elsif ($indicators eq 'f' or $indicators eq 'F') {
131 0 0 0     0 if (S_ISLNK($mode)) {
    0          
    0          
    0          
132 0         0 $entry .= '@';
133             } elsif (S_ISSOCK($mode)) {
134 0         0 $entry .= '=';
135             } elsif (S_ISFIFO($mode)) {
136 0         0 $entry .= '|';
137             } elsif ($indicators eq 'F' and $mode & S_IXUSR) {
138 0         0 $entry .= '*';
139             }
140             }
141             }
142             }
143              
144 19         245 return @entries;
145             }
146              
147             sub _stat {
148 47     47   89 my ($entry, $dir, $cache) = @_;
149 47 50 33     208 croak "Cannot lstat empty filename" unless defined $entry and length $entry;
150 47 50       99 return $cache->{$entry} if exists $cache->{$entry};
151 47         406 my $path = File::Spec->catfile($dir, $entry);
152 47         132 my $stat = lstat $path;
153 47 50       5935 unless ($stat) { # try as a subdirectory
154 0         0 $path = File::Spec->catdir($dir, $entry);
155 0         0 $stat = lstat $path;
156             }
157 47 50       92 croak "Failed to lstat '$path': $!" unless $stat;
158 47         872 return $cache->{$entry} = $stat;
159             }
160              
161             sub _ext_sorter {
162 90     90   2008 my ($entry) = @_;
163 90         223 my ($ext) = $entry =~ m/(\.[^.]*)\z/;
164 90 100       162 $ext = '' unless defined $ext;
165 90         161 return $ext;
166             }
167              
168             1;
169              
170             =head1 NAME
171              
172             Dir::ls - List the contents of a directory
173              
174             =head1 SYNOPSIS
175              
176             use Dir::ls;
177            
178             print "$_\n" for ls; # defaults to current working directory
179            
180             print "$_: ", -s "/foo/bar/$_", "\n" for ls '/foo/bar', {-a => 1, sort => 'size'};
181              
182             =head1 DESCRIPTION
183              
184             Provides the function L, which returns the contents of a directory in a
185             similar manner to the GNU coreutils command L.
186              
187             =head1 FUNCTIONS
188              
189             =head2 ls
190              
191             my @contents = ls $dir, \%options;
192              
193             Takes a directory path and optional hashref of options, and returns a list of
194             items in the directory. Home directories represented by C<~> will be expanded
195             by L. If no directory path is passed, the current working
196             directory will be used. Like in L, the returned names are relative to
197             the passed directory path, so if you want to use a filename (such as passing it
198             to C or C), you must prefix it with the directory path, with C<~>
199             expanded if present.
200              
201             # Check the size of a file in current user's home directory
202             my @contents = ls '~';
203             say -s "$ENV{HOME}/$contents[0]";
204              
205             By default, hidden files and directories (those starting with C<.>) are
206             omitted, and the results are sorted by name according to the current locale
207             (see L for more information).
208              
209             Accepts the following options (any prefixed hyphens are ignored):
210              
211             =over 2
212              
213             =item a
214              
215             =item all
216              
217             Include hidden files and directories.
218              
219             =item A
220              
221             =item almost-all
222              
223             Include hidden files and directories, but not C<.> or C<..>.
224              
225             =item B
226              
227             =item ignore-backups
228              
229             Omit files and directories ending in C<~>.
230              
231             =item c
232              
233             Sort by ctime (change time) in seconds since the epoch.
234              
235             =item F
236              
237             =item classify
238              
239             Append classification indicators to the end of file and directory names.
240             Equivalent to C<< 'indicator-style' => 'classify' >>.
241              
242             =item f
243              
244             Equivalent to passing C and setting C to C.
245              
246             =item file-type
247              
248             Append file-type indicators to the end of file and directory names. Equivalent
249             to C<< 'indicator-style' => 'file-type' >>.
250              
251             =item group-directories-first
252              
253             Return directories then files. The C algorithm will be applied within
254             these groupings, but C or C<< sort => 'none' >> will disable the grouping.
255              
256             =item hide
257              
258             Omit files and directories matching given L pattern. Overriden by
259             C/C or C/C.
260              
261             =item I
262              
263             =item ignore
264              
265             Omit files and directories matching given L pattern.
266              
267             =item indicator-style
268              
269             Append indicators to the end of filenames according to the specified style.
270             Recognized styles are: C (default), C (appends C to
271             directories), C (appends all of the below indicators except C<*>),
272             and C (appends all of the below indicators).
273              
274             / directory
275             @ symbolic link
276             = socket
277             | named pipe (FIFO)
278             * executable
279              
280             Use of indicator types other than C will render the resulting filenames
281             suitable only for display due to the extra characters.
282              
283             =item p
284              
285             Append C to the end of directory names. Equivalent to
286             C<< 'indicator-style' => 'slash' >>.
287              
288             =item r
289              
290             =item reverse
291              
292             Reverse sort order (unless C or C<< sort => 'none' >> specified).
293              
294             =item sort
295              
296             Specify sort algorithm other than the default sort-by-name. Valid values are:
297             C, C, C, C
298              
299             =item S
300              
301             Sort by file size in bytes (descending). Equivalent to C<< sort => 'size' >>.
302              
303             =item t
304              
305             Sort by mtime (modification time) in seconds since the epoch. Equivalent to
306             C<< sort => 'time' >>.
307              
308             =item u
309              
310             Sort by atime (access time) in seconds since the epoch.
311              
312             =item U
313              
314             Return entries in directory order (unsorted). Equivalent to
315             C<< sort => 'none' >>.
316              
317             =item v
318              
319             Sort naturally by version numbers within the name. Uses L
320             for sorting. Equivalent to C<< sort => 'version' >>.
321              
322             =item X
323              
324             Sort by (last) file extension, according to the current locale. Equivalent to
325             C<< sort => 'extension' >>.
326              
327             =back
328              
329             =head1 CAVEATS
330              
331             This is only an approximation of L. It makes an attempt to give the same
332             output under the supported options, but there may be differences in edge cases.
333             Weird things might happen with sorting of non-ASCII filenames, or on
334             non-Unixlike systems. Lots of options aren't supported yet. Patches welcome.
335              
336             =head1 BUGS
337              
338             Report any issues on the public bugtracker.
339              
340             =head1 AUTHOR
341              
342             Dan Book
343              
344             =head1 COPYRIGHT AND LICENSE
345              
346             This software is Copyright (c) 2017 by Dan Book.
347              
348             This is free software, licensed under:
349              
350             The Artistic License 2.0 (GPL Compatible)
351              
352             =head1 SEE ALSO
353              
354             L, L