lsコマンドを作る オブジェクト指向版

以前、Rubyの標準ライブラリのみで実装したLinuxUnix系のOSで使われるlsコマンドを、オブジェクト指向版として実装しました。

要件
  • オプションなし 最大3列で表示。 ファイルの並び順は以下のとおり。
1 5 9
2 6 10
3 7
4 8
  • -a-r-lオプションを併用できる形で実装する。

  • 実装の対象外の機能

    • 引数にファイルやディレクトリを指定可能にする。
    • mac拡張属性(@マーク)の表示。

ls_command.rb

  • コマンドラインからの入力を受け取り、それに基づいて上記のクラスを利用してファイル一覧を表示。OptionParserを用いてコマンドライン引数(-a, -r, -l)を解析し、その結果をFileListクラスのインスタンスに渡して表示を行う。
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'optparse'
require_relative 'file_info'
require_relative 'file_permission'
require_relative 'file_list'

params = {}
OptionParser.new do |opt|
  opt.on('-a') { |v| params[:a] = v }
  opt.on('-r') { |v| params[:r] = v }
  opt.on('-l') { |v| params[:l] = v }
  opt.parse!(ARGV)
end

file_list = FileList.new(params)
file_list.display_lists

file_info.rb

  • 特定のファイルに関する情報を取得。
# frozen_string_literal: true

require 'etc'

class FileInfo
  attr_reader :file

  def initialize(file)
    @file = file
  end

# ファイルの統計情報(サイズ、所有者、グループ、更新時間など)を取得
  def stat
    File.stat(file)
  end

# ファイルのパーミッションをFilePermissionクラスを用いて取得
  def permissions
    FilePermission.new(file).format_permissions
  end

# ファイルのハードリンク数を取得
  def hard_link
    stat.nlink
  end

# ファイルの所有者のユーザー名を取得
  def user
    Etc.getpwuid(stat.uid).name
  end

# ファイルの所有グループ名を取得
  def group
    Etc.getgrgid(stat.gid).name
  end

# ファイルのサイズを取得
  def filesize
    stat.size
  end

# ファイルの最終更新時間を取得
  def mtime
    File.mtime(file)
  end
end

file_list.rb

  • ディレクトリ内のファイル一覧を取得し、その一覧を指定された形式で表示。
# frozen_string_literal: true

class FileList
  COLUMNS = 3

  def initialize(params)
    @params = params
  end

  def display_lists
    file_match_option = @params[:a] ? File::FNM_DOTMATCH : 0
    files = Dir.glob('*', file_match_option).map { |file| FileInfo.new(file) }
    current_files = @params[:r] ? files.reverse : files
    @params[:l] ? display_in_long_format(current_files) : display_in_short_format(current_files)
  end

  private

  def display_in_short_format(lists)
    max_filename_length = lists.map { |file_info| file_info.file.length }.max
    line_count = (lists.length.to_f / COLUMNS).ceil
    line_count.times do |line|
      lists.each_slice(line_count) do |columns|
        print columns[line].file.ljust(max_filename_length + 2) if columns[line]
      end
      print "\n"
    end
  end

  def display_in_long_format(current_files)
    display_total_block_number(current_files)
    max_lengths = calculate_maximum_lengths(current_files)
    current_files.each { |file_info| display_file_details(file_info, max_lengths) }
  end

  def display_total_block_number(current_files)
    total = current_files.sum { |file| file.stat.blocks }
    puts "total #{total}"
  end

  def display_file_details(file_info, max_lengths)
    permissions = file_info.permissions
    hard_link = file_info.hard_link.to_s.rjust(max_lengths[:hard_link])
    user = file_info.user.rjust(max_lengths[:user])
    group = file_info.group.rjust(max_lengths[:group])
    filesize = file_info.filesize.to_s.rjust(max_lengths[:filesize])
    time = file_info.mtime.strftime('%m %d %H:%M')
    filename = file_info.file
    puts "#{permissions}  #{hard_link} #{user}  #{group}  #{filesize} #{time} #{filename}"
  end

  def calculate_maximum_lengths(file_stats)
    max_lengths = {
      hard_link: 0,
      user: 0,
      group: 0,
      filesize: 0
    }

    file_stats.each do |file|
      max_lengths[:hard_link] = [max_lengths[:hard_link], file.hard_link.to_s.length].max
      max_lengths[:user] = [max_lengths[:user], file.user.length].max
      max_lengths[:group] = [max_lengths[:group], file.group.length].max
      max_lengths[:filesize] = [max_lengths[:filesize], file.filesize.to_s.length].max
    end

    max_lengths
  end
end

file_permission.rb

  • ファイルのパーミッションを表す文字列を生成。 File.lstat(file).mode.to_s(8)を使用してファイルのモード(パーミッションとファイルの種類を含む)を8進数の文字列として取得し、読みやすい形式(例: 'rwxr-xr-x')に変換。
class FilePermission
  PERMISSIONS = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx'].freeze

  FILE_TYPE = {
    '10' => 'p',
    '20' => 'c',
    '40' => 'd',
    '60' => 'b',
    '100' => '-',
    '120' => 'l',
    '140' => 's'
  }.freeze

  attr_reader :file

  def initialize(file)
    @file = file
  end

  def format_permissions
    type_of_file + permission_code
  end

  private

  def type_of_file
    FILE_TYPE[file_mode[0..-4]]
  end

  def permission_code
    permission_numbers.map { |n| PERMISSIONS[n] }.join
  end

  def file_mode
    File.lstat(file).mode.to_s(8)
  end

  def permission_numbers
    file_mode[-3..].chars.map(&:to_i)
  end
end

【参考】