golangでlsコマンドを作ってみる

この記事は [Go5 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/go5) の 8日目の記事です！

当初、「golang×webassemblyで遊んでみる」みたいなタイトルで記事を書いていたのですが
前日にPCがぶっ壊れて記事やコードやら全部吹っ飛んだので予定を変更して「golangでlsコマンドを作ってみる」でお送りします。
バックアップ大事。

ネタ元は週末に行われたgolang社会勉強会です。ネタをパクることをお許しくださいS先輩。

それではいきましょう！Go5!

## lsコマンド実装
始めにオプションなどの実装を一切考えない単純なlsコマンドを実装してみましたが、以外に少ないコード量で実装できたのでビックリしました。

```golang
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
)

func main() {
	dir, err := os.Getwd()  // カレントディレクトリ情報取得
	if err != nil {
		log.Fatal(err)
	}

	fileInfos, err := ioutil.ReadDir(dir)
	if err != nil {
		log.Fatal(err)
	}

	for _, fileInfo := range fileInfos {
		fmt.Println(fileInfo.Name())
	}
}
```

本来のlsコマンドと区別するためにコマンド名を **go-ls** とします。

```tex:結果
% ./go-ls
.idea
go-ls
main.go
```

一応それっぽくはなってますね！
ただし上記の実装だと、本来のlsコマンドとは違う挙動になります。
それは **「隠しファイルも表示してしまう」** という点です。
本来`ls -a`で隠しファイルも表示するので-aオプションを引数にした場合だけ隠しファイルを表示するように変更してみます。

## -aオプションの実装
というわけで-aオプションを実装してみました。
実務で書いたらバグだらけで怒られそうですが、そこはご愛嬌ということで。

```golang
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"strings"
)

func main() {
	// カレントディレクトリ取得
	dir, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}

	fileInfos, err := ioutil.ReadDir(dir)
	if err != nil {
		log.Fatal(err)
	}

	if 2 <= len(os.Args) {
		if os.Args[1] == "-a" {
			for _, fileInfo := range fileInfos {
				fmt.Println(fileInfo.Name())
			}
			return
		}
	}

	for _, fileInfo := range fileInfos {
		// 隠しファイルは非表示
		if strings.HasPrefix(fileInfo.Name(), ".") {
			continue
		}

		fmt.Println(fileInfo.Name())
	}
}

```

```tex:-aオプションをつけない結果
% ./go-ls   
go-ls
main.go
```

```tex:-aオプションをつけた結果
% ./go-ls -a
.idea
go-ls
main.go
```

-aオプションをつけた場合だけ隠しファイルを表示させることが出来ました！
`os.Args`で引数に-a文字列があった場合に隠しファイルを表示させます。(上記の実装の場合、-a以外の文字列を指定して実行できちゃいます)
-aオプションを付けない場合は「.」がついているファイルは隠しファイルだと判断して表示しません。

## -tオプションの実装

続いて、-tオプションの実装をしたいと思います。
-tオプションは「更新時間ごとにファイルを並び替える」オプションです。

```golang
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"sort"
	"strings"
)

func main() {
	// カレントディレクトリ取得
	dir, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}

	fileInfos, err := ioutil.ReadDir(dir)
	if err != nil {
		log.Fatal(err)
	}

	if 2 <= len(os.Args) {
		// 全て表示
		if os.Args[1] == "-a" {
			for _, fileInfo := range fileInfos {
				fmt.Println(fileInfo.Name())
			}
			return
		}

		// 更新時間順にソート
		if os.Args[1] == "-t" {
			sort.Slice(fileInfos, func(i, j int) bool { return fileInfos[i].ModTime().After(fileInfos[j].ModTime()) })

			for _, fileInfo := range fileInfos {
				fmt.Println(fileInfo.Name())
			}
			return
		}

	}

	for _, fileInfo := range fileInfos {
		// 隠しファイルは非表示
		if strings.HasPrefix(fileInfo.Name(), ".") {
			continue
		}

		fmt.Println(fileInfo.Name())
	}
}
```

変数`fileInfo`の中に`ModTime`メソッドがあります。
そのメソッドを使用することでディレクトリ配下のファイル・ディレクトリの更新時間を取得できます。
あとはsortパッケージを使用してソートしてあげれば-tオプションの出来上がりです。

## flagパッケージを使う
今までのソースコードではコマンドライン引数を`os.Args`で取得していました。
それでも良いのですが、より高機能なflagパッケージを使ってみます。
flagパッケージを使用することで`os.Args`のパラメータチェック等を行う必要がなくなります。

```golang
package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"sort"
	"strings"
)

func main() {
	aFlg := flag.Bool("a", false, "Include directory entries whose names begin with a dot (.).")
	tFlg := flag.Bool("t", false, "Sort by time modified (most recently modified first) before sorting the operands by lexicographical order.")
	flag.Parse()

	// カレントディレクトリ取得
	dir, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}

	fileInfos, err := ioutil.ReadDir(dir)
	if err != nil {
		log.Fatal(err)
	}

	// 全て表示
	if *aFlg {
		for _, fileInfo := range fileInfos {
			fmt.Println(fileInfo.Name())
		}
		return
	}

	// 更新時間順にソート
	if *tFlg {
		sort.Slice(fileInfos, func(i, j int) bool { return fileInfos[i].ModTime().After(fileInfos[j].ModTime()) })

		for _, fileInfo := range fileInfos {
			// 隠しファイルは非表示
			if strings.HasPrefix(fileInfo.Name(), ".") {
				continue
			}

			fmt.Println(fileInfo.Name())
		}
		return
	}

	// オプションなし
	for _, fileInfo := range fileInfos {
		// 隠しファイルは非表示
		if strings.HasPrefix(fileInfo.Name(), ".") {
			continue
		}

		fmt.Println(fileInfo.Name())
	}
}
```

## まとめ
思ったよりも少ないコード量でlsコマンドっぽいものが作れました。
テストコードの書きやすさだったり、メンテナンスのしやすさを考えるともっと別の書き方があるとは思いますが・・・
(まだまだバグもありますし)
本当はもっとオプションも実装したかったのですが、そろそろ時間がなくなってきたのでこのあたりで終わります！
これからもみんなで楽しんでgolangを書いていきましょう〜！

今回のgo-lsコマンドのコードを[github](https://github.com/TsujiTakuya55/go-ls)に置いておきます。
少しずつ改修していこうと思います！