File Coverage

blib/lib/Dir/ls.pm
Criterion Covered Total %
statement 75 88 85.2
branch 30 40 75.0
condition 21 35 60.0
subroutine 13 13 100.0
pod 1 1 100.0
total 140 177 79.1


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