File Coverage

blib/lib/Dir/ls.pm
Criterion Covered Total %
statement 79 94 84.0
branch 31 42 73.8
condition 21 35 60.0
subroutine 14 14 100.0
pod 1 1 100.0
total 146 186 78.4


line stmt bran cond sub pod time code
1             package Dir::ls;
2              
3 1     1   893 use strict;
  1         6  
  1         23  
4 1     1   4 use warnings;
  1         2  
  1         22  
5 1     1   4 use Carp 'croak';
  1         1  
  1         66  
6 1     1   13 use Exporter 'import';
  1         2  
  1         25  
7 1     1   5 use File::Spec;
  1         1  
  1         22  
8 1     1   344 use Path::ExpandTilde;
  1         865  
  1         41  
9 1     1   344 use Sort::filevercmp 'fileversort';
  1         922  
  1         60  
10 1     1   421 use sort 'stable';
  1         442  
  1         3  
11              
12             our $VERSION = '0.005';
13              
14             our @EXPORT = 'ls';
15              
16             sub ls {
17 9     9 1 5556 my ($dir, $options);
18 9 100       22 if (ref $_[0] eq 'HASH') {
19 1         2 ($options) = @_;
20             } else {
21 8         16 ($dir, $options) = @_;
22             }
23 9 100 66     27 $dir = '.' unless defined $dir and length $dir;
24 9   100     68 $options ||= {};
25              
26 9         20 $dir = expand_tilde($dir); # do homedir expansion
27            
28 9 50       342 opendir my $dh, $dir or croak "Failed to open directory '$dir': $!";
29 9         181 my @entries = readdir $dh;
30 9 50       87 closedir $dh or croak "Failed to close directory '$dir': $!";
31            
32 9 50 66     52 unless ($options->{a} or $options->{all} or $options->{f}) {
      66        
33 8 100 66     26 if ($options->{A} or $options->{'almost-all'}) {
34 4 100       6 @entries = grep { $_ ne '.' and $_ ne '..' } @entries;
  52         124  
35             } else {
36 4         5 @entries = grep { !m/^\./ } @entries;
  52         104  
37             }
38             }
39            
40 9 100       23 local $options->{sort} = '' unless defined $options->{sort};
41 9 50 33     39 unless ($options->{U} or $options->{sort} eq 'none' or $options->{f}) {
      33        
42 9 100 66     24 if ($options->{v} or $options->{sort} eq 'version') {
43 1         4 @entries = fileversort @entries;
44             } else {
45             {
46             # pre-sort by alphanumeric then full name
47 8         10 my @alnum = map { _alnum_sorter($_) } @entries;
  8         9  
  86         98  
48 1     1   753 use locale;
  1         478  
  1         5  
49 8 50       27 @entries = @entries[sort { $alnum[$a] cmp $alnum[$b] or $entries[$a] cmp $entries[$b] } 0..$#entries];
  214         311  
50             }
51            
52 8 100 66     55 if ($options->{S} or $options->{sort} eq 'size') {
    100 66        
    50 33        
    50          
    50          
    50          
53 1         3 my @sizes = map { _stat_sorter($dir, $_, 7) } @entries;
  11         18  
54 1         4 @entries = @entries[sort { $sizes[$b] <=> $sizes[$a] } 0..$#entries];
  24         26  
55             } elsif ($options->{X} or $options->{sort} eq 'extension') {
56 1         2 my @extensions = map { _ext_sorter($_) } @entries;
  11         15  
57 1     1   123 use locale;
  1         1  
  1         3  
58 1         2 @entries = @entries[sort { $extensions[$a] cmp $extensions[$b] } 0..$#entries];
  28         31  
59             } elsif ($options->{t} or $options->{sort} eq 'time') {
60 0         0 my @mtimes = map { _stat_sorter($dir, $_, 9) } @entries;
  0         0  
61 0         0 @entries = @entries[sort { $mtimes[$a] <=> $mtimes[$b] } 0..$#entries];
  0         0  
62             } elsif ($options->{c}) {
63 0         0 my @ctimes = map { _stat_sorter($dir, $_, 10) } @entries;
  0         0  
64 0         0 @entries = @entries[sort { $ctimes[$a] <=> $ctimes[$b] } 0..$#entries];
  0         0  
65             } elsif ($options->{u}) {
66 0         0 my @atimes = map { _stat_sorter($dir, $_, 8) } @entries;
  0         0  
67 0         0 @entries = @entries[sort { $atimes[$a] <=> $atimes[$b] } 0..$#entries];
  0         0  
68             } elsif (length $options->{sort}) {
69 0         0 croak "Unknown sort option '$options->{sort}'; must be 'none', 'size', 'time', 'version', or 'extension'";
70             }
71             }
72            
73 9 100 66     2115 @entries = reverse @entries if $options->{r} or $options->{reverse};
74             }
75            
76 9         49 return @entries;
77             }
78              
79             sub _stat_sorter {
80 11     11   14 my ($dir, $entry, $index) = @_;
81 11         61 my $path = File::Spec->catfile($dir, $entry);
82 11         104 my @stat = stat $path;
83 11 50       25 unless (@stat) { # try as a subdirectory
84 0         0 $path = File::Spec->catdir($dir, $entry);
85 0         0 @stat = stat $path;
86             }
87 11 50       15 croak "Failed to stat '$path': $!" unless @stat;
88 11         27 return $stat[$index];
89             }
90              
91             sub _ext_sorter {
92 81     81   134 my ($entry) = @_;
93 81         159 my ($ext) = $entry =~ m/(\.[^.]*)$/;
94 81 100       119 $ext = '' unless defined $ext;
95 81         113 return $ext;
96             }
97              
98             sub _alnum_sorter {
99 150     150   1662 my ($entry) = @_;
100             # Only consider alphabetic, numeric, and blank characters (space + tab)
101 150         165 $entry =~ tr/a-zA-Z0-9 \t//cd;
102 150         190 return $entry;
103             }
104              
105             1;
106              
107             =head1 NAME
108              
109             Dir::ls - List the contents of a directory
110              
111             =head1 SYNOPSIS
112              
113             use Dir::ls;
114            
115             print "$_\n" for ls; # defaults to current working directory
116            
117             print "$_: ", -s "/foo/bar/$_", "\n" for ls '/foo/bar', {all => 1, sort => 'size'};
118              
119             =head1 DESCRIPTION
120              
121             Provides the function L, which returns the contents of a directory in a
122             similar manner to the GNU coreutils command L.
123              
124             =head1 FUNCTIONS
125              
126             =head2 ls
127              
128             my @contents = ls $dir, \%options;
129              
130             Takes a directory path and optional hashref of options, and returns a list of
131             items in the directory. Home directories represented by C<~> will be expanded
132             by L. If no directory path is passed, the current working
133             directory will be used. Like in L, the returned names are relative to
134             the passed directory path, so if you want to use a filename (such as passing it
135             to C or C), you must prefix it with the directory path, with C<~>
136             expanded if present.
137              
138             # Check the size of a file in current user's home directory
139             my @contents = ls '~';
140             say -s "$ENV{HOME}/$contents[0]";
141              
142             By default, hidden files and directories (those starting with C<.>) are
143             omitted, and the results are sorted by name according to the current locale
144             (see L for more information).
145              
146             Accepts the following options:
147              
148             =over 2
149              
150             =item a
151              
152             =item all
153              
154             Include hidden files and directories.
155              
156             =item A
157              
158             =item almost-all
159              
160             Include hidden files and directories, but not C<.> or C<..>.
161              
162             =item c
163              
164             Sort by ctime (change time) in seconds since the epoch.
165              
166             =item f
167              
168             Equivalent to passing C and setting C to C.
169              
170             =item r
171              
172             =item reverse
173              
174             Reverse sort order (unless C or C<< sort => 'none' >> specified).
175              
176             =item sort
177              
178             Specify sort algorithm other than the default sort-by-name. Valid values are:
179             C, C, C, C
180              
181             =item S
182              
183             Sort by file size in bytes (descending). Equivalent to C<< sort => 'size' >>.
184              
185             =item t
186              
187             Sort by mtime (modification time) in seconds since the epoch. Equivalent to
188             C<< sort => 'time' >>.
189              
190             =item u
191              
192             Sort by atime (access time) in seconds since the epoch.
193              
194             =item U
195              
196             Return entries in directory order (unsorted). Equivalent to
197             C<< sort => 'none' >>.
198              
199             =item v
200              
201             Sort naturally by version numbers within the name. Uses L
202             for sorting. Equivalent to C<< sort => 'version' >>.
203              
204             =item X
205              
206             Sort by (last) file extension, according to the current locale. Equivalent to
207             C<< sort => 'extension' >>.
208              
209             =back
210              
211             =head1 CAVEATS
212              
213             This is only an approximation of L. It makes an attempt to give the same
214             output under the supported options, but there may be differences in edge cases.
215             Weird things might happen with sorting of non-ASCII filenames, or on
216             non-Unixlike systems. Lots of options aren't supported yet. Patches welcome.
217              
218             =head1 BUGS
219              
220             Report any issues on the public bugtracker.
221              
222             =head1 AUTHOR
223              
224             Dan Book
225              
226             =head1 COPYRIGHT AND LICENSE
227              
228             This software is Copyright (c) 2017 by Dan Book.
229              
230             This is free software, licensed under:
231              
232             The Artistic License 2.0 (GPL Compatible)
233              
234             =head1 SEE ALSO
235              
236             L, L