3 # l / ll / lf / llf - better replacement of the classic ls command
5 # Author: Ulli Horlacher <framstag@rus.uni-stuttgart.de>
7 # Perl Artistic License
13 # the name of the game
18 # unshift @ARGV,split /\s+/,$ENV{'l_opt'} if $ENV{'l_opt'};
20 @ARGV = grep { chomp } <STDIN> if "@ARGV" eq '-';
23 $opt_l = $opt_i = $opt_t = $opt_s = $opt_a = $opt_r = $opt_d = $opt_n = 0;
24 $opt_L = $opt_N = $opt_c = $opt_u = $opt_S = $opt_R = $opt_z = $opt_h = 0;
25 $opt_U = $opt_x = $opt_E = 0;
27 $opt_m = $opt_f = $opt_F = $opt_D = '';
28 getopts('hdnlLNitcuarsxUSREz*m:f:D:F:') or usage(1);
30 $opt_z = 1 unless $opt_R;
31 $opt_l = 1 if $0 eq 'll';
32 $opt_l = $opt_i = $opt_a = $opt_S = 1 if $0 eq 'lll';
34 if ($0 eq 'lf' or $0 eq 'llf') {
36 if (scalar(@ARGV) == 0) {
38 } elsif (scalar(@ARGV) == 1) {
40 $opt_R = $opt_F if $opt_F eq '.';
41 } elsif (-d $ARGV[-1]) {
43 $opt_F = join('|',@ARGV);
45 $opt_F = join('|',@ARGV);
48 @ARGV = ($opt_R) if -d $opt_R;
51 $postsort = $opt_t||$opt_s;
52 $postproc = $postsort||$opt_z;
54 # mark for squeeze operation
55 $z = $opt_z ? "\0" : '';
57 # default sorting methode
58 if ($opt_U) { $lcsort = sub { return @_ } }
59 elsif ($opt_r) { $lcsort = sub { sort { lc $b cmp lc $a } @_ } }
60 else { $lcsort = sub { sort { lc $a cmp lc $b } @_ } }
62 # default: list only files not beginning with a dot
64 if ($opt_a) { $opt_m = '.' }
65 else { $opt_m = '^[^\.]' }
71 if ($opt_D =~ /:(\d+)([mhd])/) {
74 if ($z =~ /m/) { $older *= 60 }
75 elsif ($z =~ /h/) { $older *= 60*60 }
76 elsif ($z =~ /d/) { $older *= 60*60*24 }
77 } elsif ($opt_D =~ /:(\d\d\d\d-\d\d-\d\d)$/) {
80 if ($opt_D =~ /(\d+)([mhd]):/) {
83 if ($z =~ /m/) { $newer *= 60 }
84 elsif ($z =~ /h/) { $newer *= 60*60 }
85 elsif ($z =~ /d/) { $newer *= 60*60*24 }
86 } elsif ($opt_D =~ /^(\d\d\d\d-\d\d-\d\d):/) {
91 # preselect date field number
92 if ($opt_c) { $sdf = 'c' }
93 elsif ($opt_u) { $sdf = 'a' }
97 if (@ARGV) { @ARGV = &$lcsort(@ARGV) }
98 else { @ARGV = &getfiles('.') }
103 # post process files list?
104 # remark: if no postprocessing, files list has been already printed in list()
105 if (@LIST && $postproc) {
107 # on -t or -s option sort list on date or size
108 # and then strip of leading sorting pre-string
109 @LIST = grep { s/.{21}// } reverse sort @LIST if $postsort;
111 # squeeze size field (= remove unnecessary spaces)
112 if ($opt_z and not $opt_f) {
113 $opt_z = '%'.$opt_z.'s ';
114 @LIST = grep { s/\0 *([,\d\.\-]+) /sprintf($opt_z,$1)/e } @LIST;
117 @LIST = reverse @LIST if $opt_r;
119 if (not ($opt_t or $opt_U) and grep /^d[rR-][wW-][xX-]/,@LIST) {
120 foreach (@LIST) { print if /^d/ }
121 foreach (@LIST) { print unless /^d/ }
127 # print statistics summary?
129 print "$SS file(s):";
130 printf " r=%d (%s Bytes)",$SS{'-'},&d3($Ss) if $SS{'-'};
132 foreach my $type (qw(l d c b p s ?)) {
133 printf " %s=%d",$type,$SS{$type} if $SS{$type};
136 foreach my $type (keys %SS) { printf " %s=%d",$type,$SS{$type} }
140 exit ($found ? 0 : 1);
143 # collect files and build file lists
152 getacl(@files) if $opt_l and not $opt_n;
154 # loop over all argument files/directories
155 foreach $f (@files) {
157 # skip jed and emacs backup files
158 # next if $f =~ /~$/ and not $opt_a and not $opt_l;
166 warn "$0: dangling symlink $f\n";
173 # traverse real subdirs
174 if (-d $f and not -l $f) {
176 # skip other file systems on -x
178 my @pd = stat(dirname($f));
180 next if $pd[0] ne $sd[0];
182 collect(getfiles($f));
187 # suppress trailing / on -d option
188 $f =~ s:/$:: if $opt_d;
190 # on trailing / list subdirs, too
191 if ($f =~ m:/$:) { list(getfiles($f)) }
192 elsif ($f eq '') { list('/') }
196 warn "$0: dangling symlink $f\n";
213 # GLOBAL: @LIST (filenames-list)
216 my ($file,$line,$linkname,$inode,$links,$size,$mode,$uid,$gid,$date,%dates);
219 foreach $file (@files) {
221 next if $opt_F and not fmatch($file);
222 next if $opt_N and (not -f $file or -l $file);
224 # get file information
225 # if ($opt_L and stat $file or not $opt_L and lstat $file) {
227 ($linkname,$inode,$links,$size,$mode,$uid,$gid,$date,%dates) = &info($file);
228 } elsif ($! eq "Permission denied") {
230 $inode = $links = $size = $uid = $gid = '?';
231 $mode = $opt_l ? '?---------' : '?---';
232 $date = '????-??-?? ??:??:??';
233 %dates = ('m' => 0, 'a' => 0, 'c' => 0);
235 warn "$0: ".quote($file)." - $!\n";
243 next if $older =~ /-/ and $day gt $older;
244 next if $older !~ /-/ and $dates{m} > time-$older;
247 next if $newer =~ /-/ and $day lt $newer;
248 next if $newer !~ /-/ and $dates{m} < time-$newer;
251 if (defined $linkname) {
253 # prepend sorting string
255 $line = sprintf '%21s',$date if $opt_t;
256 $line = sprintf '%21s',$size if $opt_s;
259 $uid = substr($uid,0,8);
260 $gid = substr($gid,0,8);
263 # user defined format?
265 foreach my $i (split '',$opt_f) {
268 if ($i eq 'm') { $line .= sprintf '%06o ', $mode }
269 elsif ($i eq 'u') { $line .= sprintf '%6d ', $uid }
270 elsif ($i eq 'g') { $line .= sprintf '%6d ', $gid }
271 elsif ($i eq 's') { $line .= sprintf "$z%16s ",$size }
272 elsif ($i eq 'l') { $line .= sprintf '%3s ', $links }
273 elsif ($i eq 'i') { $line .= sprintf '%14s ', $inode }
274 elsif ($i eq 'd') { $line .= sprintf '%10s ', $date }
275 elsif ($i eq 'a') { $line .= sprintf '%10s %10s %10s ',
276 $dates{'a'},$dates{'m'},$dates{'c'} }
278 # $mode =~ s/(....)(...)/sprintf($1.uc($2))/e if $ACL{$file};
279 substr($mode,4,3) = uc(substr($mode,4,3)) if $ACL{$file};
280 if ($i eq 'm') { $line .= $mode.' ' }
281 elsif ($i eq 'u') { $line .= sprintf '%-8s ', $uid }
282 elsif ($i eq 'g') { $line .= sprintf '%-8s ', $gid }
283 elsif ($i eq 's') { $line .= sprintf "$z%19s ",$size }
284 elsif ($i eq 'l') { $line .= sprintf '%3s ', $links }
285 elsif ($i eq 'i') { $line .= sprintf '%14s ', $inode }
286 elsif ($i eq 'd') { $line .= $date.' ' }
287 elsif ($i eq 'D') { $line .= $date.' ' }
288 elsif ($i eq 'a') { $line .= &isodate($dates{'a'}).' '.
289 &isodate($dates{'m'}).' '.
290 &isodate($dates{'c'}).' ' }
291 elsif ($i eq 'A') { $line .= &isodate($dates{'a'}).' '.
292 &isodate($dates{'m'}).' '.
293 &isodate($dates{'c'}).' ' }
302 $line .= sprintf "%06o %6d %6d $z%15s %10d ",
303 $mode,$uid,$gid,$size,$date;
305 $line .= sprintf "%06o $z%15s %10d ",$mode,$size,$date;
309 # $mode .= $ACL{$file} ? '+' : ' ';
310 # $mode =~ s/(....)(...)/sprintf($1.uc($2))/e if $ACL{$file};
311 substr($mode,4,3) = uc(substr($mode,4,3)) if $ACL{$file};
312 $line .= sprintf "%s %-8s %-8s $z%19s %s ",
313 $mode,$uid,$gid,$size,$date;
315 $line .= sprintf "%s $z%19s %s ",$mode,$size,substr($date,0,-3);
319 if ($opt_i) { $line .= sprintf '%3s %10s ',$links,$inode }
322 $line .= $linkname."\n";
334 warn "$0: cannot get dir-info for ".quote($file)." - $!\n";
340 # get file information
344 # OUTPUT: filename with linkname, inode, hard link count, size, mode string,
348 my ($linkname,$links,$mode,$bmode,$uid,$gid,$date,%dates,@stat);
351 my @rwx = qw/--- --x -w- -wx r-- r-x rw- rwx/;
354 if ($opt_L) { @stat = stat $file }
355 else { @stat = lstat $file }
362 %dates = ('m' => $stat[9],
369 $date = $dates{$sdf};
371 $uid = getpwuid($stat[4]) || $stat[4];
372 $gid = getgrgid($stat[5]) || $stat[5];
373 $date = &isodate($dates{$sdf});
376 if (-f _) { $type = '-'; $size = $stat[7]; }
377 elsif (!$opt_L && -l _) { $type = 'l'; }
378 elsif (-d _) { $type = 'd'; }
379 elsif (-c _) { $type = 'c'; $size = &nodes($stat[6]); }
380 elsif (-b _) { $type = 'b'; $size = &nodes($stat[6]); }
381 elsif (-p _) { $type = 'p'; }
382 elsif (-S _) { $type = 's'; }
383 else { $type = '?'; }
387 $size = $stat[7] if $size eq '-';
390 $mode = $rwx[$bmode & 7];
392 $mode = $rwx[$bmode & 7] . $mode;
394 $mode = $rwx[$bmode & 7] . $mode;
395 substr($mode,2,1) =~ tr/-x/Ss/ if -u _;
396 substr($mode,5,1) =~ tr/-x/Ss/ if -g _;
397 substr($mode,8,1) =~ tr/-x/Tt/ if -k _;
400 # with short list display only effective file access modes
401 use filetest 'access'; # respect ACLs ==> cannot use pseudofile _
403 . (-r $file ? 'R' : '-')
404 . (-w $file ? 'W' : '-')
405 . (-x $file ? 'X' : '-');
406 substr($mode,2,1) =~ tr/-x/Ss/ if -u $file or -g $file;
407 substr($mode,3,1) =~ tr/-x/Tt/ if -k $file;
411 # fall back to ls command if perl lstat failed
416 ($mode,$links,$uid,$gid,$size) = split /\s+/,`ls -ld $file 2>/dev/null`;
417 return undef unless defined $mode;
418 $type = substr($mode,0,1);
419 # for (my $i=0;$i<3;$i++) { push @dates,'????-??-?? ??:??:??' }
420 # $date = `gfind $dir -maxdepth 1 -name $file -printf '%Ty-%Tm-%Td %TT\n'`;
424 # summarize statistics
428 $Ss += $size if $type eq '-';
433 # determine longest size field
435 my $x = length $size;
436 $opt_z = $x if $x>$opt_z;
438 $linkname = ${'opt_*'} ? $file : quote($file) ;
439 if ($type eq 'l' and $opt_f !~ /n/) {
440 my $link = readlink($file);
442 $linkname .= ' -> ' . (${'opt_*'} ? $link : quote($link));
446 #$mode .= ' ' unless $mode =~ /\+$/;
448 return ($linkname,$inode,$links,$size,$mode,$uid,$gid,$date,%dates);
459 $getfacl ||= pathsearch('getfacl') or return;
461 foreach my $file (@_) { push @files,$file if -e $file }
462 if (@files and open my $acl,'-|',$getfacl,'-ps',@files) {
464 $ACL{$1} = $1 if /^# file: (.+)/;
471 # reformat integer into 3-digit doted format
472 # (when non-numerical mode is set)
474 # INPUT: integer or '-'
479 if ($opt_n) { s/-/0/ }
480 else { while (s/(\d)(\d\d\d\b)/$1,$2/) {} }
485 # get all files matching pattern $opt_m
487 # INPUT: directory to scan
489 # OUTPUT: files which match (sorted, directories first)
496 if (opendir D,$dir) {
497 $dir = '' if $dir eq '.';
498 while (defined($f = readdir D)) {
500 # skip . and .. pseudo-subdirs
501 next if $f =~ m:(^|/)\.\.?/*$:;
502 # skip ONTAP snapshot dir
503 next if $f =~ m:(^|/)\.snapshot/*$:;
506 # skip jed and emacs backup files
507 # next if $f =~ /~$/ and not $opt_a and not $opt_l;
509 if ($f =~ /$opt_m/) {
511 if (not -l $x and -d $x and not ($opt_R or $postsort or $opt_U)) {
520 @files = &$lcsort(@files);
521 @dirs = &$lcsort(@dirs);
524 warn "$0: cannot read $dir : $!\n";
527 getacl(@dirs,@files) if $opt_l and not $opt_n;
528 return (@dirs,@files);
532 # reformat integer to string node
534 # INPUT: integer node
536 # OUTPUT: string node
539 return sprintf("%03d,%03d", ($rdev >> 8) & 255, $rdev & 255);
546 foreach my $dir (split(':',$ENV{PATH})) {
547 return "$dir/$prg" if -x "$dir/$prg";
552 # reformat timetick to ISO date string
556 # OUTPUT: ISO date string
558 my @d = localtime shift;
559 return sprintf('%d-%02d-%02d %02d:%02d:%02d',
560 $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]);
564 # quote file name to printable name and escape shell meta chars
566 # INPUT: original file name
568 # OUTPUT: printable file name
571 my $mc = '\'\[\]\\\\ `"$?&<>$*()|{};';
573 unless (defined $_) {
578 if (s/[\000-\037\200-\237\241-\250]/?/g or /\'/) {
581 # } elsif (/[$mc]/ or -d and /:/) {
591 my $link = readlink($file)||'';
593 return $file if basename($file) =~ /$opt_F/i;
594 return $link if basename($link) =~ /$opt_F/i;
600 my $opts = '[-lastcuidnrzLRxNS*] [-f format] [-D X:Y]';
601 local *OUT = $status ? *STDERR : *STDOUT;
604 print OUT "usage: $0 $opts [-F regexp] [file...]\n";
607 print OUT "usage: lf $opts regexp [regexp...] [directory]\n";
609 options: -l long list (implicit if called 'll')
610 -a list also .* files
613 -U sort by nothing (original i-node order)
614 -c list status change time instead of modification time
615 -u list last access time instead of modification time
616 -i list also inode and hard links numbers
617 -d do not list contents of diretories
620 -z squeeze size field (slows down output)
621 -L show absolute real path (dereference symbolic links)
622 -R recursive into subdirs
623 -x do not cross filesystem boundaries with -R
624 -F find files matching case insensitive regexp
625 -N show only normal (regular) files
626 -S print statistics summary at end
627 -* list plain file names (without \\ masking)
628 -f user defined format output, format characters are:
629 m=mode, u=user, g=group, s=size, l=hard links count, i=inode
630 n=name only, d=date, a=access+modification+inodechange dates
631 -D list only files newer than X and older than Y
632 XY format: NUMBER[smhd] (s=seconds, m=minutes, h=hours, d=days)
633 XY format: YYYY-MM-DD (Y=year, M=month, D=day)
641 l *.c # list files ending with .c
642 l -la # list all files in long format
643 l -Rrs # list files recursive reverse sorted by size
644 l -*f mus # list files native names with format: mode+user+size
645 l -D 10d: # list files newer than 10 days
646 ll # list files long format (equal to: l -l)
647 lll # list files extra long format (equal to: l -liS)
648 lf 'status.*mp3' # list files matching regexp (equal to: l -F 'status.*mp3')
649 lf sda1 /dev # list devices matching sda1 (equal to: l -RF sda1 /dev)