Rubyでlsコマンド実装

## はじめに
こんにちは！
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 - ディレクトリの中身をリスト表示する

と書かれています。

実際に実行すると、

```shell
$ ls
hoge huga index.html     #カレントディレクトリ内のファイルが出力される。
```

このような表示が出てくるかと思います。

今回は、このlsコマンドをRubyで実装し、

```sh
$ ruby ls.rb
hoge huga index.html
```

と出すことが目標です。

## 仕様
今回実装した`ls.rb`の仕様は、下記の通りです。

- `ls`,`ls -l`,`ls -a`, `ls -la`と同等の機能を実装
    - 'optparse'というライブラリを使った都合上、`-la`のように複数のオプションをまとめて指定することができない。組み合わせる場合は、`-l -a`と入力する必要がある。
- ACLと呼ばれる機能(説明は割愛します)の表示は未実装
- 実際の`ls`のソースコードを参考にしていないので、アルゴリズムや関数名が違うかもしれません。



## 出力形式
いきなりコードを書いて模倣することはできないので、`ls`の出力の分析を行います。
ディレクトリとファイルを用意した方がわかりやすいので、下記のコマンドを実行します。

```sh
# directory1~directory10とfile1~file10をtestディレクトリ内に作成。
$ mkdir test/directory{1..10} && touch test/file{1..10} 
```

そして、`ls test`を実行して出力結果を確認します。

```Ruby
$ 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`の出力は以下のようになっていました。

```sh:表示されている順番
名前が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クラス](https://docs.ruby-lang.org/ja/latest/class/Dir.html)は、ファイル一覧を配列で渡してくれます。今回は、これをfilesという変数に代入して扱うこととします。
ですが、受け取った配列の中身が昇順ではなかったので[Arrayクラス](https://docs.ruby-lang.org/ja/latest/class/Array.html)の`sort`メソッドで昇順に並び替えます。

```ruby: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`をうまく出力する処理を書くと、下記のような出力になります。

```sh
$ ruby ls.rb test
directory1directory3directory6directory9file2file5file8
directory10directory4directory7file1file3file6file9
directory2directory5directory8file10file4file7
```

可読性は低いですが、一歩`ls`に近づきました。

### ファイル名の間の区切り
このままだとどこからどこまでが1つのファイル名か分からないので、ファイルとファイルを区切る必要があります。
`ls`の出力結果のファイル名とファイル名はどう区切られているか、確認していきたいと思います。
適当に`print.rb`というような名前のファイルを作成し、`ls test`の実行結果の

```sh
directory1      directory3
```
をコピーし、貼り付けます。

これを文字列としてターミナルに出力してみたいと思います。
`" "`で囲み、先頭に`p`を付け、

```Ruby:print.rb
p "directory1	directory13"
```
として保存します。

ターミナル上で`ruby print.rb`と実行してみると以下のように出力されます。

```sh
$ ruby print.rb
"directory1\tdirectory13"
```

directory1とdirectory3の間に`\t`という文字が現れました。
これは、タブ文字を表しています。
このことから、ファイル名がタブ文字で区切られていることが確認できます。

これを模倣してファイル名の末尾に`\t`を追加することにします。
そのために`printf`メソッドを使用します。
ここでは`printf`について詳しく解説はしないので、気になる方は[ドキュメント](https://docs.ruby-lang.org/ja/latest/class/Kernel.html#M_PRINTF)を読んでみてください。

```Ruby:printfの使用例
printf("%s\t", files[0]) # => %sの中にfiles[0]の中身が入り、出力される。
printf("%s\t", files[1])
# => "directory1	""directory10	"
```

`ls.rb`にも実装すると、以下のような出力になります。

```sh
$ 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`を使い、考えてみましょう。
現時点での出力はこのようになっています。

```sh
directory10\tfile2 
file1\tfile3
```

ファイル名の長さが統一されていないことで、1行毎の出力に文字数のズレが生じています。

全てのファイル名の長さが同じであれば、

```sh
ファイル名\tファイル名\t
ファイル名\tファイル名\t
```
のように綺麗な見た目にすることができるので空白文字を用いて、文字数が一番長いファイル名の長さに統一します。
今回の例では、`directory1`が一番ファイル名が長いので,`directory1`の長さに統一することにします。

```sh
# 空白文字を使うことで見た目を整えることができる！
directory\tfile2    \t 
file1    \tfile3    \t
```
文字列の長さの統一も`printf`メソッドで実現することができます。
`%s`を少し改変することで幅指定を行うことができます。

```Ruby:printfの幅指定
printf("%15s", files[0])
# => "     directory1" 15文字になるよう空白文字が追加される。

printf("%-15s", files[0])
# => "directory1     " -をつけると左詰めになる。

name_len = 15

printf("%-#{name_len}s\t", files[0]) # => 式展開もできる。
# => "directory1     "
```

`ls.rb`でも幅指定をすると

```sh
$ 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

```Ruby
  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`コマンドを使い、実行時間を計測します。

```sh
$ 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の記事で当アドベントカレンダーが良い締めくくりを迎えれるよう願っています。
それでは、良いクリスマスを:thumbsup:
## 完成コード
Github : https://github.com/shinooooo/Ruby-sh

## 参考
[Ruby 2.7.0 リファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/doc/index.html)
[Man page of LS](https://linuxjm.osdn.jp/html/gnumaniak/man1/ls.1.html)