最小限のls -laを書いてみた

プログラミング初心者にオススメのPerl本にて、リャマ本(『初めてのPerl』)を読了した段階でls(1)grep(1)の簡易版を作れるようになっていればよい、と書きました。そこで実装例として、最小限のls(1)を実装してみました。また、リャマ本読了程度の知識を前提にすることと、Perl 5.8ベースで標準モジュール以外は使わない<ことを制約としました。なお、所要時間は一時間程度です。
仕様は最小限ながらそれなりに役立つようにしたかったので、以下のようにしました。

  • 引数としてディレクトリ名をうけとる
  • オプションは-la(長い出力ですべてのファイルを表示)相当で固定
  • group名、user名は省略し、パーミッション・ファイル更新日時・ファイル名を出

実装の際のポイントは以下のとおりです。

  • stat()やlocaltime()の戻り値は扱いづらいため、File::stat/Time::localtimeモジュールを使った
    • perldoc -f stat, perldoc -f localtimeにこれらモジュールを使うと楽である旨が書かれている
  • パーミッションは扱いづらいのでFcntlモジュールを使った
    • その際perldoc -f statstat(2)を参照した
  • ファイルサイズや日付を固定幅で出力するためにsprintf()を使った

パーミッションの文字列化以外はそれほど難しいところはありません。ぜひ、自分でも実装してみてください。

#!perl
# tinyls.pl
use strict;
use warnings;

use File::stat      qw(stat);
use Time::localtime qw(localtime);
use Fcntl           qw(:mode);

main(@ARGV);

sub main {
    my(@argv) = @_;

    @argv = ('.') if !@argv;

    foreach my $dir(@argv) {
        print list_one($dir);
    }
}

sub list_one {
    my($dir) = @_;

    my @output;

    opendir my $dh, $dir or die "Cannot opendir '$dir': $!";

    foreach my $file(sort readdir $dh) {
        my $st = stat("$dir/$file") or die "Cannot stat '$dir/$file': $!";

        my $mode = make_mode( $st->mode );

        my $size  = sprintf '%8s', $st->size;

        my $tm    = localtime($st->mtime);
        my $mtime = sprintf '%d-%02d-%02d %02d:%02d:%02d',
            $tm->year + 1900, $tm->mon + 1, $tm->mday,
            $tm->hour, $tm->min, $tm->sec;

        push @output, "$mode $size $mtime $file\n";
    }

    closedir $dh;

    return join '', @output;
}

sub make_mode {
    my($mode) = @_;

    my @m;
    push @m, $mode & S_IFDIR ? 'd' : '-';

    push @m, make_perm($mode, S_IRUSR, S_IWUSR, S_IXUSR);
    push @m, make_perm($mode, S_IRGRP, S_IWGRP, S_IXGRP);
    push @m, make_perm($mode, S_IROTH, S_IWOTH, S_IXOTH);

    return join '', @m;
}

sub make_perm {
    my($m, $r, $w, $x) = @_;

    return join '',
        $m & $r ? 'r' : '-',
        $m & $w ? 'w' : '-',
        $m & $x ? 'x' : '-',
    ;
}
__END__

実行結果はたとえば以下のようになります。

App-tinyls$ perl tinyls.pl
drwxr-xr-x     4096 2011-05-14 13:00:39 .
drwxr-xr-x     4096 2011-05-14 11:32:31 ..
drwxr-xr-x     4096 2011-05-14 13:02:55 .git
-rw-r--r--      200 2011-05-14 11:32:31 .gitignore
-rw-r--r--      174 2011-05-14 11:32:31 .shipit
-rw-r--r--      201 2011-05-14 11:32:31 Changes
-rw-r--r--       98 2011-05-14 11:32:31 MANIFEST.SKIP
-rw-r--r--      993 2011-05-14 11:33:06 Makefile.PL
-rw-r--r--      766 2011-05-14 11:32:31 README
drwxr-xr-x     4096 2011-05-14 11:32:31 author
drwxr-xr-x     4096 2011-05-14 11:32:31 lib
drwxr-xr-x     4096 2011-05-14 13:00:31 script
drwxr-xr-x     4096 2011-05-14 11:32:31 t
drwxr-xr-x     4096 2011-05-14 11:32:31 xt

同じディレクトリで普通のls -laを実行すると以下のようになります。だいたい合ってますね。

App-tinyls$ ls -la
total 56
drwxr-xr-x   8 gfx gfx 4096 May 14 13:00 .
drwxr-xr-x 126 gfx gfx 4096 May 14 11:32 ..
drwxr-xr-x   8 gfx gfx 4096 May 14 13:02 .git
-rw-r--r--   1 gfx gfx  200 May 14 11:32 .gitignore
-rw-r--r--   1 gfx gfx  174 May 14 11:32 .shipit
-rw-r--r--   1 gfx gfx  201 May 14 11:32 Changes
-rw-r--r--   1 gfx gfx   98 May 14 11:32 MANIFEST.SKIP
-rw-r--r--   1 gfx gfx  993 May 14 11:33 Makefile.PL
-rw-r--r--   1 gfx gfx  766 May 14 11:32 README
drwxr-xr-x   2 gfx gfx 4096 May 14 11:32 author
drwxr-xr-x   3 gfx gfx 4096 May 14 11:32 lib
drwxr-xr-x   2 gfx gfx 4096 May 14 13:00 script
drwxr-xr-x   2 gfx gfx 4096 May 14 11:32 t
drwxr-xr-x   2 gfx gfx 4096 May 14 11:32 xt

See also:

追記:
id:dankogai氏に勝手に添削されました。
404 Blog Not Found:perl - 勝手に添削 - 最小限のls -laを書いてみた

明らかに too tiny だったので。

ということでした!