Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

この記事は Go5 Advent Calendar 2019 の 8日目の記事です!

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

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

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

lsコマンド実装

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

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 とします。

結果
% ./go-ls
.idea
go-ls
main.go

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

-aオプションの実装

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

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())
    }
}

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

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

-tオプションの実装

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

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のパラメータチェック等を行う必要がなくなります。

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に置いておきます。
少しずつ改修していこうと思います!

ttakuya50
都内勤務のエンジニア。 天下一品大好き。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away