Rustで簡易lsコマンドを作る

## はじめに

Rustでファイルの一覧を表示させる簡単なコマンドラインツールを開発しました。
隠しファイルも含め全てのファイルを表示させます。
引数として受け取れるのはパスだけでオプションはありません。

## 使用例

ツール名は`rls`にしました。
引数なしの場合はカレントディレクトリになります。

```
$ ./rls 
.cargo-lock
.fingerprint
build
deps
examples
incremental
rls
rls.d
```

`~`でホームディレクトリを指定できます
ファイル数が多いので一部省略しています。
```
$ ./rls ~/
.bash_history
.bash_logout
...
Videos
```

## 解説

実装した処理について簡単に解説します。

### dirsクレートを使用

ホームディレクトリを取得するには`std::env::home_dir`を使用すればよいですが、この関数は現在非推奨です。

```
warning: use of deprecated function `std::env::home_dir`: This function's behavior is unexpected and probably not what you want. Consider using a crate from crates.io instead.
```

代わりに`dirs`クレートにある`dirs::home_dir`を使用します。
`Cargo.toml`に以下を追記します。

```toml
[dependencies]
dirs = "4.0.0"
```

### パスの文字列を解析する

パスの文字列を解析し`PathBuf`として返す関数を追加します。
先頭が`~`の場合はホームディレクトリのパスに置き換えます。

```rust
fn parse_path(path: &str) -> PathBuf {
    if path.starts_with("~") {
        let mut home_dir = dirs::home_dir().unwrap();
        path[1..].split("/").for_each(|f| home_dir.push(f));
        return home_dir;
    }
    PathBuf::from(path)
}
```

### ファイルの一覧を取得する

指定したディレクトリ内のファイルの一覧を返す関数を追加します。
一覧をファイル名でソートしてから返します。

フォルダが見つからない、またはディレクトリ以外が指定されていた場合はエラーメッセージを返します。

```rust
fn get_dir_entries(dir: &PathBuf) -> Result<Vec<DirEntry>, String> {
    if !dir.exists() {
        let mes = format!("{} が見つかりませんでした。", dir.to_str().unwrap());
        return Err(mes);
    }
    if !dir.is_dir() {
        let mes = format!("{} はディレクトリではありません。", dir.to_str().unwrap());
        return Err(mes);
    }
    let mut entries: Vec<DirEntry> = fs::read_dir(dir).unwrap().filter_map(|f| f.ok()).collect();
    entries.sort_by_key(|e| e.file_name());
    Ok(entries)
}
```

### ファイル名を取得する

`DirEntry`からファイル名を取得する関数を追加します。
`OsString`を`String`に変換します。

```rust
fn get_file_name(entry: &DirEntry) -> String {
    entry.file_name().into_string().unwrap()
}
```

#### OsStringとは

プラットフォーム依存の文字列型です。
通常の`String`はUTF-8ですが`OsString`のエンコーディングはOSによって異なります。
公式のドキュメントによるとUnix系OSの場合はUTF-8で、Windowsの場合はUTF-16です。

## コード

最後にコード全体を載せます。

<details><summary>コード</summary>

```rust
use dirs;
use std::{
    env,
    fs::{self, DirEntry},
    path::PathBuf,
    process,
};

fn parse_path(path: &str) -> PathBuf {
    if path.starts_with("~") {
        let mut home_dir = dirs::home_dir().unwrap();
        path[1..].split("/").for_each(|f| home_dir.push(f));
        return home_dir;
    }
    PathBuf::from(path)
}

fn get_dir_entries(dir: &PathBuf) -> Result<Vec<DirEntry>, String> {
    if !dir.exists() {
        let mes = format!("{} が見つかりませんでした。", dir.to_str().unwrap());
        return Err(mes);
    }
    if !dir.is_dir() {
        let mes = format!("{} はディレクトリではありません。", dir.to_str().unwrap());
        return Err(mes);
    }
    let mut entries: Vec<DirEntry> = fs::read_dir(dir).unwrap().filter_map(|f| f.ok()).collect();
    entries.sort_by_key(|e| e.file_name());
    Ok(entries)
}

fn get_file_name(entry: &DirEntry) -> String {
    entry.file_name().into_string().unwrap()
}

fn main() {
    let args: Vec<String> = env::args().skip(1).collect();
    let path = if args.len() == 0 { "." } else { &args[0] };
    let dir = parse_path(path);
    match get_dir_entries(&dir) {
        Ok(entries) => {
            entries
                .iter()
                .for_each(|e| println!("{}", get_file_name(e)));
        }
        Err(mes) => {
            println!("{}", mes);
            process::exit(1);
        }
    }
}
```

</details>

## 参考文献

https://crates.io/crates/dirs

https://doc.rust-lang.org/nightly/std/fs/fn.read_dir.html

https://doc.rust-lang.org/std/ffi/struct.OsString.html