はじめに
こんにちは!
DMM WEBCAMP Advent Calendar 2020の24日目を担当するメンターの@shinoooooです。
クリスマスまであと1日ですね!
皆様は、どうお過ごしになられるでしょうか?
人それぞれ予定は異なるとは思いますが、まずは貴重なクリスマスイブの時間を割いてこの記事を読んでいただいていることに、感謝申し上げます。
ありがとうございます!
この記事では、Rubyで実装したlsコマンドの解説及び、実装を通して得られた知見を書きました。
自分自身もRubyを学習中の身ですので、記事の内容やコードに関するアドバイスをいただけましたら幸いです。
開発環境
- macOS Catalina 10.15.7
- Ruby 2.7.2
- zsh 5.8
lsコマンドとは
プログラミングを学習されている方は、一度は実行したことがあるコマンドかと思います。
ls
の機能は、Linux関連のドキュメントを和訳しているサイトによると、
ls, dir, vdir - ディレクトリの中身をリスト表示する
と書かれています。
実際に実行すると、
$ ls
hoge huga index.html #カレントディレクトリ内のファイルが出力される。
このような表示が出てくるかと思います。
今回は、このlsコマンドをRubyで実装し、
$ ruby ls.rb
hoge huga index.html
と出すことが目標です。
仕様
今回実装したls.rb
の仕様は、下記の通りです。
-
ls
,ls -l
,ls -a
,ls -la
と同等の機能を実装- 'optparse'というライブラリを使った都合上、
-la
のように複数のオプションをまとめて指定することができない。組み合わせる場合は、-l -a
と入力する必要がある。
- 'optparse'というライブラリを使った都合上、
- ACLと呼ばれる機能(説明は割愛します)の表示は未実装
- 実際の
ls
のソースコードを参考にしていないので、アルゴリズムや関数名が違うかもしれません。
出力形式
いきなりコードを書いて模倣することはできないので、ls
の出力の分析を行います。
ディレクトリとファイルを用意した方がわかりやすいので、下記のコマンドを実行します。
# directory1~directory10とfile1~file10をtestディレクトリ内に作成。
$ mkdir test/directory{1..10} && touch test/file{1..10}
そして、ls test
を実行して出力結果を確認します。
$ ls test
directory1 directory3 directory6 directory9 file2 file5 file8
directory10 directory4 directory7 file1 file3 file6 file9
directory2 directory5 directory8 file10 file4 file7
当たり前といえば、当たり前ですが綺麗に並んでいます。
闇雲に出力するだけではls
のような整った出力結果にはならなそうなので、課題を1つ1つ洗い出しながら完成を目指すことにします。
表示順
ls
で出力されているファイル名は以下のような順番で昇順で並んでいます。
「ファイルの数」や「ターミナルの画面の大きさ」、「ファイル名の長さ」で、「1行に表示されるファイルの数」と「行数」は若干出力は変化しますが、表示順で確認したls test
の出力は以下のようになっていました。
名前が1番目に若いファイル 名前が4番目に若いファイル 名前が7番目に若いファイル ... 名前が19番目に若いファイル
名前が2番目に若いファイル 名前が5番目に若いファイル ︙ ... 名前が20番目に若いファイル
名前が3番目に若いファイル 名前が6番目に若いファイル ︙
1行ずつ名前が1,4,7,10,13,16,19番目に若いファイル
,名前が2,5,8,11,14,17,20番目に若いファイル
,名前が3,6,9,12,15,18番目に若いファイル
を順に出力すればls
の出力の再現ができそうです。
配列の処理
ディレクトリ内のファイル一覧を取得するために用いたDirクラスは、ファイル一覧を配列で渡してくれます。今回は、これをfilesという変数に代入して扱うこととします。
ですが、受け取った配列の中身が昇順ではなかったのでArrayクラスのsort
メソッドで昇順に並び替えます。
files = ["file2", "file3", "file1"]
files.sort #=> ["file1", "file2", "file3"]
配列の添字は0から始まります。
昇順で並び替えたことにより、files内の1番目に若いファイル名はfiles[0]
とすることで取得することができます。
表示順で書いた名前が1,4,7,10,13,16,19番目に若いファイル
を出力するということは、files[0],files[3],files[6],files[9],files[12],files[15],files[18]
を出力することと言えます。
1行に出力できるファイル数や、行数を求め、files
をうまく出力する処理を書くと、下記のような出力になります。
$ ruby ls.rb test
directory1directory3directory6directory9file2file5file8
directory10directory4directory7file1file3file6file9
directory2directory5directory8file10file4file7
可読性は低いですが、一歩ls
に近づきました。
ファイル名の間の区切り
このままだとどこからどこまでが1つのファイル名か分からないので、ファイルとファイルを区切る必要があります。
ls
の出力結果のファイル名とファイル名はどう区切られているか、確認していきたいと思います。
適当にprint.rb
というような名前のファイルを作成し、ls test
の実行結果の
directory1 directory3
をコピーし、貼り付けます。
これを文字列としてターミナルに出力してみたいと思います。
" "
で囲み、先頭にp
を付け、
p "directory1 directory13"
として保存します。
ターミナル上でruby print.rb
と実行してみると以下のように出力されます。
$ ruby print.rb
"directory1\tdirectory13"
directory1とdirectory3の間に\t
という文字が現れました。
これは、タブ文字を表しています。
このことから、ファイル名がタブ文字で区切られていることが確認できます。
これを模倣してファイル名の末尾に\t
を追加することにします。
そのためにprintf
メソッドを使用します。
ここではprintf
について詳しく解説はしないので、気になる方はドキュメントを読んでみてください。
printf("%s\t", files[0]) # => %sの中にfiles[0]の中身が入り、出力される。
printf("%s\t", files[1])
# => "directory1 ""directory10 "
ls.rb
にも実装すると、以下のような出力になります。
$ ruby ls.rb test
directory1 directory3 directory6 directory9 file2 file5 file8
directory10 directory4 directory7 file1 file3 file6 file9
directory2 directory5 directory8 file10 file4 file7
先ほどと比べると綺麗に見えますが、まだls
の出力とは異なります。
ファイル名の長さの統一
最後に、綺麗に見せるためにファイル名の長さを統一します。
directory10
とfile1
、file2
、file3
を使い、考えてみましょう。
現時点での出力はこのようになっています。
directory10\tfile2
file1\tfile3
ファイル名の長さが統一されていないことで、1行毎の出力に文字数のズレが生じています。
全てのファイル名の長さが同じであれば、
ファイル名\tファイル名\t
ファイル名\tファイル名\t
のように綺麗な見た目にすることができるので空白文字を用いて、文字数が一番長いファイル名の長さに統一します。
今回の例では、directory1
が一番ファイル名が長いので,directory1
の長さに統一することにします。
# 空白文字を使うことで見た目を整えることができる!
directory\tfile2 \t
file1 \tfile3 \t
文字列の長さの統一もprintf
メソッドで実現することができます。
%s
を少し改変することで幅指定を行うことができます。
printf("%15s", files[0])
# => " directory1" 15文字になるよう空白文字が追加される。
printf("%-15s", files[0])
# => "directory1 " -をつけると左詰めになる。
name_len = 15
printf("%-#{name_len}s\t", files[0]) # => 式展開もできる。
# => "directory1 "
ls.rb
でも幅指定をすると
$ ruby ls.rb test
directory1 directory3 directory6 directory9 file2 file5 file8
directory10 directory4 directory7 file1 file3 file6 file9
directory2 directory5 directory8 file10 file4 file7
ls
と同じ出力を得ることができました!
解説
全てを解説すると、長くなってしまうので、出力形式で説明した箇所のみを解説します。
また、呼び出しているメソッドに関しては細かい説明はせず、機能の解説のみをコメント文で行っています。
ソースコードは 完成コードにあります。
全体の実装が気になる方は、こちらを参照してください。
self.display_normal
def self.display_normal(dir)
files = get_files(dir) # ディレクトリ内のファイル名の配列を、filesに代入する。
name_len = 1 # ファイル名の最大値を格納するname_lenを用意する。
files.map { |file| name_len = file.length if name_len < file.length } # 一番長いファイル名を、name_lenに代入する。
total_length = (name_len + 5) * files.count # 出力すべき文字数を算出。
columns = `tput cols`.to_i # ターミナルの1行の文字数を取得。
line_count = (total_length + (columns - 1)) / columns # 必要な出力行数を求める。
column_count = (files.count + (line_count / 2)) / line_count # 1行に表示できるファイル数を求める。
line_count = 1 if line_count == 0 # 最低でも1行は出力する必要があるため、line_countが0になっていた場合、1を代入する。
(0...line_count).each do |line|
(0..column_count).each do |column|
idx = line_count * column + line
printf("%-#{name_len}s\t", files[idx]) if idx < files_count # 添字が配列内の大きさを超えていなければ、ターミナルに出力する
end
print("\n") # ここに到達すると1行分の出力が終わっているので改行する。
end
end
実行時間
time
コマンドを使い、実行時間を計測します。
$ time ls test
directory1 directory3 directory6 directory9 file2 file5 file8
directory10 directory4 directory7 file1 file3 file6 file9
directory2 directory5 directory8 file10 file4 file7
ls test 0.00s user 0.00s system 78% cpu 0.008 total
# ls testの実行時間 0.00s
$ time ruby ls.rb test
directory1 directory3 directory6 directory9 file2 file5 file8
directory10 directory4 directory7 file1 file3 file6 file9
directory2 directory5 directory8 file10 file4 file7
ruby ls.rb test 0.10s user 0.07s system 86% cpu 0.191 total
# ruby ls.rbの実行時間 0.10s
目で見てわかるぐらいにはls
よりも遅いですね。
所感
実用性はあまりないですが、かなりlsコマンドを再現できたかと思います。
車輪の再開発と呼ぶには拙いコードですが、取り組む前よりも実装力は上がったと思います。
期日に余裕があればテストを書いたり、機能拡張をしたかったです。
また、常にドキュメント片手に作業をしていたので、ドキュメントを読み漁る力も身についたと思います。
当記事では解説はできませんでしたが、ls -l
の実装も大変学びが多かったです。
おわりに
当アドベントカレンダーはWeb系の記事が多いので少し変わったことを記事にしたいと思いました。
他の方よりも、直接学びになることは少ないかもしれませんが、少しでも参考になりましたら幸いです。
いよいよ明日が最終日です。
@hide9138の記事で当アドベントカレンダーが良い締めくくりを迎えれるよう願っています。
それでは、良いクリスマスを
完成コード
Github : https://github.com/shinooooo/Ruby-sh