■ ll (ls -l) コマンド

Unix 系のコマンドで、ls コマンドはみなさんお世話になっていると思います。 この ls コマンドに -l オプションで使うこともとても多いと思います。 ll.rb スクリプトは、ls -l オプションと同等品で、そのほかに -F オプション --color オプションなどもサポートしています。 Ruby で、ファイルの情報をとりだすサンプルになりますので試してみてください。

■ 使い方

デフォルトでは、ls -l と同じ動作を行います。 オプションは次のものがあります。

オプション説明
-a「.」ではじまるファイル(ディレクトリ)名も表示
-cctime でソート
-d引数でしていされたディレクトリは展開せずにそのまま
-fソートしません
-tmtime でソート
-uatime でソート
-Fファイル名にシンボルをつけてファイルのタイプ別に表示
-pPukiWiki ファイル名を漢字ファイル名に変換
--colorカラーでファイルのタイプ別に表示

実行サンプルです。

$ ll.rb /t/lib/ruby/1.6/i686-linux 
total 1060
-rw-r--r--   1 root     root         2596 Aug 25 21:46 config.h
-r-xr-xr-x   1 root     root        13788 Sep  1 02:05 curses.so*
-rw-r--r--   1 root     root         1874 Aug 25 21:46 defines.h
-rw-r--r--   1 root     root          622 Aug 25 21:46 dln.h
-rw-r--r--   1 root     root         1093 Sep  1 01:05 env.h
-r-xr-xr-x   1 root     root         6724 Sep  1 02:05 etc.so*
-r-xr-xr-x   1 root     root         4272 Sep  1 02:05 fcntl.so*
-r-xr-xr-x   1 root     root        12212 Sep  1 02:05 gdbm.so*
-r-xr-xr-x   1 root     root        27844 Aug 29 02:28 getconf.so*
-rw-r--r--   1 root     root        13202 Aug 29 19:58 intern.h
-rw-r--r--   1 root     root       787520 Sep  1 20:01 libruby-1.6.0.a
-r-xr-xr-x   1 root     root         8004 Sep  1 02:05 md5.so*
-r-xr-xr-x   1 root     root        15348 Sep  1 02:05 nkf.so*
-rw-r--r--   1 root     root        10512 Aug 30 21:28 node.h
-r-xr-xr-x   1 root     root         9108 Sep  1 02:05 pty.so*
-rw-r--r--   1 root     root         3862 Sep  1 20:01 rbconfig.rb
-rw-r--r--   1 root     root          981 Aug 25 21:46 re.h
-r-xr-xr-x   1 root     root        10368 Sep  1 02:05 readline.so*
-rw-r--r--   1 root     root         7984 Aug 25 21:46 regex.h
-rw-r--r--   1 root     root        15701 Sep  1 01:05 ruby.h
-rw-r--r--   1 root     root         1713 Aug 25 21:46 rubyio.h
-rw-r--r--   1 root     root         1404 Aug 25 21:46 rubysig.h
-r-xr-xr-x   1 root     root        17332 Sep  1 02:05 sdbm.so*
-r-xr-xr-x   1 root     root        27656 Sep  1 02:05 socket.so*
-rw-r--r--   1 root     root         1049 Aug 25 21:46 st.h
-r-xr-xr-x   1 root     root        11032 Sep  1 02:05 tcltklib.so*
-r-xr-xr-x   1 root     root         4200 Sep  1 02:05 tkutil.so*
-rw-r--r--   1 root     root         1220 Aug 25 21:46 util.h
-rw-r--r--   1 root     root          133 Sep  1 20:01 version.h

zsh などのシェルでは、次のように alias するといいでしょう。

alias ll='ll.rb --color -F'

■ ソースコード

Web ブラウザでの表示上は Ruby のスクリプトですが、HTML の特殊文字をエスケープしています。 テキストとしてセーブしてご利用ください。

#! /usr/local/bin/ruby
# /home/tetsu/src/ruby/toolbox/ll.rb
# Created: August 22,2000 Tuesday 10:09:39
# Author: tetsu(WATANABE Tetsuya)
RCS_ID = %q$Id: ll.rb,v 1.26 2005/12/03 10:37:00 tetsu Exp $
# usage:

def usage
  STDERR.print <<EOF
usage: #{$0} [-acdftuFp] [dir, file...]
   -a  all
   -d  directory
   -F  classify 
   -c  sort of ctime
   -t  sort of mtime
   -u  sort of atime
   -f  no sort
   -p  pukiwiki name

#{RCS_ID}
EOF
  exit 1
end

require 'etc'

class Dir_colors
  FILE_NAME = '/etc/DIR_COLORS'
  @@color_str = nil

  def initialize
    if @@color_str.nil?
      @term = {}
      @str = {}
      begin
        File.foreach(FILE_NAME) do |l|
          next if l =~ /^\#/ || l =~ /^\s*$/
          l.sub!(/\#.*$/, '')
          l.sub!(/\s*$/, '')
#         key, val = l.chop.split(/[\s]+/, 2)
          key, val = l.split(/[\s]+/, 2)
          if key == 'TERM'
            @term[val] = true
          elsif
            @str[key] = "\C-[[" + val + 'm'
          end
        end
        if @term[ENV['TERM']].nil?
          @str.each do |k, val|
            val.replace('')
          end
        end
      rescue Errno::ENOENT
        case ENV['TERM']
        when 'kterm', 'xterm', 'linux'
          @str = {
            'NORMAL' => "\C-[[00m",
            'DIR' => "\C-[[01m",
            'FIFO' => "\C-[[40;33m",
            'LINK' => "\C-[[01;36m",
            'SOCK' => "\C-[[01;35m",
            'EXEC' => "\C-[[01;32m",
            '.jpg' => "\C-[[00;35m",
            '.gif' => "\C-[[00;35m",
            '.bmp' => "\C-[[00;35m",
            '.xbm' => "\C-[[00;35m",
            '.xpm' => "\C-[[00;35m",
            '.png' => "\C-[[00;35m",
            '.tif' => "\C-[[00;35m"
          }
        end
      end
      @@color_str = @str
    else
      @str = @@color_str
    end
  end
  attr_reader :term, :str
end

class Ls
  @@uid2n = {
    0 => 'root'
  }
  @@gid2n = {
    0 => 'root'
  }
  @@mode2str = {
    0100644 => '-rw-r--r--',
    0040755 => 'drwxr-xr-x',
    0100755 => '-rwxr-xr-x',
    0100444 => '-r--r--r--',
    0100600 => '-rw-------',
    0060660 => 'brw-rw----',
    0100555 => '-r-xr-xr-x',
    0040775 => 'drwxrwxr-x',
    0020660 => 'crw-rw----',
    0120777 => 'lrwxrwxrwx'
  }
  @@time_ck = Time.now - 6 * 30 * 24 * 60 * 60
  @@istty = nil

  def initialize(opt, arg = '.')
    @opt = opt
    @arg = arg
    @color = Dir_colors.new if opt['color']
    if @@istty.nil?
      @@istty = STDOUT.tty? 
      @@istty = false if @color.nil?
    end
    @name_arr = []
    @stat_arr = {}
    @total = 0
    @lstat = File.lstat(arg)
    @type = if @lstat.symlink?
              'file'
            elsif @lstat.directory?
              if opt['directory'] then 'file' else 'directory' end
            else
              'file'
            end
    case @type
    when 'file'
      @name_arr.push(arg)
      @stat_arr[arg] = @lstat
    when 'directory'
      Dir.open(arg).each do |f|
        next if f =~ /^\./ && !@opt['all']
        @name_arr.push(f)
        @stat_arr[f] = File.lstat(arg + '/' + f)
        @total += @stat_arr[f].blocks
      end
    end
  end

  def add_file(arg)
    @name_arr.push(arg)
    @stat_arr[arg] = File.lstat(arg)
  end

  def ll(file, stat)
    cfile = name2color(file, stat)
    if @opt['pwiki_name'] && file =~ /[A-F\d]{2}/
      cfile += ' ' + file.gsub(/[A-F\d]{2}/) {|x| [x.hex].pack('C*') }
    end
    time_val = if @opt['ctime_sort']
                 stat.ctime
               elsif @opt['atime_sort']
                 stat.atime
               else
                 stat.mtime
               end
    time_fmt = if time_val < @@time_ck then '%b %d  %Y' else '%b %d %H:%M' end
    begin
      @@uid2n[stat.uid] = Etc.getpwuid(stat.uid).name unless @@uid2n.key?(stat.uid)
    rescue ArgumentError
      @@uid2n[stat.uid] = stat.uid.to_s
    end
    begin
      @@gid2n[stat.gid] = Etc.getgrgid(stat.gid).name unless @@gid2n.key?(stat.gid)
    rescue ArgumentError
      @@gid2n[stat.gid] = stat.gid.to_s
    end
    if stat.blockdev? or stat.chardev?
      str = sprintf("%-10s %3d %-8s %-8s %3d, %3d %s %s",
                    mode2str(stat),
                    stat.nlink,
                    @@uid2n[stat.uid],
                    @@gid2n[stat.gid],
                    stat.rdev_major,
                    stat.rdev_minor,
                    time_val.strftime(time_fmt).sub(/(\w{3}) 0(\d)\b/, '\1  \2'),
                    cfile)
    else
      str = sprintf("%-10s %3d %-8s %-8s %8d %s %s",
                    mode2str(stat),
                    stat.nlink,
                    @@uid2n[stat.uid],
                    @@gid2n[stat.gid],
                    stat.size,
                    time_val.strftime(time_fmt).sub(/(\w{3}) 0(\d)\b/, '\1  \2'),
                    cfile)
    end
    bad_link = false
    if stat.symlink?
      link = ''
      begin
        f = if @type == 'file' then file else @arg + '/' + file end
        stat = File.stat(f)
        linkname =  File.readlink(f)
        link = ' ' + name2color(linkname, stat)
      rescue Errno::ENOENT
        bad_link = true
      end
      str.concat(' ->' + link)
    end
    if ! bad_link && @opt['classify']
      if stat.directory?
        str.concat('/')
      elsif stat.symlink?
        str.concat('@')
      elsif stat.socket?
        str.concat('=')
      elsif stat.pipe?
        str.concat('|')
      elsif stat.executable?
        str.concat('*')
      end
    end
    str.concat("\n")
  end

  def name2color(name, stat)
    if @@istty && @opt['color']
      if stat.directory?
        @color.str['DIR'] + name + @color.str['NORMAL']
      elsif stat.symlink?
        @color.str['LINK'] + name + @color.str['NORMAL']
      elsif stat.socket?
        @color.str['SOCK'] + name + @color.str['NORMAL']
      elsif stat.pipe?
        @color.str['FIFO'] + name + @color.str['NORMAL']
      elsif stat.executable?
        @color.str['EXEC'] + name + @color.str['NORMAL']
      elsif name =~ /\.(?:jpg|gif|bmp|xbm|xpm|png|tif)$/
          @color.str[$&] + name + @color.str['NORMAL']
      else
        name
      end
    else
      name
    end
  end

  def display(port = $>)
    begin
      port.write self
    rescue Errno::EPIPE
    end
  end

  def to_s
    unless @opt['no_sort']
      if @opt['mtime_sort']
        @name_arr.sort! do |a, b|
          [@stat_arr[b].mtime, a] <=> [@stat_arr[a].mtime, b]
        end
      elsif @opt['ctime_sort']
        @name_arr.sort! do |a, b|
          [@stat_arr[b].ctime, a] <=> [@stat_arr[a].ctime, b]
        end
      elsif @opt['atime_sort']
        @name_arr.sort! do |a, b|
          [@stat_arr[b].atime, a] <=> [@stat_arr[a].atime, b]
        end
      else
        @name_arr.sort!
      end
    end

    str = ''
    str.concat(sprintf("total %d\n", @total / 2)) if @type == 'directory'
    @name_arr.each do |f|
      str.concat(ll(f, @stat_arr[f]))
    end
    str
  end

  def mode2str(stat)
    m = stat.mode
    return @@mode2str[m] if @@mode2str.key?(m)
    str = '----------'
    str[0] = if stat.file?;         '-'
             elsif stat.directory?; 'd'
             elsif stat.symlink?;   'l'
             elsif stat.socket?;    's'
             elsif stat.pipe?;      'p'
             elsif stat.chardev?;   'c'
             elsif stat.blockdev?;  'b'
             else '-'
             end
    str[1] = if m & 0400 == 0 then '-' else 'r' end
    str[2] = if m & 0200 == 0 then '-' else 'w' end
    str[3] = if m & 0100 == 0
               if stat.setuid? then 'S' else '-' end
             else
               if stat.setuid? then 's' else 'x' end
             end
    str[4] = if m & 040 == 0 then '-' else 'r' end
    str[5] = if m & 020 == 0 then '-' else 'w' end
    str[6] = if m & 010 == 0
               if stat.setgid? then 'S' else '-' end
             else
               if stat.setgid? then 's' else 'x' end
             end
    str[7] = if m & 04 == 0 then '-' else 'r' end
    str[8] = if m & 02 == 0 then '-' else 'w' end
    str[9] = if m & 01 == 0
               if stat.sticky? then 'T' else '-' end
             else
               if stat.sticky? then 't' else 'x' end
             end
    @@mode2str[m] = str
  end
end

Opt = {
  'all' => false,
  'directory' => false,
  'atime_sort' => false,
  'ctime_sort' => false,
  'mtime_sort' => false,
  'no_sort' => false,
  'classify' => false,
  'color' => false,
  'pwiki_name' => false
}

while ARGV[0] =~ /^-/
  $_ = ARGV.shift
  if ~/^--color/
    Opt['color'] = true
  elsif ~/^--/
    usage
  else
    Opt['all'] = true if ~/a/
    Opt['ctime_sort'] = true if ~/c/
    Opt['directory'] = true if ~/d/
    Opt['no_sort'] = true if ~/f/
    Opt['mtime_sort'] = true if ~/t/
    Opt['atime_sort'] = true if ~/u/
    Opt['classify'] = true if ~/F/
    Opt['pwiki_name'] = true if ~/p/
    usage if ~/[^\-acdftuFp]/
  end
end

if ARGV.size == 0
  ls = Ls.new(Opt)
  ls.display
elsif ARGV.size == 1
  ls = Ls.new(Opt, ARGV.shift)
  ls.display
else
  flag_init = true
  ARGV.each do |arg|
    if File.symlink?(arg) || Opt['directory'] || !File.directory?(arg) 
      if flag_init
        ls = Ls.new(Opt, arg)
        flag_init = false
      else
        ls.add_file(arg)
      end
    end
  end
  ls.display unless flag_init

  ARGV.each do |arg|
    unless File.symlink?(arg) || Opt['directory'] || !File.directory?(arg) 
      ls = Ls.new(Opt, arg)
      print "\n", arg, ':', "\n"
      ls.display
    end
  end
end

■ 解説

この ll.rb は、まさに ls -l コマンドそのままです。 ということで、コマンドとしては特別なことはなく、オプションで指定した動きをします。 ただ、余計なマスクとかはしていないので漢字のファイル名もそのまま表示します。 Win のパーティションやドライブを mount している場合に便利だったりします。

処理的には次のようなことをしています。

内部のロジックはちょっとわかりにくいですね。

■ 履歴

1.26 2005/12/3

デバイスファイル対応。 stat.mode の数字を使っていた部分を File::Stat で置き換え。

1.25 2005/12/2

-p の PukiWiki ファイル名のエンコードオプションの対象を「.txt」以外にも拡張。

1.24 2005/11/24

-p オプションで PukiWiki ファイル名を漢字表示。 現在は漢字コードが EUC Japan 固定です。

1.23 2003/5/1

Ruby 1.8 対応。 File.foreach chop

1.22 2003/3/25

2003/3/23 対応修正。 [^-abc] 表記でエスケープが必要になったため。

1.21 2002/5/24

パーミッションのよく使われるものを先に生成するようにしました。

1.20 2002/4/30

いままで補足していなかった例外 Errno::EPIPE を補足するようにしました。

1.19 2001/10/3

拡張子のカラー化の正規表現を間違っていました。

1.18 2001/8/14

ruby 1.7.1 2001-08-06 対応です。


渡辺哲也(WATANABE Tetsuya): Tetsuya.WATANABE atmark nifty.com