Rubyでlsコマンドを作ります
表題の通りlsコマンドを作ってみます とはいっても完全再現するととんでもないコード量になるので、今回はオプションを実装しません
また今回はフィヨルドブートキャンプのプラクティス内にあるlsコマンドを作るを題材として作っていきますので フィヨルドブートキャンプ生の方でプラクティスを修了前の方は閲覧を控えるようお願いします。
また筆者はフィヨルドブートキャンプ生ですが、該当プラクティス合格前に書いたコードになります。
余談になりますが、フィヨルドブートキャンプのカリキュラムではファイルやフォルダを指定するようにすることは必須ではありません。
要件
今回模倣するlsコマンドの要件です
- 引数無しで実行するとカレントディレクトリ内のファイルが一覧表示されること
- Rubyの標準ライブラリしか使わないこと
- 最大3列を維持して表示すること
- 列数の変更に対応できること
- ファイルやディレクトリを指定できること
- ファイルが指定された場合、相対パスで指定された場合相対パスを、チルダパスで指定された場合絶対パスを、絶対パスで指定された場合絶対パスを返す。
- 2バイト文字があっても表示が崩れないこと
実装
繰り返しになりますが、フィヨルドブートキャンプ生の方でプラクティス修了前の方は閲覧を控えるようお願い致します。
手始めに使う予定のライブラリと定数を定義します、合わせてshebangも記述しておきます。
#!/usr/bin/env ruby # frozen_string_literal: true require 'pathname' DISPLAY_COLUMNS_COUNT = 3 DISPLAY_WIDTH = 18
main
メソッドを定義して、その中に動きを定義していく感じで書いていこうと思います
ついでに後で使う予定の配列も定義しておきます
def main argument_type = argument_parse if ARGV[0] # ARGVには引数が入っている directory_files = [] # 後の計算で使用 end
処理を開始する前に今回はファイルやディレクトリを指定可能にしなければならないので、引数が渡されてるのかどうか、また引数はファイルなのかフォルダなのかを判断します。
ではその判断してくれるargument_parse
の中身を書いていきます
def argument_parse(argument) if File.directory?(argument) :directory_path elsif File.file?(argument) :file_path else :invalid end end
こんな感じでいけるかな?
rubyではメソッドが最終的に評価した式が返り値になるので、argument_type
に引数に渡されたタイプ毎に分類されたシンボルが返ります
この時文字列ではなくシンボルを返すのは内部的にシンボルのほうが処理速度が速いからです。
次にmain
メソッド側にこの戻り値を利用した処理を書き加えていきます
def main argument_type = argument_parse if ARGV[0] case argument_type when :invalid error_message # 渡された引数が不正ならエラーメッセージを表示するメソッドを呼び出す when :file_path display_file_results(ARGV[0]) # 渡された引数がファイルなら結果を出力するメソッドを呼び出す when :directory_path directory_files = Dir.glob("#{ARGV[0]}/*").map(&File.method(:basename)) # 渡された引数がディレクトリなら、渡されたディレクトリの一覧を取得する else directory_files = Dir.glob('*') # 引数が渡されなかったら現在のカレントディレクトリ内の一覧を取得する end end
こんな感じでいいでしょう。
実装が簡単な引数が不正な値だった場合から片付けていきます
def error_message puts "'#{ARGV[0]}'にアクセスできません:そのようなファイルやディレクトリはありません" end
いい感じですね。
では次に引数がファイルだった場合を考えていきます。
前述の要件の通りファイルが指定された場合は少し面倒くさいです。
何故この仕様なのかというと、筆者の環境でlsコマンドを使うとそのように結果が返ってくるからです。
相対パスと絶対パスでファイルを指定される時というのはそもそもそのままの値を返せばよく、チルダパスを指定された時のみ絶対値を計算すればよさそうなので、このように・・・
def display_file_results(file_path) specified_path = file_path[0] == '~' ? Pathname.new(file_path).expand_path('~') : file_path # 渡されたパスの先頭が"~"なら絶対パスを、それ以外ならそのまま返す puts specified_path end
実装完了です。
pathnameは標準ライブラリですので、requireを忘れないようにしてください。
次はいよいよ本命のディレクトリに移っていきましょう。
1個のメソッドに処理を詰め込んでしまってもいいのですが、後からのメンテナンス性と可読性を考えて小分けに実装することにします
メインメソッドをこんな感じに追記して...
def main 省略 row_size = calculate_row_size(directory_files) # 表示する行のサイズを計算するメソッド display_data = create_display_data(row_size,directory_files) # 計算した行のサイズを元に1行毎の配列を作る display_directory_results(row_size, display_data) # 1行毎の配列を列数に合わせて表示する end
この3個の処理を経て結果が表示されるようにしていきます。
まずはcalulate_row_size
で行数を計算します。
def calculate_row_size(directory_files) (directory_files.size + DISPLAY_COLUMNS_COUNT - 1) / DISPLAY_COLUMNS_COUNT - 1 #最適な行数の計算 end
表示したい列の行数をこれで row_size
に格納したので
次はこれをもとに実際に表示するためのデータを配列に入れていきます
def create_display_data(row_size, directory_files) directory_files.each_slice(row_size + 1).to_a # 1行毎の配列を作る,3行目は自動的に余りの数で作ってくれる end
これで1行当たりのデータが配列になってdirectory_files
に格納されました
次は先ほど計算したrow_size
と今回作成した配列directory_files
を使って実際にデータを出力します
def display_directory_results(row_size, display_data) (row_size + 1).times do |row| DISPLAY_COLUMNS_COUNT.times do |col| print display_data[col][row].to_s.ljust(DISPLAY_WIDTH) #ファイル名を18文字として左寄せで表示 end puts end puts end
これで完成です、お疲れ様でし・・
そうです、このままだと2バイト文字に対応してません。
2バイト文字が混ざると表記がずれるのは、2バイト文字であっても1文字は1文字とカウントされるためです。
そこで今回は表示するファイルに含まれる2バイトの文字を数え、その分だけDISPLAY_WIDTHから値を引くことで表記ずれを防ごうと思います。
まずは表示する側にコードを書きます。
def display_directory_results(row_size, display_data) (row_size + 1).times do |row| DISPLAY_COLUMNS_COUNT.times do |col| wide_chars_count = count_characters(display_data[col][row]) || 0 # wide_chars_countに2バイト文字の数を入れる、2バイト文字がなかった場合nilが返るので対策しておく print display_data[col][row].to_s.ljust(DISPLAY_WIDTH - wide_chars_count) end puts end puts end
それでは次にチェック側を書きます
def count_characters(file_name) file_name.each_char.count { |char| char.bytesize > 1 } if !!(file_name =~ /[^[:ascii:]]/) #2バイト文字があればカウントを1増やす end
if !!(file_name =~ /[^[:ascii:]]/)
はなくても動作しますが、むやみやたらにファイル名の全文字を解析するのもリソースの無駄なので、ascii文字だけで構成されたファイル名の場合はスルーさせています。
これで後は表示を確認してあげれば
大丈夫そうですね、お疲れ様でした。